Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dot/parachain): implement candidate validation from chainstate #4067

Merged
merged 12 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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

97 changes: 30 additions & 67 deletions dot/parachain/candidate-validation/candidate_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
package candidatevalidation

import (
"bytes"
"context"
"errors"
"fmt"
"sync"

parachainruntime "github.com/ChainSafe/gossamer/dot/parachain/runtime"
parachaintypes "github.com/ChainSafe/gossamer/dot/parachain/types"
"github.com/ChainSafe/gossamer/dot/state"
"github.com/ChainSafe/gossamer/internal/log"
"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/pkg/scale"
Expand All @@ -32,6 +32,7 @@ type CandidateValidation struct {
SubsystemToOverseer chan<- any
OverseerToSubsystem <-chan any
ValidationHost parachainruntime.ValidationHost
BlockState state.BlockState
}

// NewCandidateValidation creates a new CandidateValidation subsystem
Expand Down Expand Up @@ -80,7 +81,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 +173,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
203 changes: 191 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,190 @@ 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)
axaysagathiya marked this conversation as resolved.
Show resolved Hide resolved

// 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
isValid bool
}{
"invalid_pov_hash": {
candidateReceipt: candidateReceipt2,
want: &ValidationResult{
InvalidResult: &povHashMismatch,
},
isValid: false,
},
"invalid_pov_size": {
candidateReceipt: candidateReceipt3,
want: &ValidationResult{
InvalidResult: &paramsTooLarge,
},
},
"code_mismatch": {
candidateReceipt: candidateReceipt4,
want: &ValidationResult{
InvalidResult: &codeHashMismatch,
},
isValid: false,
},
"bad_signature": {
candidateReceipt: candidateReceipt5,
want: &ValidationResult{
InvalidResult: &badSignature,
},
isValid: false,
},
"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,
},
},
},
isValid: true,
},
}
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.isValid, got.IsValid())
require.Equal(t, *tt.want, *got)
kishansagathiya marked this conversation as resolved.
Show resolved Hide resolved
})
}
}
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
Loading