Skip to content

Commit

Permalink
feat(lib/parachain): introduced parachain candidate validation (#3249)
Browse files Browse the repository at this point in the history
This commit adds all the functions required for parachain candidate validations.

With this commit, we are able to take candidate receipts and
- get validation data for it,
- perform basic checks on it,
- run respective parachain's validate_block on parachain's runtime,
- get validate results from parachain's runtime and verify those validation results again relaychain runtime,
- and declare candidate as valid or invalid in the end.

This commit also includes tests for candidate validation.
  • Loading branch information
kishansagathiya committed Jan 24, 2024
1 parent 74a8c52 commit 1c35f41
Show file tree
Hide file tree
Showing 23 changed files with 595 additions and 16 deletions.
3 changes: 3 additions & 0 deletions lib/parachain/available_data_fetching.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
Expand Down
3 changes: 3 additions & 0 deletions lib/parachain/available_data_fetching_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
Expand Down
139 changes: 139 additions & 0 deletions lib/parachain/candidate_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
"bytes"
"errors"
"fmt"

"github.com/ChainSafe/gossamer/lib/common"
parachainruntime "github.com/ChainSafe/gossamer/lib/parachain/runtime"
parachaintypes "github.com/ChainSafe/gossamer/lib/parachain/types"

"github.com/ChainSafe/gossamer/pkg/scale"
)

var (
ErrValidationCodeMismatch = errors.New("validation code hash does not match")
ErrValidationInputOverLimit = errors.New("validation input is over the limit")
)

// PoVRequestor gets proof of validity by issuing network requests to validators of the current backing group.
// TODO: Implement PoV requestor
type PoVRequestor interface {
RequestPoV(povHash common.Hash) PoV
}

func getValidationData(runtimeInstance parachainruntime.RuntimeInstance, paraID uint32,
) (*parachaintypes.PersistedValidationData, *parachaintypes.ValidationCode, error) {

var mergedError error

for _, assumptionValue := range []scale.VaryingDataTypeValue{
parachaintypes.IncludedOccupiedCoreAssumption{},
parachaintypes.TimedOutOccupiedCoreAssumption{},
parachaintypes.Free{},
} {
assumption := parachaintypes.NewOccupiedCoreAssumption()
err := assumption.Set(assumptionValue)
if err != nil {
return nil, nil, fmt.Errorf("getting assumption: %w", err)
}
persistedValidationData, err := runtimeInstance.ParachainHostPersistedValidationData(paraID, assumption)
if err != nil {
mergedError = errors.Join(mergedError, err)
continue
}

validationCode, err := runtimeInstance.ParachainHostValidationCode(paraID, assumption)
if err != nil {
return nil, nil, fmt.Errorf("getting validation code: %w", err)
}

return persistedValidationData, validationCode, nil
}

return nil, nil, fmt.Errorf("getting persisted validation data: %w", mergedError)
}

// 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,
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)
}

// check candidate signature
err = candidateReceipt.Descriptor.CheckCollatorSignature()
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,
}

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

validationResults, err := parachainRuntimeInstance.ValidateBlock(validationParams)
if err != nil {
return nil, nil, false, fmt.Errorf("executing validate_block: %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
}
134 changes: 134 additions & 0 deletions lib/parachain/candidate_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
"os"
"testing"

"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/lib/crypto/sr25519"
parachaintypes "github.com/ChainSafe/gossamer/lib/parachain/types"
"github.com/ChainSafe/gossamer/pkg/scale"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

func createTestCandidateReceiptAndValidationCode(t *testing.T) (
parachaintypes.CandidateReceipt, parachaintypes.ValidationCode) {
// this wasm was achieved by building polkadot's adder test parachain
runtimeFilePath := "./testdata/test_parachain_adder.wasm"
validationCodeBytes, err := os.ReadFile(runtimeFilePath)
require.NoError(t, err)

validationCode := parachaintypes.ValidationCode(validationCodeBytes)

validationCodeHashV := common.MustBlake2bHash(validationCodeBytes)

collatorKeypair, err := sr25519.GenerateKeypair()
require.NoError(t, err)
collatorID, err := sr25519.NewPublicKey(collatorKeypair.Public().Encode())
require.NoError(t, err)

candidateReceipt := parachaintypes.CandidateReceipt{
Descriptor: parachaintypes.CandidateDescriptor{
ParaID: uint32(1000),
RelayParent: common.MustHexToHash("0xded542bacb3ca6c033a57676f94ae7c8f36834511deb44e3164256fd3b1c0de0"), //nolint:lll
Collator: collatorID.AsBytes(),
PersistedValidationDataHash: common.MustHexToHash("0x690d8f252ef66ab0f969c3f518f90012b849aa5ac94e1752c5e5ae5a8996de37"), //nolint:lll
PovHash: common.MustHexToHash("0xe7df1126ac4b4f0fb1bc00367a12ec26ca7c51256735a5e11beecdc1e3eca274"), //nolint:lll
ErasureRoot: common.MustHexToHash("0xc07f658163e93c45a6f0288d229698f09c1252e41076f4caa71c8cbc12f118a1"), //nolint:lll
ParaHead: common.MustHexToHash("0x9a8a7107426ef873ab89fc8af390ec36bdb2f744a9ff71ad7f18a12d55a7f4f5"), //nolint:lll
ValidationCodeHash: parachaintypes.ValidationCodeHash(validationCodeHashV),
},

CommitmentsHash: common.MustHexToHash("0xa54a8dce5fd2a27e3715f99e4241f674a48f4529f77949a4474f5b283b823535"),
}

payload, err := candidateReceipt.Descriptor.CreateSignaturePayload()
require.NoError(t, err)

signatureBytes, err := collatorKeypair.Sign(payload)
require.NoError(t, err)

signature := [sr25519.SignatureLength]byte{}
copy(signature[:], signatureBytes)

candidateReceipt.Descriptor.Signature = parachaintypes.CollatorSignature(signature)

return candidateReceipt, validationCode
}

func TestValidateFromChainState(t *testing.T) {

candidateReceipt, validationCode := createTestCandidateReceiptAndValidationCode(t)

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

pov := PoV{
BlockData: bd,
}

// 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),
}

ctrl := gomock.NewController(t)

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().
ParachainHostCheckValidationOutputs(uint32(1000), gomock.AssignableToTypeOf(parachaintypes.CandidateCommitments{})).
Return(true, nil)

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

candidateCommitments, persistedValidationData, isValid, err := ValidateFromChainState(
mockInstance, mockPoVRequestor, candidateReceipt)
require.NoError(t, err)
require.True(t, isValid)
require.NotNil(t, candidateCommitments)
require.Equal(t, expectedPersistedValidationData, *persistedValidationData)
}

type HeadDataInAdderParachain struct {
Number uint64
ParentHash [32]byte
PostState [32]byte
}

type BlockDataInAdderParachain struct {
State uint64
Add uint64
}
3 changes: 3 additions & 0 deletions lib/parachain/chunk_fetching.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
Expand Down
3 changes: 3 additions & 0 deletions lib/parachain/chunk_fetching_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
Expand Down
3 changes: 3 additions & 0 deletions lib/parachain/collation_fetching.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
Expand Down
3 changes: 3 additions & 0 deletions lib/parachain/collation_fetching_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
Expand Down
3 changes: 3 additions & 0 deletions lib/parachain/collation_protocol.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
Expand Down
11 changes: 8 additions & 3 deletions lib/parachain/collation_protocol_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

import (
Expand Down Expand Up @@ -55,9 +58,11 @@ func TestCollationProtocol(t *testing.T) {
ValidationCodeHash: parachaintypes.ValidationCodeHash(hash5),
},
Commitments: parachaintypes.CandidateCommitments{
UpwardMessages: []parachaintypes.UpwardMessage{{1, 2, 3}},
NewValidationCode: &parachaintypes.ValidationCode{1, 2, 3},
HeadData: []byte{1, 2, 3},
UpwardMessages: []parachaintypes.UpwardMessage{{1, 2, 3}},
NewValidationCode: &parachaintypes.ValidationCode{1, 2, 3},
HeadData: parachaintypes.HeadData{
Data: []byte{1, 2, 3},
},
ProcessedDownwardMessages: uint32(5),
HrmpWatermark: uint32(0),
},
Expand Down
6 changes: 6 additions & 0 deletions lib/parachain/mocks_generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package parachain

//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . RuntimeInstance,PoVRequestor
Loading

0 comments on commit 1c35f41

Please sign in to comment.