Skip to content

Commit

Permalink
test(dot/parachain/backing): can not second multiple candidate per re…
Browse files Browse the repository at this point in the history
…lay parent without prospective parachain (#4134)

- I have written a test to ensure it's impossible to second multiple candidates per relay parent when prospective parachain mode is inactive(async backing is not supported).
    - received a candidate backing message to second a candidate. Ensured it seconds the candidate.
    - I received another candidate backing message to second a candidate with the same relay parent. I ensured this candidate got rejected.
- Written some helper functions to reuse the code.
- Added a function to stop the mockable overseer and wait some time to finish the ongoing process before exiting the test.
- I have added an extra check to handle expected overseer action in the mockable overseer.
- Because of this extra check, other tests failed, so I fixed them.
  • Loading branch information
axaysagathiya authored Aug 22, 2024
1 parent 7cdb52b commit baea974
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 94 deletions.
277 changes: 204 additions & 73 deletions dot/parachain/backing/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import (
gomock "go.uber.org/mock/gomock"
)

// Ensure overseer stops before test completion
func stopOverseerAndWaitForCompletion(overseer *overseer.MockableOverseer) {
overseer.Stop()
time.Sleep(100 * time.Millisecond) // Give some time for any ongoing processes to finish
}

// register the backing subsystem, run backing subsystem, start overseer
func initBackingAndOverseerMock(t *testing.T) (*backing.CandidateBacking, *overseer.MockableOverseer) {
t.Helper()
Expand Down Expand Up @@ -207,10 +213,52 @@ func signingContext(t *testing.T) parachaintypes.SigningContext {
}
}

// this is a helper function to create an expected action for the ValidateFromExhaustive message
// that will return a valid result
func validResponseForValidateFromExhaustive(
headData parachaintypes.HeadData,
pvd parachaintypes.PersistedValidationData,
) func(msg any) bool {
return func(msg any) bool {
msgValidate, ok := msg.(candidatevalidation.ValidateFromExhaustive)
if !ok {
return false
}

msgValidate.Ch <- parachaintypes.OverseerFuncRes[candidatevalidation.ValidationResult]{
Data: candidatevalidation.ValidationResult{
ValidResult: &candidatevalidation.ValidValidationResult{
CandidateCommitments: parachaintypes.CandidateCommitments{
HeadData: headData,
UpwardMessages: []parachaintypes.UpwardMessage{},
HorizontalMessages: []parachaintypes.OutboundHrmpMessage{},
NewValidationCode: nil,
ProcessedDownwardMessages: 0,
HrmpWatermark: 0,
},
PersistedValidationData: pvd,
},
},
}
return true
}
}

// this is a expected action for the StoreAvailableData message that will return a nil error
func storeAvailableData(msg any) bool {
store, ok := msg.(availabilitystore.StoreAvailableData)
if !ok {
return false
}

store.Sender <- nil
return true
}

// we can second a valid candidate when the previous candidate has been found invalid
func TestSecondsValidCandidate(t *testing.T) {
candidateBacking, overseer := initBackingAndOverseerMock(t)
defer overseer.Stop()
defer stopOverseerAndWaitForCompletion(overseer)

paraValidators := parachainValidators(t, candidateBacking.Keystore)
numOfValidators := uint(len(paraValidators))
Expand Down Expand Up @@ -313,7 +361,7 @@ func TestSecondsValidCandidate(t *testing.T) {
PoV: pov1,
})

time.Sleep(3 * time.Second)
time.Sleep(1 * time.Second)

pov2 := parachaintypes.PoV{BlockData: []byte{45, 46, 47}}

Expand Down Expand Up @@ -344,38 +392,7 @@ func TestSecondsValidCandidate(t *testing.T) {
mockRuntime.EXPECT().ParachainHostValidationCodeByHash(gomock.AssignableToTypeOf(common.Hash{})).
Return(&validationCode2, nil)

validate2 := func(msg any) bool {
validateFromExhaustive, ok := msg.(candidatevalidation.ValidateFromExhaustive)
if !ok {
return false
}

validateFromExhaustive.Ch <- parachaintypes.OverseerFuncRes[candidatevalidation.ValidationResult]{
Data: candidatevalidation.ValidationResult{
ValidResult: &candidatevalidation.ValidValidationResult{
CandidateCommitments: parachaintypes.CandidateCommitments{
UpwardMessages: []parachaintypes.UpwardMessage{},
HorizontalMessages: []parachaintypes.OutboundHrmpMessage{},
NewValidationCode: nil,
HeadData: candidate2.Commitments.HeadData,
ProcessedDownwardMessages: 0,
HrmpWatermark: 0,
},
PersistedValidationData: pvd2,
},
},
}
return true
}

storeAvailableData := func(msg any) bool {
store, ok := msg.(availabilitystore.StoreAvailableData)
if !ok {
return false
}
store.Sender <- nil
return true
}
validate2 := validResponseForValidateFromExhaustive(candidate2.Commitments.HeadData, pvd2)

distribute := func(msg any) bool {
// we have seconded a candidate and shared the statement to peers
Expand Down Expand Up @@ -412,14 +429,14 @@ func TestSecondsValidCandidate(t *testing.T) {
PoV: pov2,
})

time.Sleep(3 * time.Second)
time.Sleep(1 * time.Second)
}

// candidate reaches quorum.
// in legacy backing, we need 2 approvals to reach quorum.
func TestCandidateReachesQuorum(t *testing.T) {
candidateBacking, overseer := initBackingAndOverseerMock(t)
defer overseer.Stop()
defer stopOverseerAndWaitForCompletion(overseer)

paraValidators := parachainValidators(t, candidateBacking.Keystore)
numOfValidators := uint(len(paraValidators))
Expand Down Expand Up @@ -519,47 +536,20 @@ func TestCandidateReachesQuorum(t *testing.T) {
return true
}

validate1 := func(msg any) bool {
msgValidate, ok := msg.(candidatevalidation.ValidateFromExhaustive)
if !ok {
return false
}

msgValidate.Ch <- parachaintypes.OverseerFuncRes[candidatevalidation.ValidationResult]{
Data: candidatevalidation.ValidationResult{
ValidResult: &candidatevalidation.ValidValidationResult{
CandidateCommitments: parachaintypes.CandidateCommitments{
HeadData: headData,
UpwardMessages: []parachaintypes.UpwardMessage{},
HorizontalMessages: []parachaintypes.OutboundHrmpMessage{},
NewValidationCode: nil,
ProcessedDownwardMessages: 0,
HrmpWatermark: 0,
},
PersistedValidationData: pvd,
},
},
}
return true
}

storeData := func(msg any) bool {
store, ok := msg.(availabilitystore.StoreAvailableData)
if !ok {
return false
}

store.Sender <- nil
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, validate1, storeData, distribute)
overseer.ExpectActions(fetchPov, validate, storeAvailableData, distribute, provisionerMessageProvisionableData)

// receive statement message from overseer to candidate backing subsystem containing seconded statement
overseer.ReceiveMessage(backing.StatementMessage{
Expand Down Expand Up @@ -632,15 +622,13 @@ func TestCandidateReachesQuorum(t *testing.T) {
// as it is a valid statement, we do not validate the candidate, just store into the statement table.
require.Len(t, backableCandidates, 1)
require.Len(t, backableCandidates[0].ValidityVotes, 3)

time.Sleep(3 * time.Second)
}

// if the validation of the candidate has failed this does not stop the work of this subsystem
// and so it is not fatal to the node.
func TestValidationFailDoesNotStopSubsystem(t *testing.T) {
candidateBacking, overseer := initBackingAndOverseerMock(t)
defer overseer.Stop()
defer stopOverseerAndWaitForCompletion(overseer)

paraValidators := parachainValidators(t, candidateBacking.Keystore)
numOfValidators := uint(len(paraValidators))
Expand Down Expand Up @@ -779,3 +767,146 @@ func TestValidationFailDoesNotStopSubsystem(t *testing.T) {

require.Len(t, backableCandidates, 0)
}

// It's impossible to second multiple candidates per relay parent without prospective parachains.
func TestCanNotSecondMultipleCandidatesPerRelayParent(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)

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(4)

// mock Runtime Instance methods
mockRuntime.EXPECT().ParachainHostAsyncBackingParams().
Return(nil, wazero_runtime.ErrExportFunctionNotFound)
mockRuntime.EXPECT().ParachainHostSessionIndexForChild().
Return(parachaintypes.SessionIndex(1), nil).Times(3)
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().
ParachainHostSessionExecutorParams(gomock.AssignableToTypeOf(parachaintypes.SessionIndex(0))).
Return(nil, wazero_runtime.ErrExportFunctionNotFound).Times(2)

//mock ImplicitView
mockImplicitView.EXPECT().AllAllowedRelayParents().
Return([]common.Hash{})

// to make entry in perRelayParent map
overseer.ReceiveMessage(parachaintypes.ActiveLeavesUpdateSignal{
Activated: &parachaintypes.ActivatedLeaf{Hash: relayParent, Number: 1},
})

time.Sleep(1 * time.Second)

headData := parachaintypes.HeadData{Data: []byte{4, 5, 6}}

pov := parachaintypes.PoV{BlockData: []byte{1, 2, 3}}
povHash, err := pov.Hash()
require.NoError(t, err)

pvd := dummyPVD(t)
pvdHash, err := pvd.Hash()
require.NoError(t, err)

validationCode1 := parachaintypes.ValidationCode{1, 2, 3}

candidate1 := newCommittedCandidate(t,
paraID,
headData,
povHash,
relayParent,
makeErasureRoot(t, numOfValidators, pov, pvd),
pvdHash,
validationCode1,
)

validate := validResponseForValidateFromExhaustive(headData, pvd)

distribute := func(msg any) bool {
// we have seconded a candidate and shared the statement to peers
share, ok := msg.(parachaintypes.StatementDistributionMessageShare)
if !ok {
return false
}

statement, err := share.SignedFullStatementWithPVD.SignedFullStatement.Payload.Value()
require.NoError(t, err)

require.Equal(t, statement, parachaintypes.Seconded(candidate1))
require.Equal(t, *share.SignedFullStatementWithPVD.PersistedValidationData, pvd)
require.Equal(t, share.RelayParent, relayParent)

return true
}

informSeconded := func(msg any) bool {
// informed collator protocol that we have seconded the candidate
_, ok := msg.(collatorprotocolmessages.Seconded)
return ok
}

overseer.ExpectActions(validate, storeAvailableData, distribute, informSeconded)

// mocked for candidate1
mockRuntime.EXPECT().ParachainHostValidationCodeByHash(gomock.AssignableToTypeOf(common.Hash{})).
Return(&validationCode1, nil)

overseer.ReceiveMessage(backing.SecondMessage{
RelayParent: relayParent,
CandidateReceipt: candidate1.ToPlain(),
PersistedValidationData: pvd,
PoV: pov,
})

time.Sleep(1 * time.Second)

validationCode2 := parachaintypes.ValidationCode{4, 5, 6}

candidate2 := newCommittedCandidate(t,
paraID,
headData,
povHash,
relayParent,
makeErasureRoot(t, numOfValidators, pov, pvd),
pvdHash,
validationCode2,
)

// Validate the candidate, but the candidate is rejected because the leaf is already occupied.
// should not expect `StatementDistributionMessageShare` and `collator protocol messages.Seconded` overseer messages.
overseer.ExpectActions(validate, storeAvailableData)

// mocked for candidate2
mockRuntime.EXPECT().ParachainHostValidationCodeByHash(gomock.AssignableToTypeOf(common.Hash{})).
Return(&validationCode2, nil)

// Try to second candidate with the same relay parent again.
overseer.ReceiveMessage(backing.SecondMessage{
RelayParent: relayParent,
CandidateReceipt: candidate2.ToPlain(),
PersistedValidationData: pvd,
PoV: pov,
})

time.Sleep(1 * time.Second)
}
Loading

0 comments on commit baea974

Please sign in to comment.