diff --git a/dot/parachain/backing/integration_test.go b/dot/parachain/backing/integration_test.go index 41c2f0cfc5..a0192e4e20 100644 --- a/dot/parachain/backing/integration_test.go +++ b/dot/parachain/backing/integration_test.go @@ -481,7 +481,7 @@ func TestCandidateReachesQuorum(t *testing.T) { Return(&validationCode, nil) mockRuntime.EXPECT(). ParachainHostSessionExecutorParams(gomock.AssignableToTypeOf(parachaintypes.SessionIndex(0))). - Return(nil, wazero_runtime.ErrExportFunctionNotFound).Times(1) + Return(nil, wazero_runtime.ErrExportFunctionNotFound) //mock ImplicitView mockImplicitView.EXPECT().AllAllowedRelayParents(). @@ -673,7 +673,7 @@ func TestValidationFailDoesNotStopSubsystem(t *testing.T) { Return(&validationCode, nil) mockRuntime.EXPECT(). ParachainHostSessionExecutorParams(gomock.AssignableToTypeOf(parachaintypes.SessionIndex(0))). - Return(nil, wazero_runtime.ErrExportFunctionNotFound).Times(1) + Return(nil, wazero_runtime.ErrExportFunctionNotFound) //mock ImplicitView mockImplicitView.EXPECT().AllAllowedRelayParents(). @@ -952,7 +952,7 @@ func TestNewLeafDoesNotClobberOld(t *testing.T) { Return(&validationCode, nil) mockRuntime.EXPECT(). ParachainHostSessionExecutorParams(gomock.AssignableToTypeOf(parachaintypes.SessionIndex(0))). - Return(nil, wazero_runtime.ErrExportFunctionNotFound).Times(1) + Return(nil, wazero_runtime.ErrExportFunctionNotFound) //mock ImplicitView mockImplicitView.EXPECT().AllAllowedRelayParents(). @@ -1034,3 +1034,189 @@ func TestNewLeafDoesNotClobberOld(t *testing.T) { time.Sleep(1 * time.Second) } + +// Issuing conflicting statements on the same candidate should be a misbehaviour. +func TestConflictingStatementIsMisbehavior(t *testing.T) { + candidateBacking, overseer := initBackingAndOverseerMock(t) + defer stopOverseerAndWaitForCompletion(overseer) + + paraValidators := parachainValidators(t, candidateBacking.Keystore) + numOfValidators := uint(len(paraValidators)) + relayParent := getDummyHash(t, 5) + paraID := uint32(1) + + pov := parachaintypes.PoV{BlockData: []byte{1, 2, 3}} + povHash, err := pov.Hash() + require.NoError(t, err) + + pvd := dummyPVD(t) + validationCode := parachaintypes.ValidationCode{1, 2, 3} + + signingContext := signingContext(t) + + ctrl := gomock.NewController(t) + mockBlockState := backing.NewMockBlockState(ctrl) + mockRuntime := backing.NewMockInstance(ctrl) + mockImplicitView := backing.NewMockImplicitView(ctrl) + + candidateBacking.BlockState = mockBlockState + candidateBacking.ImplicitView = mockImplicitView + + // mock BlockState methods + mockBlockState.EXPECT().GetRuntime(gomock.AssignableToTypeOf(common.Hash{})). + Return(mockRuntime, nil).Times(3) + + // mock Runtime Instance methods + mockRuntime.EXPECT().ParachainHostAsyncBackingParams(). + Return(nil, wazero_runtime.ErrExportFunctionNotFound) + mockRuntime.EXPECT().ParachainHostSessionIndexForChild(). + Return(parachaintypes.SessionIndex(1), nil).Times(2) + mockRuntime.EXPECT().ParachainHostValidators(). + Return(paraValidators, nil) + mockRuntime.EXPECT().ParachainHostValidatorGroups(). + Return(validatorGroups(t), nil) + mockRuntime.EXPECT().ParachainHostAvailabilityCores(). + Return(availabilityCores(t), nil) + mockRuntime.EXPECT().ParachainHostMinimumBackingVotes(). + Return(backing.LEGACY_MIN_BACKING_VOTES, nil) + mockRuntime.EXPECT().ParachainHostValidationCodeByHash(gomock.AssignableToTypeOf(common.Hash{})). + Return(&validationCode, nil) + mockRuntime.EXPECT(). + ParachainHostSessionExecutorParams(gomock.AssignableToTypeOf(parachaintypes.SessionIndex(0))). + Return(nil, wazero_runtime.ErrExportFunctionNotFound) + + //mock ImplicitView + mockImplicitView.EXPECT().AllAllowedRelayParents(). + Return([]common.Hash{}) + + // to make entry in perRelayParent map + overseer.ReceiveMessage(parachaintypes.ActiveLeavesUpdateSignal{ + Activated: ¶chaintypes.ActivatedLeaf{Hash: relayParent, Number: 1}, + }) + time.Sleep(500 * time.Millisecond) + + headData := parachaintypes.HeadData{Data: []byte{4, 5, 6}} + + candidate := newCommittedCandidate( + t, + paraID, + headData, + povHash, + relayParent, + makeErasureRoot(t, numOfValidators, pov, pvd), + common.Hash{}, + validationCode, + ) + + statementSeconded := parachaintypes.NewStatementVDT() + err = statementSeconded.SetValue(parachaintypes.Seconded(candidate)) + require.NoError(t, err) + + statementSecondedSign, err := statementSeconded.Sign(candidateBacking.Keystore, signingContext, paraValidators[2]) + require.NoError(t, err) + + signedStatementSeconded := parachaintypes.SignedFullStatementWithPVD{ + SignedFullStatement: parachaintypes.SignedFullStatement{ + Payload: statementSeconded, + ValidatorIndex: 2, + Signature: *statementSecondedSign, + }, + PersistedValidationData: &pvd, + } + + fetchPov := func(msg any) bool { + fetch, ok := msg.(parachaintypes.AvailabilityDistributionMessageFetchPoV) + if !ok { + return false + } + + fetch.PovCh <- parachaintypes.OverseerFuncRes[parachaintypes.PoV]{Data: pov} + return true + } + + validate := validResponseForValidateFromExhaustive(headData, pvd) + + distribute := func(msg any) bool { + _, ok := msg.(parachaintypes.StatementDistributionMessageShare) + return ok + } + + provisionerMessageProvisionableData := func(msg any) bool { + _, ok := msg.(parachaintypes.ProvisionerMessageProvisionableData) + return ok + } + + // set expected actions for overseer messages we send from the subsystem. + overseer.ExpectActions(fetchPov, validate, storeAvailableData, distribute, provisionerMessageProvisionableData) + + // receive statement message from overseer to candidate backing subsystem containing `Seconded` statement + overseer.ReceiveMessage(backing.StatementMessage{ + RelayParent: relayParent, + SignedFullStatement: signedStatementSeconded, + }) + time.Sleep(1 * time.Second) + + candidateHash, err := parachaintypes.GetCandidateHash(candidate) + require.NoError(t, err) + + statementValid := parachaintypes.NewStatementVDT() + err = statementValid.SetValue(parachaintypes.Valid(candidateHash)) + require.NoError(t, err) + + statementValidSign, err := statementValid.Sign(candidateBacking.Keystore, signingContext, paraValidators[2]) + require.NoError(t, err) + + signedStatementValid := parachaintypes.SignedFullStatementWithPVD{ + SignedFullStatement: parachaintypes.SignedFullStatement{ + Payload: statementValid, + ValidatorIndex: 2, + Signature: *statementValidSign, + }, + } + + reportMisbehavior := func(msg any) bool { + provisionerMessage, ok := msg.(parachaintypes.ProvisionerMessageProvisionableData) + if !ok { + return false + } + + require.Equal(t, relayParent, provisionerMessage.RelayParent) + misbehvaiorReport, ok := provisionerMessage.ProvisionableData.(parachaintypes.ProvisionableDataMisbehaviorReport) + require.True(t, ok) + + require.Equal(t, parachaintypes.ValidatorIndex(2), misbehvaiorReport.ValidatorIndex) + doubleVote, ok := misbehvaiorReport.Misbehaviour.(parachaintypes.ValidityDoubleVoteIssuedAndValidity) + require.True(t, ok) + + signForSeconded := doubleVote.CommittedCandidateReceiptAndSign.Signature + statementSeconded := parachaintypes.NewStatementVDT() + err := statementSeconded.SetValue( + parachaintypes.Seconded(doubleVote.CommittedCandidateReceiptAndSign.CommittedCandidateReceipt)) + require.NoError(t, err) + + ok, err = statementSeconded.VerifySignature(paraValidators[2], signingContext, signForSeconded) + require.NoError(t, err) + require.True(t, ok) + + signForValid := doubleVote.CandidateHashAndSign.Signature + statementValid := parachaintypes.NewStatementVDT() + err = statementValid.SetValue(parachaintypes.Valid(doubleVote.CandidateHashAndSign.CandidateHash)) + require.NoError(t, err) + + ok, err = statementValid.VerifySignature(paraValidators[2], signingContext, signForValid) + require.NoError(t, err) + require.True(t, ok) + + return true + } + + overseer.ExpectActions(reportMisbehavior) + + // receive statement message from overseer to candidate backing subsystem containing `Valid` statement. + // this candidate is already seconded by the same validator So, it is a misbehaviour for conflicting statements. + overseer.ReceiveMessage(backing.StatementMessage{ + RelayParent: relayParent, + SignedFullStatement: signedStatementValid, + }) + time.Sleep(1 * time.Second) +} diff --git a/dot/parachain/types/statement.go b/dot/parachain/types/statement.go index 50316827d9..1627393a8b 100644 --- a/dot/parachain/types/statement.go +++ b/dot/parachain/types/statement.go @@ -113,6 +113,33 @@ func (s *StatementVDT) Sign( return &valSign, nil } +// VerifySignature verifies the validator signature for the statement. +func (s *StatementVDT) VerifySignature( + validator ValidatorID, + signingContext SigningContext, + validatorSignature ValidatorSignature, +) (bool, error) { + encodedMsg, err := scale.Marshal(s) + if err != nil { + return false, fmt.Errorf("marshalling statementVDT: %w", err) + } + + signingContextBytes, err := scale.Marshal(signingContext) + if err != nil { + return false, fmt.Errorf("marshalling signing context: %w", err) + } + + encodedMsg = append(encodedMsg, signingContextBytes...) + + publicKey, err := sr25519.NewPublicKey(validator[:]) + if err != nil { + return false, fmt.Errorf("getting public key: %w", err) + } + + ok, err := publicKey.Verify(encodedMsg, validatorSignature[:]) + return ok, err +} + // 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, @@ -143,6 +170,9 @@ type SignedFullStatement UncheckedSignedFullStatement // SignedFullStatementWithPVD represents a signed full statement along with associated Persisted Validation Data (PVD). type SignedFullStatementWithPVD struct { - SignedFullStatement SignedFullStatement + SignedFullStatement SignedFullStatement + + // PersistedValidationData must be set only for `Seconded` statement. + // otherwise, it should be nil. PersistedValidationData *PersistedValidationData } diff --git a/dot/parachain/types/statement_test.go b/dot/parachain/types/statement_test.go index 14c6043e82..0ef5ffddb6 100644 --- a/dot/parachain/types/statement_test.go +++ b/dot/parachain/types/statement_test.go @@ -165,14 +165,7 @@ func TestStatementVDT_Sign(t *testing.T) { valSign, err := statement.Sign(ks, signingContext, validatorID) require.NoError(t, err) - encodedMsg, err := scale.Marshal(statement) - require.NoError(t, err) - - signingContextBytes, err := scale.Marshal(signingContext) - require.NoError(t, err) - - encodedMsg = append(encodedMsg, signingContextBytes...) - ok, err := keyPair.Public().Verify(encodedMsg, valSign[:]) + ok, err := statement.VerifySignature(validatorID, signingContext, *valSign) require.NoError(t, err) require.True(t, ok) }