Skip to content

Commit

Permalink
feat(dot/parachain): implement candidate validation from chainstate (#…
Browse files Browse the repository at this point in the history
…4067)

Co-authored-by: Timothy Wu <tim.wu@chainsafe.io>
  • Loading branch information
edwardmack and timwu20 authored Aug 12, 2024
1 parent 9150342 commit a58b1a4
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 95 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

104 changes: 36 additions & 68 deletions dot/parachain/candidate-validation/candidate_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package candidatevalidation

import (
"bytes"
"context"
"errors"
"fmt"
Expand All @@ -14,6 +13,7 @@ import (
parachaintypes "github.com/ChainSafe/gossamer/dot/parachain/types"
"github.com/ChainSafe/gossamer/internal/log"
"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/lib/runtime"
"github.com/ChainSafe/gossamer/pkg/scale"
)

Expand All @@ -32,12 +32,18 @@ type CandidateValidation struct {
SubsystemToOverseer chan<- any
OverseerToSubsystem <-chan any
ValidationHost parachainruntime.ValidationHost
BlockState BlockState
}

type BlockState interface {
GetRuntime(blockHash common.Hash) (instance runtime.Instance, err error)
}

// NewCandidateValidation creates a new CandidateValidation subsystem
func NewCandidateValidation(overseerChan chan<- any) *CandidateValidation {
func NewCandidateValidation(overseerChan chan<- any, blockState BlockState) *CandidateValidation {
candidateValidation := CandidateValidation{
SubsystemToOverseer: overseerChan,
BlockState: blockState,
}
return &candidateValidation
}
Expand Down Expand Up @@ -80,7 +86,24 @@ func (cv *CandidateValidation) processMessages(wg *sync.WaitGroup) {
logger.Debugf("received message %v", msg)
switch msg := msg.(type) {
case ValidateFromChainState:
// TODO: implement functionality to handle ValidateFromChainState, see issue #3919
runtimeInstance, err := cv.BlockState.GetRuntime(msg.CandidateReceipt.Descriptor.RelayParent)
if err != nil {
logger.Errorf("failed to get runtime: %w", err)
msg.Ch <- parachaintypes.OverseerFuncRes[ValidationResult]{
Err: err,
}
}
result, err := validateFromChainState(runtimeInstance, msg.Pov, msg.CandidateReceipt)
if err != nil {
logger.Errorf("failed to validate from chain state: %w", err)
msg.Ch <- parachaintypes.OverseerFuncRes[ValidationResult]{
Err: err,
}
} else {
msg.Ch <- parachaintypes.OverseerFuncRes[ValidationResult]{
Data: *result,
}
}
case ValidateFromExhaustive:
result, err := validateFromExhaustive(cv.ValidationHost, msg.PersistedValidationData,
msg.ValidationCode, msg.CandidateReceipt, msg.PoV)
Expand Down Expand Up @@ -155,82 +178,27 @@ func getValidationData(runtimeInstance parachainruntime.RuntimeInstance, paraID

// validateFromChainState validates a candidate parachain block with provided parameters using relay-chain
// state and using the parachain runtime.
func validateFromChainState(runtimeInstance parachainruntime.RuntimeInstance, povRequestor PoVRequestor,
func validateFromChainState(runtimeInstance parachainruntime.RuntimeInstance, pov parachaintypes.PoV,
candidateReceipt parachaintypes.CandidateReceipt) (
*parachaintypes.CandidateCommitments, *parachaintypes.PersistedValidationData, bool, error) {

persistedValidationData, validationCode, err := getValidationData(runtimeInstance, candidateReceipt.Descriptor.ParaID)
if err != nil {
return nil, nil, false, fmt.Errorf("getting validation data: %w", err)
}

// check that the candidate does not exceed any parameters in the persisted validation data
pov := povRequestor.RequestPoV(candidateReceipt.Descriptor.PovHash)

// basic checks

// check if encoded size of pov is less than max pov size
buffer := bytes.NewBuffer(nil)
encoder := scale.NewEncoder(buffer)
err = encoder.Encode(pov)
if err != nil {
return nil, nil, false, fmt.Errorf("encoding pov: %w", err)
}
encodedPoVSize := buffer.Len()
if encodedPoVSize > int(persistedValidationData.MaxPovSize) {
return nil, nil, false, fmt.Errorf("%w, limit: %d, got: %d", ErrValidationInputOverLimit,
persistedValidationData.MaxPovSize, encodedPoVSize)
}

validationCodeHash, err := common.Blake2bHash([]byte(*validationCode))
if err != nil {
return nil, nil, false, fmt.Errorf("hashing validation code: %w", err)
}

if validationCodeHash != common.Hash(candidateReceipt.Descriptor.ValidationCodeHash) {
return nil, nil, false, fmt.Errorf("%w, expected: %s, got %s", ErrValidationCodeMismatch,
candidateReceipt.Descriptor.ValidationCodeHash, validationCodeHash)
}
*ValidationResult, error) {

// check candidate signature
err = candidateReceipt.Descriptor.CheckCollatorSignature()
persistedValidationData, validationCode, err := getValidationData(runtimeInstance,
candidateReceipt.Descriptor.ParaID)
if err != nil {
return nil, nil, false, fmt.Errorf("verifying collator signature: %w", err)
}

validationParams := parachainruntime.ValidationParameters{
ParentHeadData: persistedValidationData.ParentHead,
BlockData: pov.BlockData,
RelayParentNumber: persistedValidationData.RelayParentNumber,
RelayParentStorageRoot: persistedValidationData.RelayParentStorageRoot,
return nil, fmt.Errorf("getting validation data: %w", err)
}

parachainRuntimeInstance, err := parachainruntime.SetupVM(*validationCode)
if err != nil {
return nil, nil, false, fmt.Errorf("setting up VM: %w", err)
return nil, fmt.Errorf("setting up VM: %w", err)
}

validationResults, err := parachainRuntimeInstance.ValidateBlock(validationParams)
validationResults, err := validateFromExhaustive(parachainRuntimeInstance, *persistedValidationData,
*validationCode, candidateReceipt, pov)
if err != nil {
return nil, nil, false, fmt.Errorf("executing validate_block: %w", err)
return nil, fmt.Errorf("validating from exhaustive: %w", err)
}

candidateCommitments := parachaintypes.CandidateCommitments{
UpwardMessages: validationResults.UpwardMessages,
HorizontalMessages: validationResults.HorizontalMessages,
NewValidationCode: validationResults.NewValidationCode,
HeadData: validationResults.HeadData,
ProcessedDownwardMessages: validationResults.ProcessedDownwardMessages,
HrmpWatermark: validationResults.HrmpWatermark,
}

isValid, err := runtimeInstance.ParachainHostCheckValidationOutputs(
candidateReceipt.Descriptor.ParaID, candidateCommitments)
if err != nil {
return nil, nil, false, fmt.Errorf("executing validate_block: %w", err)
}

return &candidateCommitments, persistedValidationData, isValid, nil
return validationResults, nil
}

// validateFromExhaustive validates a candidate parachain block with provided parameters
Expand Down
197 changes: 185 additions & 12 deletions dot/parachain/candidate-validation/candidate_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,12 @@ func TestValidateFromChainState(t *testing.T) {
mockInstance.EXPECT().
ParachainHostValidationCode(uint32(1000), gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&validationCode, nil)
mockInstance.EXPECT().
ParachainHostCheckValidationOutputs(uint32(1000), gomock.AssignableToTypeOf(parachaintypes.CandidateCommitments{})).
Return(true, nil)

mockPoVRequestor := NewMockPoVRequestor(ctrl)
mockPoVRequestor.EXPECT().
RequestPoV(common.MustHexToHash("0xb608991ffc48dd405fd4b10e92eaebe2b5a2eedf44d0c3efb8997fdee8bebed9")).Return(pov)

candidateCommitments, persistedValidationData, isValid, err := validateFromChainState(
mockInstance, mockPoVRequestor, candidateReceipt)
validationResult, err := validateFromChainState(
mockInstance, pov, candidateReceipt)
require.NoError(t, err)
require.True(t, isValid)
require.NotNil(t, candidateCommitments)
require.Equal(t, expectedPersistedValidationData, *persistedValidationData)
require.True(t, validationResult.IsValid())
require.Equal(t, expectedPersistedValidationData, validationResult.ValidResult.PersistedValidationData)
}

type HeadDataInAdderParachain struct {
Expand Down Expand Up @@ -677,3 +669,184 @@ func Test_performBasicChecks(t *testing.T) {
})
}
}

func TestCandidateValidation_validateFromChainState(t *testing.T) {
t.Parallel()
candidateReceipt, validationCode := createTestCandidateReceiptAndValidationCode(t)
candidateReceipt2 := candidateReceipt
candidateReceipt2.Descriptor.PovHash = common.MustHexToHash(
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
candidateReceipt2.Descriptor.ParaID = 2

candidateReceipt3 := candidateReceipt
candidateReceipt3.Descriptor.ParaID = 3

candidateReceipt4 := candidateReceipt
candidateReceipt4.Descriptor.ParaID = 4
candidateReceipt4.Descriptor.ValidationCodeHash = parachaintypes.ValidationCodeHash(common.MustHexToHash(
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"))

candidateReceipt5 := candidateReceipt
candidateReceipt5.Descriptor.ParaID = 5

povHashMismatch := PoVHashMismatch
paramsTooLarge := ParamsTooLarge
codeHashMismatch := CodeHashMismatch
badSignature := BadSignature

ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)

// NOTE: adder parachain internally compares postState with bd.State in it's validate_block,
// so following is necessary.
encodedState, err := scale.Marshal(uint64(1))
require.NoError(t, err)
postState, err := common.Keccak256(encodedState)
require.NoError(t, err)

hd, err := scale.Marshal(HeadDataInAdderParachain{
Number: uint64(1),
ParentHash: common.MustHexToHash("0x0102030405060708090001020304050607080900010203040506070809000102"),
PostState: postState,
})
require.NoError(t, err)

expectedPersistedValidationData := parachaintypes.PersistedValidationData{
ParentHead: parachaintypes.HeadData{Data: hd},
RelayParentNumber: uint32(1),
RelayParentStorageRoot: common.MustHexToHash("0x50c969706800c0e9c3c4565dc2babb25e4a73d1db0dee1bcf7745535a32e7ca1"),
MaxPovSize: uint32(2048),
}

expectedPersistedValidationDataSmallMax := parachaintypes.PersistedValidationData{
ParentHead: parachaintypes.HeadData{Data: hd},
RelayParentNumber: uint32(1),
RelayParentStorageRoot: common.MustHexToHash("0x50c969706800c0e9c3c4565dc2babb25e4a73d1db0dee1bcf7745535a32e7ca1"),
MaxPovSize: uint32(10),
}

mockInstance := NewMockRuntimeInstance(ctrl)
mockInstance.EXPECT().
ParachainHostPersistedValidationData(
uint32(1000),
gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&expectedPersistedValidationData, nil)
mockInstance.EXPECT().
ParachainHostValidationCode(uint32(1000), gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&validationCode, nil)

mockInstance.EXPECT().
ParachainHostPersistedValidationData(
uint32(2),
gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&expectedPersistedValidationData, nil)
mockInstance.EXPECT().
ParachainHostValidationCode(uint32(2), gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&validationCode, nil)

mockInstance.EXPECT().
ParachainHostPersistedValidationData(
uint32(3),
gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&expectedPersistedValidationDataSmallMax, nil)
mockInstance.EXPECT().
ParachainHostValidationCode(uint32(3), gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&validationCode, nil)

mockInstance.EXPECT().
ParachainHostPersistedValidationData(
uint32(4),
gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&expectedPersistedValidationData, nil)
mockInstance.EXPECT().
ParachainHostValidationCode(uint32(4), gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&validationCode, nil)

mockInstance.EXPECT().
ParachainHostPersistedValidationData(
uint32(5),
gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&expectedPersistedValidationData, nil)
mockInstance.EXPECT().
ParachainHostValidationCode(uint32(5), gomock.AssignableToTypeOf(parachaintypes.OccupiedCoreAssumption{})).
Return(&validationCode, nil)

bd, err := scale.Marshal(BlockDataInAdderParachain{
State: uint64(1),
Add: uint64(1),
})
require.NoError(t, err)
pov := parachaintypes.PoV{
BlockData: bd,
}

tests := map[string]struct {
candidateReceipt parachaintypes.CandidateReceipt
want *ValidationResult
expectedError error
}{
"invalid_pov_hash": {
candidateReceipt: candidateReceipt2,
want: &ValidationResult{
InvalidResult: &povHashMismatch,
},
},
"invalid_pov_size": {
candidateReceipt: candidateReceipt3,
want: &ValidationResult{
InvalidResult: &paramsTooLarge,
},
},
"code_mismatch": {
candidateReceipt: candidateReceipt4,
want: &ValidationResult{
InvalidResult: &codeHashMismatch,
},
},
"bad_signature": {
candidateReceipt: candidateReceipt5,
want: &ValidationResult{
InvalidResult: &badSignature,
},
},
"happy_path": {
candidateReceipt: candidateReceipt,
want: &ValidationResult{
ValidResult: &ValidValidationResult{
CandidateCommitments: parachaintypes.CandidateCommitments{
HeadData: parachaintypes.HeadData{Data: []byte{2, 0, 0, 0, 0, 0, 0, 0, 123, 207, 206, 8, 219, 227,
136, 82, 236, 169, 14, 100, 45, 100, 31, 177, 154, 160, 220, 245, 59, 106, 76, 168, 122, 109,
164, 169, 22, 46, 144, 39, 103, 92, 31, 78, 66, 72, 252, 64, 24, 194, 129, 162, 128, 1, 77, 147,
200, 229, 189, 242, 111, 198, 236, 139, 16, 143, 19, 245, 113, 233, 138, 210}},
ProcessedDownwardMessages: 0,
HrmpWatermark: 1,
},
PersistedValidationData: parachaintypes.PersistedValidationData{
ParentHead: parachaintypes.HeadData{Data: []byte{1, 0, 0, 0, 0, 0, 0, 0, 1,
2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 0, 1, 2, 48, 246, 146, 178, 86, 226, 64, 9,
188, 179, 77, 14, 232, 77, 167, 60, 41, 138, 250, 204, 9, 36, 224, 17, 5, 226, 235,
15, 1, 168, 127, 226}},
RelayParentNumber: 1,
RelayParentStorageRoot: common.MustHexToHash(
"0x50c969706800c0e9c3c4565dc2babb25e4a73d1db0dee1bcf7745535a32e7ca1"),
MaxPovSize: 2048,
},
},
},
},
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
t.Parallel()
got, err := validateFromChainState(mockInstance, pov, tt.candidateReceipt)
if tt.expectedError != nil {
require.EqualError(t, err, tt.expectedError.Error())
} else {
require.NoError(t, err)
}
require.Equal(t, *tt.want, *got)
})
}
}
2 changes: 1 addition & 1 deletion dot/parachain/candidate-validation/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type ValidateFromChainState struct {
Pov parachaintypes.PoV
ExecutorParams parachaintypes.ExecutorParams
ExecKind parachaintypes.PvfExecTimeoutKind
Sender chan ValidateFromExhaustive
Ch chan parachaintypes.OverseerFuncRes[ValidationResult]
}

// ValidateFromExhaustive performs full validation of a candidate with provided parameters,
Expand Down
2 changes: 1 addition & 1 deletion dot/parachain/candidate-validation/mocks_runtime_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a58b1a4

Please sign in to comment.