From 35a3d3a8e31873d9ac5ec99ae37f496a73b0e1a9 Mon Sep 17 00:00:00 2001 From: Axay Sagathiya Date: Tue, 20 Jun 2023 15:46:42 +0530 Subject: [PATCH] feat(parachain): Add StatementDistributionMessage varingDataType (#3316) --- go.mod | 2 +- lib/parachain/statement.go | 81 +++++++ .../statement_distribution_message.go | 116 ++++++++++ .../statement_distribution_message_test.go | 199 ++++++++++++++++++ lib/parachain/statement_test.go | 88 ++++++++ .../statement_distribution_message.yaml | 13 ++ 6 files changed, 498 insertions(+), 1 deletion(-) create mode 100644 lib/parachain/statement.go create mode 100644 lib/parachain/statement_distribution_message.go create mode 100644 lib/parachain/statement_distribution_message_test.go create mode 100644 lib/parachain/statement_test.go create mode 100644 lib/parachain/testdata/statement_distribution_message.yaml diff --git a/go.mod b/go.mod index 725e0369f2..fbe1e82e89 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( golang.org/x/exp v0.0.0-20240213143201-ec583247a57a golang.org/x/term v0.22.0 google.golang.org/protobuf v1.34.2 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -196,7 +197,6 @@ require ( gonum.org/v1/gonum v0.13.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect ) diff --git a/lib/parachain/statement.go b/lib/parachain/statement.go new file mode 100644 index 0000000000..a2c1d531e9 --- /dev/null +++ b/lib/parachain/statement.go @@ -0,0 +1,81 @@ +package parachain + +import ( + "fmt" + + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/pkg/scale" +) + +// Statement is a result of candidate validation. It could be either `Valid` or `Seconded`. +type StatementValues interface { + Valid | Seconded +} + +type Statement struct { + inner any +} + +func setStatement[Value StatementValues](mvdt *Statement, value Value) { + mvdt.inner = value +} + +func (mvdt *Statement) SetValue(value any) (err error) { + switch value := value.(type) { + case Valid: + setStatement(mvdt, value) + return + + case Seconded: + setStatement(mvdt, value) + return + + default: + return fmt.Errorf("unsupported type") + } +} + +func (mvdt Statement) IndexValue() (index uint, value any, err error) { + switch mvdt.inner.(type) { + case Valid: + return 2, mvdt.inner, nil + + case Seconded: + return 1, mvdt.inner, nil + + } + return 0, nil, scale.ErrUnsupportedVaryingDataTypeValue +} + +func (mvdt Statement) Value() (value any, err error) { + _, value, err = mvdt.IndexValue() + return +} + +func (mvdt Statement) ValueAt(index uint) (value any, err error) { + switch index { + case 2: + return *new(Valid), nil + + case 1: + return *new(Seconded), nil + + } + return nil, scale.ErrUnknownVaryingDataTypeValue +} + +// NewStatement returns a new Statement VaryingDataType +func NewStatement() Statement { + return Statement{} +} + +// Seconded represents a statement that a validator seconds a candidate. +type Seconded CommittedCandidateReceipt + +// Valid represents a statement that a validator has deemed a candidate valid. +type Valid CandidateHash + +// CandidateHash makes it easy to enforce that a hash is a candidate hash on the type level. +type CandidateHash struct { + Value common.Hash `scale:"1"` +} diff --git a/lib/parachain/statement_distribution_message.go b/lib/parachain/statement_distribution_message.go new file mode 100644 index 0000000000..5ba323c6be --- /dev/null +++ b/lib/parachain/statement_distribution_message.go @@ -0,0 +1,116 @@ +package parachain + +import ( + "fmt" + + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/pkg/scale" +) + +type StatementDistributionMessageValues interface { + SignedFullStatement | SecondedStatementWithLargePayload +} + +// StatementDistributionMessage represents network messages used by the statement distribution subsystem +type StatementDistributionMessage struct { + inner any +} + +func setStatementDistributionMessage[Value StatementDistributionMessageValues](mvdt *StatementDistributionMessage, value Value) { + mvdt.inner = value +} + +func (mvdt *StatementDistributionMessage) SetValue(value any) (err error) { + switch value := value.(type) { + case SignedFullStatement: + setStatementDistributionMessage(mvdt, value) + return + + case SecondedStatementWithLargePayload: + setStatementDistributionMessage(mvdt, value) + return + + default: + return fmt.Errorf("unsupported type") + } +} + +func (mvdt StatementDistributionMessage) IndexValue() (index uint, value any, err error) { + switch mvdt.inner.(type) { + case SignedFullStatement: + return 0, mvdt.inner, nil + + case SecondedStatementWithLargePayload: + return 1, mvdt.inner, nil + + } + return 0, nil, scale.ErrUnsupportedVaryingDataTypeValue +} + +func (mvdt StatementDistributionMessage) Value() (value any, err error) { + _, value, err = mvdt.IndexValue() + return +} + +func (mvdt StatementDistributionMessage) ValueAt(index uint) (value any, err error) { + switch index { + case 0: + return *new(SignedFullStatement), nil + + case 1: + return *new(SecondedStatementWithLargePayload), nil + + } + return nil, scale.ErrUnknownVaryingDataTypeValue +} + +// NewStatementDistributionMessage returns a new StatementDistributionMessage VaryingDataType +func NewStatementDistributionMessage() StatementDistributionMessage { + return StatementDistributionMessage{} +} + +// SignedFullStatement represents a signed full statement under a given relay-parent. +type SignedFullStatement struct { + Hash common.Hash `scale:"1"` + UncheckedSignedFullStatement UncheckedSignedFullStatement `scale:"2"` +} + +// Seconded statement with large payload (e.g. containing a runtime upgrade). +// +// We only gossip the hash in that case, actual payloads can be fetched from sending node +// via request/response. +type SecondedStatementWithLargePayload StatementMetadata + +// UncheckedSignedFullStatement is a Variant of `SignedFullStatement` where the signature has not yet been verified. +type UncheckedSignedFullStatement struct { + // The payload is part of the signed data. The rest is the signing context, + // which is known both at signing and at validation. + Payload Statement `scale:"1"` + + // The index of the validator signing this statement. + ValidatorIndex ValidatorIndex `scale:"2"` + + // The signature by the validator of the signed payload. + Signature ValidatorSignature `scale:"3"` +} + +// StatementMetadata represents the data that makes a statement unique. +type StatementMetadata struct { + // Relay parent this statement is relevant under. + RelayParent common.Hash `scale:"1"` + + // Hash of the candidate that got validated. + CandidateHash CandidateHash `scale:"2"` + + // Validator that attested the validity. + SignedBy ValidatorIndex `scale:"3"` + + // Signature of seconding validator. + Signature ValidatorSignature `scale:"4"` +} + +// ValidatorSignature represents the signature with which parachain validators sign blocks. +type ValidatorSignature Signature + +// Signature represents a cryptographic signature. +type Signature [64]byte diff --git a/lib/parachain/statement_distribution_message_test.go b/lib/parachain/statement_distribution_message_test.go new file mode 100644 index 0000000000..e57ffdfd1d --- /dev/null +++ b/lib/parachain/statement_distribution_message_test.go @@ -0,0 +1,199 @@ +package parachain + +import ( + _ "embed" + "fmt" + "testing" + + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/pkg/scale" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +//go:embed testdata/statement_distribution_message.yaml +var testSDMHexRaw string + +var testSDMHex map[string]string + +func init() { + err := yaml.Unmarshal([]byte(testSDMHexRaw), &testSDMHex) + if err != nil { + fmt.Printf("Error unmarshaling test data: %s\n", err) + return + } +} + +func TestStatementDistributionMessage(t *testing.T) { + t.Parallel() + + var collatorSignature CollatorSignature + tempSignature := common.MustHexToBytes(testSDMHex["collatorSignature"]) + copy(collatorSignature[:], tempSignature) + + var validatorSignature ValidatorSignature + copy(validatorSignature[:], tempSignature) + + var collatorID CollatorID + tempCollatID := common.MustHexToBytes("0x48215b9d322601e5b1a95164cea0dc4626f545f98343d07f1551eb9543c4b147") + copy(collatorID[:], tempCollatID) + + hash5 := getDummyHash(5) + + statementWithValid := NewStatement() + err := statementWithValid.SetValue(Valid{hash5}) + require.NoError(t, err) + + secondedEnumValue := Seconded{ + Descriptor: CandidateDescriptor{ + ParaID: uint32(1), + RelayParent: hash5, + Collator: collatorID, + PersistedValidationDataHash: hash5, + PovHash: hash5, + ErasureRoot: hash5, + Signature: collatorSignature, + ParaHead: hash5, + ValidationCodeHash: ValidationCodeHash(hash5), + }, + Commitments: CandidateCommitments{ + UpwardMessages: []UpwardMessage{{1, 2, 3}}, + HorizontalMessages: []OutboundHrmpMessage{}, + NewValidationCode: &ValidationCode{1, 2, 3}, + HeadData: headData{1, 2, 3}, + ProcessedDownwardMessages: uint32(5), + HrmpWatermark: uint32(0), + }, + } + + statementWithSeconded := NewStatement() + err = statementWithSeconded.SetValue(secondedEnumValue) + require.NoError(t, err) + + signedFullStatementWithValid := SignedFullStatement{ + Hash: hash5, + UncheckedSignedFullStatement: UncheckedSignedFullStatement{ + Payload: statementWithValid, + ValidatorIndex: ValidatorIndex(5), + Signature: validatorSignature, + }, + } + + signedFullStatementWithSeconded := SignedFullStatement{ + Hash: hash5, + UncheckedSignedFullStatement: UncheckedSignedFullStatement{ + Payload: statementWithSeconded, + ValidatorIndex: ValidatorIndex(5), + Signature: validatorSignature, + }, + } + + secondedStatementWithLargePayload := SecondedStatementWithLargePayload{ + RelayParent: hash5, + CandidateHash: CandidateHash{hash5}, + SignedBy: ValidatorIndex(5), + Signature: validatorSignature, + } + + testCases := []struct { + name string + enumValue any + encodingValue []byte + }{ + // expected encoding is generated by running rust test code: + // fn statement_distribution_message_encode() { + // let hash1 = Hash::repeat_byte(5); + // let candidate_hash = CandidateHash(hash1); + // let statement_valid = Statement::Valid(candidate_hash); + // let val_sign = ValidatorSignature::from( + // sr25519::Signature([198, 124, 185, 59, 240, 163, 111, 206, 227, + // 210, 157, 232, 166, 166, 154, 117, 150, 89, 104, 10, 207, 72, 100, 117, 224, 162, 85, 42, 95, 190, + // 216, 126, 69, 173, 206, 95, 41, 6, 152, 216, 89, 96, 149, 114, 43, 51, 89, 146, 39, 247, 70, 31, + // 81, 175, 134, 23, 200, 190, 116, 184, 148, 207, 27, 134])); + // let unchecked_signed_full_statement_valid = UncheckedSignedFullStatement::new( + // statement_valid, ValidatorIndex(5), val_sign.clone()); + // let sdm_statement_valid = StatementDistributionMessage::Statement( + // hash1, unchecked_signed_full_statement_valid); + // println!("encode SignedFullStatement with valid statement => {:?}\n\n", sdm_statement_valid.encode()); + + // let collator_result = sr25519::Public::from_string( + // "0x48215b9d322601e5b1a95164cea0dc4626f545f98343d07f1551eb9543c4b147"); + // let collator = collator_result.unwrap(); + // let collsign = CollatorSignature::from(sr25519::Signature( + // [198, 124, 185, 59, 240, 163, 111, 206, 227, 210, 157, 232, + // 166, 166, 154, 117, 150, 89, 104, 10, 207, 72, 100, 117, 224, 162, 85, 42, 95, 190, 216, 126, 69, 173, + // 206, 95, 41, 6, 152, 216, 89, 96, 149, 114, 43, 51, 89, 146, 39, 247, 70, 31, 81, 175, 134, 23, 200, + // 190, 116, 184, 148, 207, 27, 134])); + // let candidate_descriptor = CandidateDescriptor{ + // para_id: 1.into(), + // relay_parent: hash1, + // collator: CollatorId::from(collator), + // persisted_validation_data_hash: hash1, + // pov_hash: hash1, + // erasure_root: hash1, + // signature: collsign, + // para_head: hash1, + // validation_code_hash: ValidationCodeHash::from(hash1) + // }; + // let commitments_new = CandidateCommitments{ + // upward_messages: vec![vec![1, 2, 3]].try_into().expect("error - upward_messages"), + // horizontal_messages: vec![].try_into().expect("error - horizontal_messages"), + // head_data: HeadData(vec![1, 2, 3]), + // hrmp_watermark: 0_u32, + // new_validation_code: ValidationCode(vec![1, 2, 3]).try_into().expect("error - new_validation_code"), + // processed_downward_messages: 5 + // }; + // let committed_candidate_receipt = CommittedCandidateReceipt{ + // descriptor: candidate_descriptor, + // commitments : commitments_new + // }; + // let statement_second = Statement::Seconded(committed_candidate_receipt); + // let unchecked_signed_full_statement_second = UncheckedSignedFullStatement::new( + // statement_second, ValidatorIndex(5), val_sign.clone()); + // let sdm_statement_second = StatementDistributionMessage::Statement( + // hash1, unchecked_signed_full_statement_second); + // println!("encode SignedFullStatement with Seconded statement => {:?}\n\n", sdm_statement_second.encode()); + + // let sdm_large_statement = StatementDistributionMessage::LargeStatement(StatementMetadata{ + // relay_parent: hash1, + // candidate_hash: CandidateHash(hash1), + // signed_by: ValidatorIndex(5_u32), + // signature: val_sign.clone(), + // }); + // println!("encode SecondedStatementWithLargePayload => {:?}\n\n", sdm_large_statement.encode()); + // } + + { + name: "SignedFullStatement with valid statement", + enumValue: signedFullStatementWithValid, + encodingValue: common.MustHexToBytes(testSDMHex["sfsValid"]), + }, + { + name: "SignedFullStatement with Seconded statement", + enumValue: signedFullStatementWithSeconded, + encodingValue: common.MustHexToBytes(testSDMHex["sfsSeconded"]), + }, + { + name: "Seconded Statement With LargePayload", + enumValue: secondedStatementWithLargePayload, + encodingValue: common.MustHexToBytes(testSDMHex["statementWithLargePayload"]), + }, + } + + for _, c := range testCases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + vtd := NewStatementDistributionMessage() + + err := vtd.SetValue(c.enumValue) + require.NoError(t, err) + + bytes, err := scale.Marshal(vtd) + require.NoError(t, err) + + require.Equal(t, c.encodingValue, bytes) + }) + } +} diff --git a/lib/parachain/statement_test.go b/lib/parachain/statement_test.go new file mode 100644 index 0000000000..beec791a88 --- /dev/null +++ b/lib/parachain/statement_test.go @@ -0,0 +1,88 @@ +package parachain + +import ( + "testing" + + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/pkg/scale" + "github.com/stretchr/testify/require" +) + +func getDummyHash(num byte) common.Hash { + hash := common.Hash{} + for i := 0; i < 32; i++ { + hash[i] = num + } + return hash +} + +func TestStatement(t *testing.T) { + t.Parallel() + + var collatorID CollatorID + tempCollatID := common.MustHexToBytes("0x48215b9d322601e5b1a95164cea0dc4626f545f98343d07f1551eb9543c4b147") + copy(collatorID[:], tempCollatID) + + var collatorSignature CollatorSignature + tempSignature := common.MustHexToBytes(testSDMHex["collatorSignature"]) + copy(collatorSignature[:], tempSignature) + + hash5 := getDummyHash(5) + + secondedEnumValue := Seconded{ + Descriptor: CandidateDescriptor{ + ParaID: uint32(1), + RelayParent: hash5, + Collator: collatorID, + PersistedValidationDataHash: hash5, + PovHash: hash5, + ErasureRoot: hash5, + Signature: collatorSignature, + ParaHead: hash5, + ValidationCodeHash: ValidationCodeHash(hash5), + }, + Commitments: CandidateCommitments{ + UpwardMessages: []UpwardMessage{{1, 2, 3}}, + HorizontalMessages: []OutboundHrmpMessage{}, + NewValidationCode: &ValidationCode{1, 2, 3}, + HeadData: headData{1, 2, 3}, + ProcessedDownwardMessages: uint32(5), + HrmpWatermark: uint32(0), + }, + } + + testCases := []struct { + name string + enumValue any + encodingValue []byte + }{ + { + name: "Seconded", + enumValue: secondedEnumValue, + encodingValue: common.MustHexToBytes(testSDMHex["statementSeconded"]), + // expected Hex stored in statement_distribution_message.yaml + }, + { + name: "Valid", + enumValue: Valid{hash5}, + encodingValue: common.MustHexToBytes("0x020505050505050505050505050505050505050505050505050505050505050505"), + }, + } + + for _, c := range testCases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + vtd := NewStatement() + + err := vtd.SetValue(c.enumValue) + require.NoError(t, err) + + bytes, err := scale.Marshal(vtd) + require.NoError(t, err) + + require.Equal(t, c.encodingValue, bytes) + }) + } +} diff --git a/lib/parachain/testdata/statement_distribution_message.yaml b/lib/parachain/testdata/statement_distribution_message.yaml new file mode 100644 index 0000000000..e43dd59bb8 --- /dev/null +++ b/lib/parachain/testdata/statement_distribution_message.yaml @@ -0,0 +1,13 @@ +collatorSignature: "0xc67cb93bf0a36fcee3d29de8a6a69a759659680acf486475e0a2552a5fbed87e45adce5f290698d8596095722b33599227f7461f51af8617c8be74b894cf1b86" + +# Seconded +statementSeconded: "0x0101000000050505050505050505050505050505050505050505050505050505050505050548215b9d322601e5b1a95164cea0dc4626f545f98343d07f1551eb9543c4b147050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505c67cb93bf0a36fcee3d29de8a6a69a759659680acf486475e0a2552a5fbed87e45adce5f290698d8596095722b33599227f7461f51af8617c8be74b894cf1b8605050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505040c01020300010c0102030c0102030500000000000000" + +# SignedFullStatement with valid statement +sfsValid: "0x00050505050505050505050505050505050505050505050505050505050505050502050505050505050505050505050505050505050505050505050505050505050505000000c67cb93bf0a36fcee3d29de8a6a69a759659680acf486475e0a2552a5fbed87e45adce5f290698d8596095722b33599227f7461f51af8617c8be74b894cf1b86" + +# SignedFullStatement with Seconded statement +sfsSeconded: "0x0005050505050505050505050505050505050505050505050505050505050505050101000000050505050505050505050505050505050505050505050505050505050505050548215b9d322601e5b1a95164cea0dc4626f545f98343d07f1551eb9543c4b147050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505c67cb93bf0a36fcee3d29de8a6a69a759659680acf486475e0a2552a5fbed87e45adce5f290698d8596095722b33599227f7461f51af8617c8be74b894cf1b8605050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505040c01020300010c0102030c010203050000000000000005000000c67cb93bf0a36fcee3d29de8a6a69a759659680acf486475e0a2552a5fbed87e45adce5f290698d8596095722b33599227f7461f51af8617c8be74b894cf1b86" + +# Seconded Statement With LargePayload +statementWithLargePayload: "0x010505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505000000c67cb93bf0a36fcee3d29de8a6a69a759659680acf486475e0a2552a5fbed87e45adce5f290698d8596095722b33599227f7461f51af8617c8be74b894cf1b86" \ No newline at end of file