diff --git a/dot/parachain/backing/active_leaves_update.go b/dot/parachain/backing/active_leaves_update.go index 281d22c493..429f1e5b40 100644 --- a/dot/parachain/backing/active_leaves_update.go +++ b/dot/parachain/backing/active_leaves_update.go @@ -360,7 +360,7 @@ func constructPerRelayParentState( } } - tableContext := TableContext{ + tableContext := tableContext{ validator: localValidator, groups: groups, validators: validators, diff --git a/dot/parachain/backing/candidate_backing.go b/dot/parachain/backing/candidate_backing.go index b77561b421..2da1f9fe0b 100644 --- a/dot/parachain/backing/candidate_backing.go +++ b/dot/parachain/backing/candidate_backing.go @@ -25,6 +25,7 @@ import ( "context" "errors" "fmt" + "slices" "sync" parachaintypes "github.com/ChainSafe/gossamer/dot/parachain/types" @@ -115,14 +116,24 @@ type attestingData struct { backing []parachaintypes.ValidatorIndex } -// TableContext represents the contextual information associated with a validator and groups +// tableContext represents the contextual information associated with a validator and groups // for a table under a relay-parent. -type TableContext struct { +type tableContext struct { validator *validator groups map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex validators []parachaintypes.ValidatorID } +// isMemberOf returns true if the validator is a member of the group of validators assigned to the parachain. +func (tc *tableContext) isMemberOf(validatorIndex parachaintypes.ValidatorIndex, paraID parachaintypes.ParaID) bool { + indexes, ok := tc.groups[paraID] + if !ok { + return false + } + + return slices.Contains(indexes, validatorIndex) +} + // validator represents local validator information. // It can be created if the local node is a validator in the context of a particular relay chain block. type validator struct { @@ -307,7 +318,7 @@ func (cb *CandidateBacking) handleStatementMessage( var attesting attestingData switch statementVDT := statementVDT.(type) { case parachaintypes.Seconded: - commitedCandidateReceipt, err := rpState.table.getCandidate(summary.Candidate) + commitedCandidateReceipt, err := rpState.table.getCommittedCandidateReceipt(summary.Candidate) if err != nil { return fmt.Errorf("getting candidate: %w", err) } diff --git a/dot/parachain/backing/candidate_backing_test.go b/dot/parachain/backing/candidate_backing_test.go index 8f5c60144f..276d0c1aeb 100644 --- a/dot/parachain/backing/candidate_backing_test.go +++ b/dot/parachain/backing/candidate_backing_test.go @@ -223,8 +223,8 @@ func TestImportStatement(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(new(Summary), nil) return perRelayParentState{ @@ -259,8 +259,8 @@ func TestImportStatement(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(new(Summary), nil) return perRelayParentState{ @@ -290,8 +290,8 @@ func TestImportStatement(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(new(Summary), nil) return perRelayParentState{ @@ -310,8 +310,8 @@ func TestImportStatement(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(new(Summary), nil) return perRelayParentState{ @@ -397,10 +397,10 @@ func dummyValidityAttestation(t *testing.T, value string) parachaintypes.Validit return vdt } -func dummyTableContext(t *testing.T) TableContext { +func dummyTableContext(t *testing.T) tableContext { t.Helper() - return TableContext{ + return tableContext{ validator: &validator{ index: 1, }, @@ -447,7 +447,7 @@ func rpStateWhenPpmDisabled(t *testing.T) perRelayParentState { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(&attestedToReturn, nil) @@ -498,7 +498,7 @@ func TestPostImportStatement(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(nil, errors.New("could not get attested candidate from table")) @@ -524,7 +524,7 @@ func TestPostImportStatement(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(&attestedCandidate{ groupID: 4, @@ -550,7 +550,7 @@ func TestPostImportStatement(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(&attestedCandidate{ groupID: 3, @@ -909,8 +909,8 @@ func TestHandleStatementMessage(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(nil, nil) mockTable.EXPECT().drainMisbehaviors(). Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) @@ -936,8 +936,8 @@ func TestHandleStatementMessage(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(&Summary{ GroupID: 4, }, nil) @@ -945,7 +945,7 @@ func TestHandleStatementMessage(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(nil, errors.New("could not get attested candidate from table")) @@ -971,8 +971,8 @@ func TestHandleStatementMessage(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(&Summary{ Candidate: candidateHash, GroupID: 4, @@ -981,7 +981,7 @@ func TestHandleStatementMessage(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(new(attestedCandidate), nil) @@ -1009,8 +1009,8 @@ func TestHandleStatementMessage(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(&Summary{ Candidate: candidateHash, GroupID: 4, @@ -1019,7 +1019,7 @@ func TestHandleStatementMessage(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(new(attestedCandidate), nil) @@ -1050,8 +1050,8 @@ func TestHandleStatementMessage(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(&Summary{ Candidate: candidateHash, GroupID: 4, @@ -1060,7 +1060,7 @@ func TestHandleStatementMessage(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(new(attestedCandidate), nil) @@ -1094,8 +1094,8 @@ func TestHandleStatementMessage(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(&Summary{ Candidate: candidateHash, GroupID: 4, @@ -1104,7 +1104,7 @@ func TestHandleStatementMessage(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(new(attestedCandidate), nil) @@ -1145,8 +1145,8 @@ func TestHandleStatementMessage(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(&Summary{ Candidate: candidateHash, GroupID: 4, @@ -1155,7 +1155,7 @@ func TestHandleStatementMessage(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(new(attestedCandidate), nil) mockTable.EXPECT().getCandidate( @@ -1189,8 +1189,8 @@ func TestHandleStatementMessage(t *testing.T) { mockTable := NewMockTable(ctrl) mockTable.EXPECT().importStatement( - gomock.AssignableToTypeOf(new(TableContext)), - gomock.AssignableToTypeOf(parachaintypes.SignedFullStatementWithPVD{}), + gomock.AssignableToTypeOf(new(tableContext)), + gomock.AssignableToTypeOf(parachaintypes.SignedFullStatement{}), ).Return(&Summary{ Candidate: candidateHash, GroupID: 4, @@ -1199,7 +1199,7 @@ func TestHandleStatementMessage(t *testing.T) { Return([]parachaintypes.ProvisionableDataMisbehaviorReport{}) mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(new(attestedCandidate), nil) mockTable.EXPECT().getCandidate( diff --git a/dot/parachain/backing/get_backed_candidates.go b/dot/parachain/backing/get_backed_candidates.go index a03bd5163b..c56dab70b8 100644 --- a/dot/parachain/backing/get_backed_candidates.go +++ b/dot/parachain/backing/get_backed_candidates.go @@ -34,7 +34,7 @@ func (cb *CandidateBacking) handleGetBackedCandidatesMessage(requestedCandidates continue } - backed := attestedToBackedCandidate(*attested, &rpState.tableContext) + backed := attested.toBackedCandidate(&rpState.tableContext) backedCandidates = append(backedCandidates, backed) } diff --git a/dot/parachain/backing/get_backed_candidates_test.go b/dot/parachain/backing/get_backed_candidates_test.go index fbb00934dd..a9ad97d2ee 100644 --- a/dot/parachain/backing/get_backed_candidates_test.go +++ b/dot/parachain/backing/get_backed_candidates_test.go @@ -41,7 +41,7 @@ func TestHandleGetBackedCandidatesMessage(t *testing.T) { mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(nil, errors.New("could not get attested candidate from table")) @@ -60,7 +60,7 @@ func TestHandleGetBackedCandidatesMessage(t *testing.T) { mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(nil, nil) @@ -79,7 +79,7 @@ func TestHandleGetBackedCandidatesMessage(t *testing.T) { mockTable.EXPECT().attestedCandidate( gomock.AssignableToTypeOf(parachaintypes.CandidateHash{}), - gomock.AssignableToTypeOf(new(TableContext)), + gomock.AssignableToTypeOf(new(tableContext)), gomock.AssignableToTypeOf(uint32(0)), ).Return(new(attestedCandidate), nil) diff --git a/dot/parachain/backing/mocks_test.go b/dot/parachain/backing/mocks_test.go index fda2ef7a7e..fb9979f48d 100644 --- a/dot/parachain/backing/mocks_test.go +++ b/dot/parachain/backing/mocks_test.go @@ -41,7 +41,7 @@ func (m *MockTable) EXPECT() *MockTableMockRecorder { } // attestedCandidate mocks base method. -func (m *MockTable) attestedCandidate(arg0 parachaintypes.CandidateHash, arg1 *TableContext, arg2 uint32) (*attestedCandidate, error) { +func (m *MockTable) attestedCandidate(arg0 parachaintypes.CandidateHash, arg1 *tableContext, arg2 uint32) (*attestedCandidate, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "attestedCandidate", arg0, arg1, arg2) ret0, _ := ret[0].(*attestedCandidate) @@ -69,8 +69,8 @@ func (mr *MockTableMockRecorder) drainMisbehaviors() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "drainMisbehaviors", reflect.TypeOf((*MockTable)(nil).drainMisbehaviors)) } -// getCandidate mocks base method. -func (m *MockTable) getCandidate(arg0 parachaintypes.CandidateHash) (parachaintypes.CommittedCandidateReceipt, error) { +// getCommittedCandidateReceipt mocks base method. +func (m *MockTable) getCommittedCandidateReceipt(arg0 parachaintypes.CandidateHash) (parachaintypes.CommittedCandidateReceipt, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "getCandidate", arg0) ret0, _ := ret[0].(parachaintypes.CommittedCandidateReceipt) @@ -81,11 +81,11 @@ func (m *MockTable) getCandidate(arg0 parachaintypes.CandidateHash) (parachainty // getCandidate indicates an expected call of getCandidate. func (mr *MockTableMockRecorder) getCandidate(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getCandidate", reflect.TypeOf((*MockTable)(nil).getCandidate), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getCandidate", reflect.TypeOf((*MockTable)(nil).getCommittedCandidateReceipt), arg0) } // importStatement mocks base method. -func (m *MockTable) importStatement(arg0 *TableContext, arg1 parachaintypes.SignedFullStatementWithPVD) (*Summary, error) { +func (m *MockTable) importStatement(arg0 *tableContext, arg1 parachaintypes.SignedFullStatement) (*Summary, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "importStatement", arg0, arg1) ret0, _ := ret[0].(*Summary) diff --git a/dot/parachain/backing/per_relay_parent_state.go b/dot/parachain/backing/per_relay_parent_state.go index 3deb20f674..bfb3ccc5b8 100644 --- a/dot/parachain/backing/per_relay_parent_state.go +++ b/dot/parachain/backing/per_relay_parent_state.go @@ -15,9 +15,10 @@ import ( wazero_runtime "github.com/ChainSafe/gossamer/lib/runtime/wazero" "github.com/ChainSafe/gossamer/lib/common" - "github.com/ChainSafe/gossamer/pkg/scale" ) +var errNilPersistedValidationData = errors.New("persisted validation data is nil") + // PerRelayParentState represents the state information for a relay-parent in the subsystem. type perRelayParentState struct { prospectiveParachainsMode parachaintypes.ProspectiveParachainsMode @@ -28,7 +29,7 @@ type perRelayParentState struct { // The table of candidates and statements under this relay-parent. table Table // The table context, including groups. - tableContext TableContext + tableContext tableContext // Data needed for retrying in case of `ValidatedCandidateCommand::AttestNoPoV`. fallbacks map[parachaintypes.CandidateHash]attestingData // These candidates are undergoing validation in the background. @@ -52,13 +53,13 @@ func (rpState *perRelayParentState) importStatement( return nil, fmt.Errorf("getting value from statementVDT: %w", err) } - if statementVDT.Index() == 2 { // Valid - return rpState.table.importStatement(&rpState.tableContext, signedStatementWithPVD) + if statementVDT.Index() != 1 { // Not Seconded + return rpState.table.importStatement(&rpState.tableContext, signedStatementWithPVD.SignedFullStatement) } // PersistedValidationData should not be nil if the statementVDT is Seconded. if signedStatementWithPVD.PersistedValidationData == nil { - return nil, fmt.Errorf("persisted validation data is nil") + return nil, errNilPersistedValidationData } statementVDTSeconded := statementVDT.(parachaintypes.Seconded) @@ -70,7 +71,7 @@ func (rpState *perRelayParentState) importStatement( candidateHash := parachaintypes.CandidateHash{Value: hash} if _, ok := perCandidate[candidateHash]; ok { - return rpState.table.importStatement(&rpState.tableContext, signedStatementWithPVD) + return rpState.table.importStatement(&rpState.tableContext, signedStatementWithPVD.SignedFullStatement) } if rpState.prospectiveParachainsMode.IsEnabled { @@ -109,15 +110,16 @@ func (rpState *perRelayParentState) importStatement( relayParent: statementVDTSeconded.Descriptor.RelayParent, } - return rpState.table.importStatement(&rpState.tableContext, signedStatementWithPVD) + return rpState.table.importStatement(&rpState.tableContext, signedStatementWithPVD.SignedFullStatement) } // postImportStatement handles a summary received from importStatement func and dispatches `Backed` notifications and // misbehaviors as a result of importing a statement. func (rpState *perRelayParentState) postImportStatement(subSystemToOverseer chan<- any, summary *Summary) { - // If the summary is nil, issue new misbehaviors and return. + defer issueNewMisbehaviors(subSystemToOverseer, rpState.relayParent, rpState.table) + + // Return, If the summary is nil. if summary == nil { - issueNewMisbehaviors(subSystemToOverseer, rpState.relayParent, rpState.table) return } @@ -126,23 +128,19 @@ func (rpState *perRelayParentState) postImportStatement(subSystemToOverseer chan logger.Error(err.Error()) } - // If the candidate is not attested, issue new misbehaviors and return. + // Return, If the candidate is not attested. if attested == nil { - issueNewMisbehaviors(subSystemToOverseer, rpState.relayParent, rpState.table) return } - hash, err := attested.committedCandidateReceipt.Hash() + candidateHash, err := parachaintypes.GetCandidateHash(attested.committedCandidateReceipt) if err != nil { logger.Error(err.Error()) return } - candidateHash := parachaintypes.CandidateHash{Value: hash} - - // If the candidate is already backed, issue new misbehaviors and return. + // Return, If the candidate is already backed. if rpState.backed[candidateHash] { - issueNewMisbehaviors(subSystemToOverseer, rpState.relayParent, rpState.table) return } @@ -150,9 +148,8 @@ func (rpState *perRelayParentState) postImportStatement(subSystemToOverseer chan rpState.backed[candidateHash] = true // Convert the attested candidate to a backed candidate. - backedCandidate := attestedToBackedCandidate(*attested, &rpState.tableContext) + backedCandidate := attested.toBackedCandidate(&rpState.tableContext) if backedCandidate == nil { - issueNewMisbehaviors(subSystemToOverseer, rpState.relayParent, rpState.table) return } @@ -188,8 +185,6 @@ func (rpState *perRelayParentState) postImportStatement(subSystemToOverseer chan ProvisionableData: parachaintypes.ProvisionableDataBackedCandidate(backedCandidate.Candidate.ToPlain()), } } - - issueNewMisbehaviors(subSystemToOverseer, rpState.relayParent, rpState.table) } // issueNewMisbehaviors checks for new misbehaviors and sends necessary messages to the Overseer subsystem. @@ -215,38 +210,6 @@ func issueNewMisbehaviors(subSystemToOverseer chan<- any, relayParent common.Has } } -func attestedToBackedCandidate( - attested attestedCandidate, - tableContext *TableContext, -) *parachaintypes.BackedCandidate { - group := tableContext.groups[attested.groupID] - validatorIndices := make([]bool, len(group)) - var validityAttestations []parachaintypes.ValidityAttestation - - // The order of the validity votes in the backed candidate must match - // the order of bits set in the bitfield, which is not necessarily - // the order of the `validity_votes` we got from the table. - for positionInGroup, validatorIndex := range group { - for _, validityVote := range attested.validityAttestations { - if validityVote.validatorIndex == validatorIndex { - validatorIndices[positionInGroup] = true - validityAttestations = append(validityAttestations, validityVote.validityAttestation) - } - } - - if !validatorIndices[positionInGroup] { - logger.Error("validity vote from unknown validator") - return nil - } - } - - return ¶chaintypes.BackedCandidate{ - Candidate: attested.committedCandidateReceipt, - ValidityVotes: validityAttestations, - ValidatorIndices: scale.NewBitVec(validatorIndices), - } -} - // Kick off validation work and distribute the result as a signed statement. func (rpState *perRelayParentState) kickOffValidationWork( blockState BlockState, diff --git a/dot/parachain/backing/statement_table.go b/dot/parachain/backing/statement_table.go index 24a1b63343..a1dc97ce95 100644 --- a/dot/parachain/backing/statement_table.go +++ b/dot/parachain/backing/statement_table.go @@ -10,26 +10,26 @@ import ( "slices" parachaintypes "github.com/ChainSafe/gossamer/dot/parachain/types" + "github.com/ChainSafe/gossamer/pkg/scale" ) var errCandidateDataNotFound = errors.New("candidate data not found") var errNotEnoughValidityVotes = errors.New("not enough validity votes") +var errUnknownValidityVote = errors.New("unknown validity vote") + +var _ Table = (*statementTable)(nil) // statementTable implements the Table interface. type statementTable struct { - authorityData map[parachaintypes.ValidatorIndex]authorityData //nolint:unused - candidateVotes map[parachaintypes.CandidateHash]candidateData - config tableConfig //nolint:unused - - // TODO: Implement this - // detected_misbehaviour: HashMap>>, + authorityData map[parachaintypes.ValidatorIndex][]proposal + detectedMisbehaviour map[parachaintypes.ValidatorIndex][]parachaintypes.Misbehaviour + candidateVotes map[parachaintypes.CandidateHash]*candidateData + config tableConfig } -type authorityData []proposal //nolint:unused - -type proposal struct { //nolint:unused +type proposal struct { candidateHash parachaintypes.CandidateHash - signature parachaintypes.Signature + signature parachaintypes.ValidatorSignature } type candidateData struct { @@ -38,9 +38,17 @@ type candidateData struct { validityVotes map[parachaintypes.ValidatorIndex]validityVoteWithSign } +func (data *candidateData) getSummary(candidateHash parachaintypes.CandidateHash) *Summary { + return &Summary{ + GroupID: data.groupID, + Candidate: candidateHash, + ValidityVotes: uint(len(data.validityVotes)), + } +} + // attested yields a full attestation for a candidate. // If the candidate can be included, it will return attested candidate. -func (data candidateData) attested(validityThreshold uint) (*attestedCandidate, error) { +func (data *candidateData) attested(validityThreshold uint) (*attestedCandidate, error) { numOfValidityVotes := uint(len(data.validityVotes)) if numOfValidityVotes < validityThreshold { return nil, fmt.Errorf("%w: %d < %d", errNotEnoughValidityVotes, numOfValidityVotes, validityThreshold) @@ -72,7 +80,7 @@ func (data candidateData) attested(validityThreshold uint) (*attestedCandidate, validityAttestation: attestation, }) default: - return nil, fmt.Errorf("unknown validity vote: %d", voteWithSign.validityVote) + return nil, fmt.Errorf("%w: %d", errUnknownValidityVote, voteWithSign.validityVote) } } @@ -103,7 +111,7 @@ const ( ) // getCommittedCandidateReceipt returns the committed candidate receipt for the given candidate hash. -func (table *statementTable) getCommittedCandidateReceipt(candidateHash parachaintypes.CandidateHash, //nolint:unused +func (table *statementTable) getCommittedCandidateReceipt(candidateHash parachaintypes.CandidateHash, ) (parachaintypes.CommittedCandidateReceipt, error) { data, ok := table.candidateVotes[candidateHash] if !ok { @@ -113,17 +121,239 @@ func (table *statementTable) getCommittedCandidateReceipt(candidateHash parachai return data.candidate, nil } -func (statementTable) importStatement( //nolint:unused - ctx *TableContext, statement parachaintypes.SignedFullStatementWithPVD, +// importStatement imports a statement into the table. +func (table *statementTable) importStatement( + tableCtx *tableContext, signedStatement parachaintypes.SignedFullStatement, ) (*Summary, error) { - // TODO: Implement this method - return nil, nil + var summary *Summary + var misbehaviour parachaintypes.Misbehaviour + + statementVDT, err := signedStatement.Payload.Value() + if err != nil { + return nil, fmt.Errorf("getting value from statement: %w", err) + } + + switch statementVDT := statementVDT.(type) { + case parachaintypes.Seconded: + summary, misbehaviour, err = table.importCandidate( + signedStatement.ValidatorIndex, + parachaintypes.CommittedCandidateReceipt(statementVDT), + signedStatement.Signature, + tableCtx, + ) + case parachaintypes.Valid: + summary, misbehaviour, err = table.validityVote( + signedStatement.ValidatorIndex, + parachaintypes.CandidateHash(statementVDT), + validityVoteWithSign{validityVote: valid, signature: signedStatement.Signature}, + tableCtx, + ) + } + + if err != nil && !errors.Is(err, errCandidateDataNotFound) { + return nil, err + } + + // If misbehaviour is detected, store it. + if misbehaviour != nil { + misbehaviors, ok := table.detectedMisbehaviour[signedStatement.ValidatorIndex] + if !ok { + misbehaviors = []parachaintypes.Misbehaviour{misbehaviour} + } else { + misbehaviors = append(misbehaviors, misbehaviour) + } + + table.detectedMisbehaviour[signedStatement.ValidatorIndex] = misbehaviors + } + + return summary, nil +} + +func isCandidateAlreadyProposed(proposals []proposal, candidateHash parachaintypes.CandidateHash) bool { + return slices.ContainsFunc(proposals, func(p proposal) bool { + return p.candidateHash == candidateHash + }) +} + +func (table *statementTable) importCandidate( + authority parachaintypes.ValidatorIndex, + candidate parachaintypes.CommittedCandidateReceipt, + signature parachaintypes.ValidatorSignature, + tableCtx *tableContext, +) (*Summary, parachaintypes.Misbehaviour, error) { + paraID := parachaintypes.ParaID(candidate.Descriptor.ParaID) + + if !tableCtx.isMemberOf(authority, paraID) { + statementSeconded := parachaintypes.NewStatementVDT() + err := statementSeconded.Set(parachaintypes.Seconded(candidate)) + if err != nil { + return nil, nil, fmt.Errorf("setting seconded statement: %w", err) + } + + misbehaviour := parachaintypes.UnauthorizedStatement{ + Payload: statementSeconded, + ValidatorIndex: authority, + Signature: signature, + } + + return nil, misbehaviour, nil + } + + candidateHash, err := parachaintypes.GetCandidateHash(candidate) + if err != nil { + return nil, nil, fmt.Errorf("getting candidate hash: %w", err) + } + + proposals, ok := table.authorityData[authority] + if !ok { + table.authorityData[authority] = []proposal{{candidateHash, signature}} + table.addCandidateVote(candidateHash, paraID, candidate) + + return table.validityVote(authority, candidateHash, + validityVoteWithSign{validityVote: issued, signature: signature}, tableCtx) + } + + switch { + case !table.config.allowMultipleSeconded && len(proposals) == 1: + oldCandidateHash := proposals[0].candidateHash + oldSignature := proposals[0].signature + + // if digest is different, fetch candidate and note misbehaviour. + if oldCandidateHash != candidateHash { + data, ok := table.candidateVotes[oldCandidateHash] + if !ok { + // when proposal first received from authority, candidate votes entry is created. + // and here proposals is not empty, so candidate votes entry should be present. + // So, this should never happen. + panic(fmt.Sprintf("%s for candidate-hash: %s", errCandidateDataNotFound, oldCandidateHash)) + } + + oldCandidate := data.candidate + + misbehaviour := parachaintypes.MultipleCandidates{ + First: parachaintypes.CommittedCandidateReceiptAndSign{ + CommittedCandidateReceipt: oldCandidate, + Signature: oldSignature, + }, + Second: parachaintypes.CommittedCandidateReceiptAndSign{ + CommittedCandidateReceipt: candidate, + Signature: signature, + }, + } + return nil, misbehaviour, nil + } + case table.config.allowMultipleSeconded && isCandidateAlreadyProposed(proposals, candidateHash): + // nothing to do here. + default: + proposals = append(proposals, proposal{candidateHash, signature}) + table.authorityData[authority] = proposals + + table.addCandidateVote(candidateHash, paraID, candidate) + } + + return table.validityVote(authority, candidateHash, + validityVoteWithSign{validityVote: issued, signature: signature}, tableCtx) +} + +func (table *statementTable) addCandidateVote( + candidateHash parachaintypes.CandidateHash, + paraID parachaintypes.ParaID, + candidate parachaintypes.CommittedCandidateReceipt, +) { + table.candidateVotes[candidateHash] = &candidateData{ + groupID: paraID, + candidate: candidate, + validityVotes: make(map[parachaintypes.ValidatorIndex]validityVoteWithSign), + } +} + +func (table *statementTable) validityVote( + from parachaintypes.ValidatorIndex, + candidateHash parachaintypes.CandidateHash, + voteWithSign validityVoteWithSign, + tableCtx *tableContext, +) (*Summary, parachaintypes.Misbehaviour, error) { + data, ok := table.candidateVotes[candidateHash] + if !ok { + return nil, nil, errCandidateDataNotFound + } + + // check that this authority actually can vote in this group. + if !tableCtx.isMemberOf(from, data.groupID) { + switch voteWithSign.validityVote { + case valid: + validStatement := parachaintypes.NewStatementVDT() + err := validStatement.Set(parachaintypes.Valid(candidateHash)) + if err != nil { + return nil, nil, fmt.Errorf("setting valid statement: %w", err) + } + + misbehaviour := parachaintypes.UnauthorizedStatement{ + Payload: validStatement, + ValidatorIndex: from, + Signature: voteWithSign.signature, + } + + return nil, misbehaviour, nil + case issued: + panic("implicit issuance vote must only cast from `importCandidate` after checking group membership of issuer.") + default: + return nil, nil, fmt.Errorf("%w: %d", errUnknownValidityVote, voteWithSign.validityVote) + } + } + + existingVoteWithSign, ok := data.validityVotes[from] + if !ok { + data.validityVotes[from] = voteWithSign + return data.getSummary(candidateHash), nil, nil + } + + // check for double votes. + if existingVoteWithSign != voteWithSign { + var misbehaviour parachaintypes.Misbehaviour + + switch { + // valid vote conflicting with candidate statement + case existingVoteWithSign.validityVote == issued && voteWithSign.validityVote == valid, + existingVoteWithSign.validityVote == valid && voteWithSign.validityVote == issued: + misbehaviour = parachaintypes.ValidityDoubleVoteIssuedAndValidity{ + CommittedCandidateReceiptAndSign: parachaintypes.CommittedCandidateReceiptAndSign{ + CommittedCandidateReceipt: data.candidate, + Signature: existingVoteWithSign.signature, + }, + CandidateHashAndSign: parachaintypes.CandidateHashAndSign{ + CandidateHash: candidateHash, + Signature: voteWithSign.signature, + }, + } + + // two signatures on same candidate + case existingVoteWithSign.validityVote == issued && voteWithSign.validityVote == issued: + misbehaviour = parachaintypes.DoubleSignOnSeconded{ + Candidate: data.candidate, + Sign1: existingVoteWithSign.signature, + Sign2: voteWithSign.signature, + } + + // two signatures on same validity vote + case existingVoteWithSign.validityVote == valid && voteWithSign.validityVote == valid: + misbehaviour = parachaintypes.DoubleSignOnValidity{ + CandidateHash: candidateHash, + Sign1: existingVoteWithSign.signature, + Sign2: voteWithSign.signature, + } + } + + return nil, misbehaviour, nil + } + + return nil, nil, nil } // attestedCandidate retrieves the attested candidate for the given candidate hash. // returns attested candidate if the candidate exists and is includable. -func (table statementTable) attestedCandidate( - candidateHash parachaintypes.CandidateHash, tableContext *TableContext, minimumBackingVotes uint32, +func (table *statementTable) attestedCandidate( + candidateHash parachaintypes.CandidateHash, tableCtx *tableContext, minimumBackingVotes uint32, ) (*attestedCandidate, error) { data, ok := table.candidateVotes[candidateHash] if !ok { @@ -131,7 +361,7 @@ func (table statementTable) attestedCandidate( } var validityThreshold uint - group, ok := tableContext.groups[data.groupID] + group, ok := tableCtx.groups[data.groupID] if ok { // size of the backing group. groupLen := uint(len(group)) @@ -150,21 +380,25 @@ func effectiveMinimumBackingVotes(groupLen uint, configuredMinimumBackingVotes u return min(groupLen, uint(configuredMinimumBackingVotes)) } -func (statementTable) drainMisbehaviors() []parachaintypes.ProvisionableDataMisbehaviorReport { //nolint:unused +func (statementTable) drainMisbehaviors() []parachaintypes.ProvisionableDataMisbehaviorReport { // TODO: Implement this method return nil } type Table interface { - getCandidate(parachaintypes.CandidateHash) (parachaintypes.CommittedCandidateReceipt, error) - importStatement(*TableContext, parachaintypes.SignedFullStatementWithPVD) (*Summary, error) - attestedCandidate(parachaintypes.CandidateHash, *TableContext, uint32) (*attestedCandidate, error) + getCommittedCandidateReceipt(parachaintypes.CandidateHash) (parachaintypes.CommittedCandidateReceipt, error) + importStatement(*tableContext, parachaintypes.SignedFullStatement) (*Summary, error) + attestedCandidate(parachaintypes.CandidateHash, *tableContext, uint32) (*attestedCandidate, error) drainMisbehaviors() []parachaintypes.ProvisionableDataMisbehaviorReport } -func newTable(tableConfig) Table { - // TODO: Implement this function - return nil +func newTable(config tableConfig) *statementTable { + return &statementTable{ + authorityData: make(map[parachaintypes.ValidatorIndex][]proposal), + detectedMisbehaviour: make(map[parachaintypes.ValidatorIndex][]parachaintypes.Misbehaviour), + candidateVotes: make(map[parachaintypes.CandidateHash]*candidateData), + config: config, + } } // Summary represents summary of import of a statement. @@ -174,7 +408,7 @@ type Summary struct { // The group that the candidate is in. GroupID parachaintypes.ParaID // How many validity votes are currently witnessed. - ValidityVotes uint64 + ValidityVotes uint } // attestedCandidate represents an attested-to candidate. @@ -187,6 +421,35 @@ type attestedCandidate struct { validityAttestations []validatorIndexWithAttestation } +func (attested *attestedCandidate) toBackedCandidate(tableCtx *tableContext) *parachaintypes.BackedCandidate { + group := tableCtx.groups[attested.groupID] + validatorIndices := make([]bool, len(group)) + var validityAttestations []parachaintypes.ValidityAttestation + + // The order of the validity votes in the backed candidate must match + // the order of bits set in the bitfield, which is not necessarily + // the order of the `validity_votes` we got from the table. + for positionInGroup, validatorIndex := range group { + for _, validityVote := range attested.validityAttestations { + if validityVote.validatorIndex == validatorIndex { + validatorIndices[positionInGroup] = true + validityAttestations = append(validityAttestations, validityVote.validityAttestation) + } + } + + if !validatorIndices[positionInGroup] { + logger.Error("validity vote from unknown validator") + return nil + } + } + + return ¶chaintypes.BackedCandidate{ + Candidate: attested.committedCandidateReceipt, + ValidityVotes: validityAttestations, + ValidatorIndices: scale.NewBitVec(validatorIndices), + } +} + // validatorIndexWithAttestation represents a validity attestation for a candidate. type validatorIndexWithAttestation struct { validatorIndex parachaintypes.ValidatorIndex diff --git a/dot/parachain/backing/statement_table_test.go b/dot/parachain/backing/statement_table_test.go index 2e7cda98d2..affff0ff9d 100644 --- a/dot/parachain/backing/statement_table_test.go +++ b/dot/parachain/backing/statement_table_test.go @@ -4,6 +4,7 @@ import ( "testing" parachaintypes "github.com/ChainSafe/gossamer/dot/parachain/types" + "github.com/ChainSafe/gossamer/lib/common" "github.com/stretchr/testify/require" ) @@ -79,7 +80,7 @@ func TestStatementTable_attestedCandidate(t *testing.T) { type args struct { candidateHash parachaintypes.CandidateHash - tableContext *TableContext + tableContext *tableContext minimumBackingVotes uint32 } tests := []struct { @@ -92,7 +93,7 @@ func TestStatementTable_attestedCandidate(t *testing.T) { { name: "candidate_votes_not_available_for_given_candidate_hash", table: &statementTable{ - candidateVotes: map[parachaintypes.CandidateHash]candidateData{}, + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{}, }, args: args{ candidateHash: dummyCandidateHash(t), @@ -102,7 +103,7 @@ func TestStatementTable_attestedCandidate(t *testing.T) { { name: "not_enough_validity_votes", table: &statementTable{ - candidateVotes: map[parachaintypes.CandidateHash]candidateData{ + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ dummyCandidateHash(t): { groupID: 1, validityVotes: map[parachaintypes.ValidatorIndex]validityVoteWithSign{}, @@ -111,7 +112,7 @@ func TestStatementTable_attestedCandidate(t *testing.T) { }, args: args{ candidateHash: dummyCandidateHash(t), - tableContext: &TableContext{ + tableContext: &tableContext{ groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ 1: {1, 2, 3}, 2: {4, 5, 6}, @@ -138,3 +139,468 @@ func TestStatementTable_attestedCandidate(t *testing.T) { }) } } + +func TestStatementTable_importStatement(t *testing.T) { + t.Parallel() + committedCandidate := getDummyCommittedCandidateReceipt(t) + + testCases := []struct { + description string + statementVDT parachaintypes.StatementVDT + detectedMisbehaviourLen int + }{ + { + description: "seconded_statement", + statementVDT: func() parachaintypes.StatementVDT { + secondedStatement := parachaintypes.NewStatementVDT() + err := secondedStatement.Set(parachaintypes.Seconded(committedCandidate)) + require.NoError(t, err) + + return secondedStatement + }(), + detectedMisbehaviourLen: 1, + }, + { + description: "valid_statement", + statementVDT: func() parachaintypes.StatementVDT { + candidateHash, err := parachaintypes.GetCandidateHash(committedCandidate) + require.NoError(t, err) + + validStatement := parachaintypes.NewStatementVDT() + err = validStatement.Set(parachaintypes.Valid(candidateHash)) + require.NoError(t, err) + + return validStatement + }(), + detectedMisbehaviourLen: 0, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + + tableCtx := &tableContext{} + signedStatement := parachaintypes.SignedFullStatement{ + Payload: tc.statementVDT, + } + + table := newTable(tableConfig{}) + + summary, err := table.importStatement(tableCtx, signedStatement) + require.NoError(t, err) + require.Nil(t, summary) + + require.Len(t, table.detectedMisbehaviour, tc.detectedMisbehaviourLen) + }) + } +} + +func TestStatementTable_importCandidate(t *testing.T) { + t.Parallel() + + authority := parachaintypes.ValidatorIndex(10) + candidate := getDummyCommittedCandidateReceipt(t) + var signature parachaintypes.ValidatorSignature + + var tempSignature = common.MustHexToBytes("0xc67cb93bf0a36fcee3d29de8a6a69a759659680acf486475e0a2552a5fbed87e45adce5f290698d8596095722b33599227f7461f51af8617c8be74b894cf1b86") //nolint:lll + copy(signature[:], tempSignature) + + statementSeconded := parachaintypes.NewStatementVDT() + err := statementSeconded.Set(parachaintypes.Seconded(candidate)) + require.NoError(t, err) + + candidateHash, err := parachaintypes.GetCandidateHash(candidate) + require.NoError(t, err) + + oldCandidate := parachaintypes.CommittedCandidateReceipt{} + oldCandidateHash, err := parachaintypes.GetCandidateHash(oldCandidate) + require.NoError(t, err) + + oldSign := parachaintypes.ValidatorSignature{1, 2, 3} + + testCases := []struct { + description string + tableCtx *tableContext + table *statementTable + expectedError error + expectedMisehaviour parachaintypes.Misbehaviour + expectedSummary *Summary + }{ + { + description: "validator_not_present_in_group_of_parachain_validators", + tableCtx: &tableContext{}, + table: newTable(tableConfig{}), + expectedError: nil, + expectedMisehaviour: parachaintypes.UnauthorizedStatement{ + Payload: statementSeconded, + ValidatorIndex: authority, + Signature: signature, + }, + expectedSummary: nil, + }, + { + description: "no_proposals_available_from_the_validator", + tableCtx: &tableContext{ + groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ + 1: {10}, + }, + }, + table: newTable(tableConfig{}), + expectedError: nil, + expectedMisehaviour: nil, + expectedSummary: &Summary{ + Candidate: candidateHash, + GroupID: parachaintypes.ParaID(candidate.Descriptor.ParaID), + ValidityVotes: 1, + }, + }, + { + description: "multiple_seconded_not_allowed_and_a_proposal_already_exists_for_different_candidate", + tableCtx: &tableContext{ + groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ + 1: {10}, + }, + }, + table: &statementTable{ + authorityData: map[parachaintypes.ValidatorIndex][]proposal{ + authority: { + { + candidateHash: oldCandidateHash, + signature: oldSign, + }, + }, + }, + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ + oldCandidateHash: { + groupID: 1, + candidate: oldCandidate, + validityVotes: make(map[parachaintypes.ValidatorIndex]validityVoteWithSign), + }, + }, + }, + expectedError: nil, + expectedMisehaviour: parachaintypes.MultipleCandidates{ + First: parachaintypes.CommittedCandidateReceiptAndSign{ + CommittedCandidateReceipt: oldCandidate, + Signature: oldSign, + }, + Second: parachaintypes.CommittedCandidateReceiptAndSign{ + CommittedCandidateReceipt: candidate, + Signature: signature, + }, + }, + expectedSummary: nil, + }, + { + description: "multiple_seconded_allowed_and_a_proposal_already_exists_for_current_candidate", + tableCtx: &tableContext{ + groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ + 1: {10}, + }, + }, + table: &statementTable{ + authorityData: map[parachaintypes.ValidatorIndex][]proposal{ + authority: { + { + candidateHash: candidateHash, + signature: signature, + }, + }, + }, + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ + candidateHash: { + groupID: 1, + candidate: candidate, + validityVotes: make(map[parachaintypes.ValidatorIndex]validityVoteWithSign), + }, + }, + config: tableConfig{ + allowMultipleSeconded: true, + }, + }, + expectedError: nil, + expectedMisehaviour: nil, + expectedSummary: &Summary{ + Candidate: candidateHash, + GroupID: parachaintypes.ParaID(candidate.Descriptor.ParaID), + ValidityVotes: 1, + }, + }, + { + description: "multiple_seconded_allowed_and_a_proposal_not_exists_for_current_candidate", + tableCtx: &tableContext{ + groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ + 1: {10}, + }, + }, + table: &statementTable{ + authorityData: map[parachaintypes.ValidatorIndex][]proposal{ + authority: { + { + candidateHash: oldCandidateHash, + signature: oldSign, + }, + }, + }, + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ + oldCandidateHash: { + groupID: 1, + candidate: oldCandidate, + validityVotes: make(map[parachaintypes.ValidatorIndex]validityVoteWithSign), + }, + }, + config: tableConfig{ + allowMultipleSeconded: true, + }, + }, + expectedError: nil, + expectedMisehaviour: nil, + expectedSummary: &Summary{ + Candidate: candidateHash, + GroupID: parachaintypes.ParaID(candidate.Descriptor.ParaID), + ValidityVotes: 1, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + + summary, misehaviour, err := tc.table.importCandidate(authority, candidate, signature, tc.tableCtx) + require.Equal(t, tc.expectedError, err) + require.Equal(t, tc.expectedMisehaviour, misehaviour) + require.Equal(t, tc.expectedSummary, summary) + + }) + } +} + +func TestStatementTable_validityVote(t *testing.T) { + t.Parallel() + + validatorIndex := parachaintypes.ValidatorIndex(10) + committedCandidate := getDummyCommittedCandidateReceipt(t) + + candidateHash, err := parachaintypes.GetCandidateHash(committedCandidate) + require.NoError(t, err) + + validStatement := parachaintypes.NewStatementVDT() + err = validStatement.Set(parachaintypes.Valid(candidateHash)) + require.NoError(t, err) + + var validatorSign parachaintypes.ValidatorSignature + var tempSignature = common.MustHexToBytes("0xc67cb93bf0a36fcee3d29de8a6a69a759659680acf486475e0a2552a5fbed87e45adce5f290698d8596095722b33599227f7461f51af8617c8be74b894cf1b86") //nolint:lll + copy(validatorSign[:], tempSignature) + + oldSign := parachaintypes.ValidatorSignature{} + + testCases := []struct { + description string + vote validityVote + tableCtx *tableContext + table *statementTable + expectedError error + expectedMisehaviour parachaintypes.Misbehaviour + expectedSummary *Summary + }{ + { + description: "no_votes_available_for_the_given_candidate_hash", + vote: valid, + tableCtx: &tableContext{}, + table: newTable(tableConfig{}), + expectedError: errCandidateDataNotFound, + expectedMisehaviour: nil, + expectedSummary: nil, + }, + { + description: "validator_index_not_present_in_group_of_parachain_validators", + vote: valid, + tableCtx: &tableContext{}, + table: &statementTable{ + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ + candidateHash: { + groupID: 1, + candidate: committedCandidate, + validityVotes: make(map[parachaintypes.ValidatorIndex]validityVoteWithSign), + }, + }, + }, + expectedError: nil, + expectedMisehaviour: parachaintypes.UnauthorizedStatement{ + Payload: validStatement, + ValidatorIndex: validatorIndex, + Signature: validatorSign, + }, + expectedSummary: nil, + }, + { + description: "validity_vote_not_available_from_the_given_validator_index", + vote: valid, + tableCtx: &tableContext{ + groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ + 1: {10}, + }, + }, + table: &statementTable{ + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ + candidateHash: { + groupID: 1, + candidate: committedCandidate, + validityVotes: make(map[parachaintypes.ValidatorIndex]validityVoteWithSign), + }, + }, + }, + expectedError: nil, + expectedMisehaviour: nil, + expectedSummary: &Summary{ + Candidate: candidateHash, + GroupID: parachaintypes.ParaID(committedCandidate.Descriptor.ParaID), + ValidityVotes: 1, + }, + }, + { + description: "validity_vote_available_from_validator_index_with_same_vote_and_sign", + vote: valid, + tableCtx: &tableContext{ + groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ + 1: {10}, + }, + }, + table: &statementTable{ + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ + candidateHash: { + groupID: 1, + candidate: committedCandidate, + validityVotes: map[parachaintypes.ValidatorIndex]validityVoteWithSign{ + validatorIndex: { + validityVote: valid, + signature: validatorSign, + }, + }, + }, + }, + }, + expectedError: nil, + expectedMisehaviour: nil, + expectedSummary: nil, + }, + // below cases to check double votes + { + description: "vote_confict_with_candidate_statement", + vote: valid, + tableCtx: &tableContext{ + groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ + 1: {10}, + }, + }, + table: &statementTable{ + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ + candidateHash: { + groupID: 1, + candidate: committedCandidate, + validityVotes: map[parachaintypes.ValidatorIndex]validityVoteWithSign{ + validatorIndex: { + validityVote: issued, + signature: oldSign, + }, + }, + }, + }, + }, + expectedError: nil, + expectedMisehaviour: parachaintypes.ValidityDoubleVoteIssuedAndValidity{ + CommittedCandidateReceiptAndSign: parachaintypes.CommittedCandidateReceiptAndSign{ + CommittedCandidateReceipt: committedCandidate, + Signature: oldSign, + }, + CandidateHashAndSign: parachaintypes.CandidateHashAndSign{ + CandidateHash: candidateHash, + Signature: validatorSign, + }, + }, + expectedSummary: nil, + }, + { + description: "two_signatures_on_same_validity_vote", + vote: valid, + tableCtx: &tableContext{ + groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ + 1: {10}, + }, + }, + table: &statementTable{ + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ + candidateHash: { + groupID: 1, + candidate: committedCandidate, + validityVotes: map[parachaintypes.ValidatorIndex]validityVoteWithSign{ + validatorIndex: { + validityVote: valid, + signature: oldSign, + }, + }, + }, + }, + }, + expectedError: nil, + expectedMisehaviour: parachaintypes.DoubleSignOnValidity{ + CandidateHash: candidateHash, + Sign1: oldSign, + Sign2: validatorSign, + }, + expectedSummary: nil, + }, + { + description: "two_signatures_on_same_seconded_candidate", + vote: issued, + tableCtx: &tableContext{ + groups: map[parachaintypes.ParaID][]parachaintypes.ValidatorIndex{ + 1: {10}, + }, + }, + table: &statementTable{ + candidateVotes: map[parachaintypes.CandidateHash]*candidateData{ + candidateHash: { + groupID: 1, + candidate: committedCandidate, + validityVotes: map[parachaintypes.ValidatorIndex]validityVoteWithSign{ + validatorIndex: { + validityVote: issued, + signature: oldSign, + }, + }, + }, + }, + }, + expectedError: nil, + expectedMisehaviour: parachaintypes.DoubleSignOnSeconded{ + Candidate: committedCandidate, + Sign1: oldSign, + Sign2: validatorSign, + }, + expectedSummary: nil, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run("", func(t *testing.T) { + t.Parallel() + + summary, misehaviour, err := tc.table.validityVote( + validatorIndex, + candidateHash, + validityVoteWithSign{tc.vote, validatorSign}, + tc.tableCtx, + ) + + require.Equal(t, tc.expectedError, err) + require.Equal(t, tc.expectedMisehaviour, misehaviour) + require.Equal(t, tc.expectedSummary, summary) + }) + } +} diff --git a/dot/parachain/types/misbehavior.go b/dot/parachain/types/misbehavior.go index 35c0ff9d67..2bb83a444d 100644 --- a/dot/parachain/types/misbehavior.go +++ b/dot/parachain/types/misbehavior.go @@ -4,14 +4,11 @@ package parachaintypes var ( - _ Misbehaviour = (*MultipleCandidates)(nil) - _ Misbehaviour = (*UnauthorizedStatement)(nil) - _ Misbehaviour = (*IssuedAndValidity)(nil) - _ Misbehaviour = (*OnSeconded)(nil) - _ Misbehaviour = (*OnValidity)(nil) - _ DoubleSign = (*OnSeconded)(nil) - _ DoubleSign = (*OnValidity)(nil) - _ ValidityDoubleVote = (*IssuedAndValidity)(nil) + _ Misbehaviour = (*MultipleCandidates)(nil) + _ Misbehaviour = (*UnauthorizedStatement)(nil) + _ Misbehaviour = (*ValidityDoubleVoteIssuedAndValidity)(nil) + _ Misbehaviour = (*DoubleSignOnSeconded)(nil) + _ Misbehaviour = (*DoubleSignOnValidity)(nil) ) // Misbehaviour is intended to represent different kinds of misbehaviour along with supporting proofs. @@ -19,25 +16,17 @@ type Misbehaviour interface { IsMisbehaviour() } +// ValidityDoubleVoteIssuedAndValidity misbehaviour: voting implicitly by issuing and explicit voting for validity. +// // ValidityDoubleVote misbehaviour: voting more than one way on candidate validity. // Since there are three possible ways to vote, a double vote is possible in // three possible combinations (unordered) -type ValidityDoubleVote interface { - Misbehaviour - IsValidityDoubleVote() -} - -// IssuedAndValidity represents an implicit vote by issuing and explicit voting for validity. -type IssuedAndValidity struct { +type ValidityDoubleVoteIssuedAndValidity struct { CommittedCandidateReceiptAndSign CommittedCandidateReceiptAndSign - CandidateHashAndSign struct { - CandidateHash CandidateHash - Signature ValidatorSignature - } + CandidateHashAndSign CandidateHashAndSign } -func (IssuedAndValidity) IsMisbehaviour() {} -func (IssuedAndValidity) IsValidityDoubleVote() {} +func (ValidityDoubleVoteIssuedAndValidity) IsMisbehaviour() {} // CommittedCandidateReceiptAndSign combines a committed candidate receipt and its associated signature. type CommittedCandidateReceiptAndSign struct { @@ -45,6 +34,12 @@ type CommittedCandidateReceiptAndSign struct { Signature ValidatorSignature } +// CandidateHashAndSign combines a candidate hash and its associated signature. +type CandidateHashAndSign struct { + CandidateHash CandidateHash + Signature ValidatorSignature +} + // MultipleCandidates misbehaviour: declaring multiple candidates. type MultipleCandidates struct { First CommittedCandidateReceiptAndSign @@ -53,43 +48,26 @@ type MultipleCandidates struct { func (MultipleCandidates) IsMisbehaviour() {} -// SignedStatement represents signed statements about candidates. -type SignedStatement struct { - Statement StatementVDT `scale:"1"` - Signature ValidatorSignature `scale:"2"` - Sender ValidatorIndex `scale:"3"` -} - // UnauthorizedStatement misbehaviour: submitted statement for wrong group. -type UnauthorizedStatement struct { - // A signed statement which was submitted without proper authority. - Statement SignedStatement -} +// A signed statement which was submitted without proper authority. +type UnauthorizedStatement SignedFullStatement func (UnauthorizedStatement) IsMisbehaviour() {} -// DoubleSign misbehaviour: multiple signatures on same statement. -type DoubleSign interface { - Misbehaviour - IsDoubleSign() -} - -// OnSeconded represents a double sign on a candidate. -type OnSeconded struct { +// DoubleSignOnSeconded represents a double sign on a candidate. +type DoubleSignOnSeconded struct { Candidate CommittedCandidateReceipt Sign1 ValidatorSignature Sign2 ValidatorSignature } -func (OnSeconded) IsMisbehaviour() {} -func (OnSeconded) IsDoubleSign() {} +func (DoubleSignOnSeconded) IsMisbehaviour() {} -// OnValidity represents a double sign on validity. -type OnValidity struct { +// DoubleSignOnValidity represents a double sign on validity. +type DoubleSignOnValidity struct { CandidateHash CandidateHash Sign1 ValidatorSignature Sign2 ValidatorSignature } -func (OnValidity) IsMisbehaviour() {} -func (OnValidity) IsDoubleSign() {} +func (DoubleSignOnValidity) IsMisbehaviour() {} diff --git a/dot/parachain/types/types.go b/dot/parachain/types/types.go index 518074d4ff..d2d9de510e 100644 --- a/dot/parachain/types/types.go +++ b/dot/parachain/types/types.go @@ -301,6 +301,22 @@ func (c CommittedCandidateReceipt) Hash() (common.Hash, error) { return c.ToPlain().Hash() } +type hashable interface { + Hash() (common.Hash, error) +} + +// GetCandidateHash returns the CandidateHash. +// +// candidate would be either CommittedCandidateReceipt or CandidateReceipt. +func GetCandidateHash(candidate hashable) (CandidateHash, error) { + h, err := candidate.Hash() + if err != nil { + return CandidateHash{}, err + } + + return CandidateHash{Value: h}, nil +} + // AssignmentID The public key of a keypair used by a validator for determining assignments // to approve included parachain candidates. type AssignmentID [sr25519.PublicKeyLength]byte