github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/votecollector/staking_vote_processor_test.go (about) 1 package votecollector 2 3 import ( 4 "errors" 5 "sync" 6 "testing" 7 8 "github.com/onflow/crypto" 9 "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/require" 11 "github.com/stretchr/testify/suite" 12 "go.uber.org/atomic" 13 14 "github.com/onflow/flow-go/consensus/hotstuff" 15 "github.com/onflow/flow-go/consensus/hotstuff/committees" 16 "github.com/onflow/flow-go/consensus/hotstuff/helper" 17 mockhotstuff "github.com/onflow/flow-go/consensus/hotstuff/mocks" 18 "github.com/onflow/flow-go/consensus/hotstuff/model" 19 hotstuffvalidator "github.com/onflow/flow-go/consensus/hotstuff/validator" 20 "github.com/onflow/flow-go/consensus/hotstuff/verification" 21 "github.com/onflow/flow-go/model/flow" 22 "github.com/onflow/flow-go/module/local" 23 modulemock "github.com/onflow/flow-go/module/mock" 24 "github.com/onflow/flow-go/module/signature" 25 "github.com/onflow/flow-go/utils/unittest" 26 ) 27 28 func TestStakingVoteProcessor(t *testing.T) { 29 suite.Run(t, new(StakingVoteProcessorTestSuite)) 30 } 31 32 // StakingVoteProcessorTestSuite is a test suite that holds mocked state for isolated testing of StakingVoteProcessor. 33 type StakingVoteProcessorTestSuite struct { 34 VoteProcessorTestSuiteBase 35 36 processor *StakingVoteProcessor 37 allParticipants flow.IdentityList 38 } 39 40 func (s *StakingVoteProcessorTestSuite) SetupTest() { 41 s.VoteProcessorTestSuiteBase.SetupTest() 42 s.allParticipants = unittest.IdentityListFixture(14) 43 s.processor = &StakingVoteProcessor{ 44 log: unittest.Logger(), 45 block: s.proposal.Block, 46 stakingSigAggtor: s.stakingAggregator, 47 onQCCreated: s.onQCCreated, 48 minRequiredWeight: s.minRequiredWeight, 49 done: *atomic.NewBool(false), 50 allParticipants: s.allParticipants, 51 } 52 } 53 54 // TestInitialState tests that Block() and Status() return correct values after calling constructor 55 func (s *StakingVoteProcessorTestSuite) TestInitialState() { 56 require.Equal(s.T(), s.proposal.Block, s.processor.Block()) 57 require.Equal(s.T(), hotstuff.VoteCollectorStatusVerifying, s.processor.Status()) 58 } 59 60 // TestProcess_VoteNotForProposal tests that vote should pass to validation only if it has correct 61 // view and block ID matching proposal that is locked in StakingVoteProcessor 62 func (s *StakingVoteProcessorTestSuite) TestProcess_VoteNotForProposal() { 63 err := s.processor.Process(unittest.VoteFixture(unittest.WithVoteView(s.proposal.Block.View))) 64 require.ErrorAs(s.T(), err, &VoteForIncompatibleBlockError) 65 require.False(s.T(), model.IsInvalidVoteError(err)) 66 67 err = s.processor.Process(unittest.VoteFixture(unittest.WithVoteBlockID(s.proposal.Block.BlockID))) 68 require.ErrorAs(s.T(), err, &VoteForIncompatibleViewError) 69 require.False(s.T(), model.IsInvalidVoteError(err)) 70 71 s.stakingAggregator.AssertNotCalled(s.T(), "Verify") 72 } 73 74 // TestProcess_InvalidSignature tests that StakingVoteProcessor doesn't collect signatures for votes with invalid signature. 75 // Checks are made for cases where both staking and threshold signatures were submitted. 76 func (s *StakingVoteProcessorTestSuite) TestProcess_InvalidSignature() { 77 exception := errors.New("unexpected-exception") 78 79 // sentinel error from `InvalidSignerError` should be wrapped as `InvalidVoteError` 80 voteA := unittest.VoteForBlockFixture(s.proposal.Block, unittest.VoteWithStakingSig()) 81 s.stakingAggregator.On("Verify", voteA.SignerID, mock.Anything).Return(model.NewInvalidSignerErrorf("")).Once() 82 err := s.processor.Process(voteA) 83 require.Error(s.T(), err) 84 require.True(s.T(), model.IsInvalidVoteError(err)) 85 require.True(s.T(), model.IsInvalidSignerError(err)) 86 87 // sentinel error from `ErrInvalidSignature` should be wrapped as `InvalidVoteError` 88 voteB := unittest.VoteForBlockFixture(s.proposal.Block, unittest.VoteWithStakingSig()) 89 s.stakingAggregator.On("Verify", voteB.SignerID, mock.Anything).Return(model.ErrInvalidSignature).Once() 90 err = s.processor.Process(voteB) 91 require.Error(s.T(), err) 92 require.True(s.T(), model.IsInvalidVoteError(err)) 93 require.ErrorAs(s.T(), err, &model.ErrInvalidSignature) 94 95 // unexpected errors from `Verify` should be propagated, but should _not_ be wrapped as `InvalidVoteError` 96 voteC := unittest.VoteForBlockFixture(s.proposal.Block, unittest.VoteWithStakingSig()) 97 s.stakingAggregator.On("Verify", voteC.SignerID, mock.Anything).Return(exception) 98 err = s.processor.Process(voteC) 99 require.ErrorIs(s.T(), err, exception) // unexpected errors from verifying the vote signature should be propagated 100 require.False(s.T(), model.IsInvalidVoteError(err)) // but not interpreted as an invalid vote 101 102 s.stakingAggregator.AssertNotCalled(s.T(), "TrustedAdd") 103 } 104 105 // TestProcess_TrustedAdd_Exception tests that unexpected exceptions returned by 106 // WeightedSignatureAggregator.TrustedAdd(..) are _not_ interpreted as invalid votes 107 func (s *StakingVoteProcessorTestSuite) TestProcess_TrustedAdd_Exception() { 108 exception := errors.New("unexpected-exception") 109 stakingVote := unittest.VoteForBlockFixture(s.proposal.Block, unittest.VoteWithStakingSig()) 110 *s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{} 111 s.stakingAggregator.On("Verify", stakingVote.SignerID, mock.Anything).Return(nil).Once() 112 s.stakingAggregator.On("TrustedAdd", stakingVote.SignerID, mock.Anything).Return(uint64(0), exception).Once() 113 err := s.processor.Process(stakingVote) 114 require.ErrorIs(s.T(), err, exception) 115 require.False(s.T(), model.IsInvalidVoteError(err)) 116 s.stakingAggregator.AssertExpectations(s.T()) 117 } 118 119 // TestProcess_BuildQCError tests error path during process of building QC. 120 // Building QC is a one time operation, we need to make sure that failing in one of the steps leads to exception. 121 func (s *StakingVoteProcessorTestSuite) TestProcess_BuildQCError() { 122 // In this test we will mock all dependencies for happy path, and replace some branches with unhappy path 123 // to simulate errors along the branches. 124 vote := unittest.VoteForBlockFixture(s.proposal.Block) 125 126 // in this test case we aren't able to aggregate staking signature 127 exception := errors.New("staking-aggregate-exception") 128 stakingSigAggregator := &mockhotstuff.WeightedSignatureAggregator{} 129 stakingSigAggregator.On("Verify", mock.Anything, mock.Anything).Return(nil).Once() 130 stakingSigAggregator.On("TrustedAdd", mock.Anything, mock.Anything).Return(s.minRequiredWeight, nil).Once() 131 stakingSigAggregator.On("Aggregate").Return(nil, nil, exception).Once() 132 133 s.processor.stakingSigAggtor = stakingSigAggregator 134 err := s.processor.Process(vote) 135 require.ErrorIs(s.T(), err, exception) 136 stakingSigAggregator.AssertExpectations(s.T()) 137 } 138 139 // TestProcess_NotEnoughStakingWeight tests a scenario where we first don't have enough weight, 140 // then we iteratively increase it to the point where we have enough staking weight. No QC should be created. 141 func (s *StakingVoteProcessorTestSuite) TestProcess_NotEnoughStakingWeight() { 142 for i := s.sigWeight; i < s.minRequiredWeight; i += s.sigWeight { 143 vote := unittest.VoteForBlockFixture(s.proposal.Block) 144 s.stakingAggregator.On("Verify", vote.SignerID, crypto.Signature(vote.SigData)).Return(nil).Once() 145 err := s.processor.Process(vote) 146 require.NoError(s.T(), err) 147 } 148 require.False(s.T(), s.processor.done.Load()) 149 s.onQCCreatedState.AssertNotCalled(s.T(), "onQCCreated") 150 s.stakingAggregator.AssertExpectations(s.T()) 151 } 152 153 // TestProcess_CreatingQC tests a scenario when we have collected enough staking weight 154 // and proceed to build QC. Created QC has to have all signatures and identities aggregated by 155 // aggregator. 156 func (s *StakingVoteProcessorTestSuite) TestProcess_CreatingQC() { 157 // prepare test setup: 13 votes with staking sigs 158 stakingSigners := s.allParticipants[:14].NodeIDs() 159 signerIndices, err := signature.EncodeSignersToIndices(stakingSigners, stakingSigners) 160 require.NoError(s.T(), err) 161 162 // setup aggregator 163 *s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{} 164 expectedSigData := unittest.RandomBytes(128) 165 s.stakingAggregator.On("Aggregate").Return(stakingSigners, expectedSigData, nil).Once() 166 167 // expected QC 168 s.onQCCreatedState.On("onQCCreated", mock.Anything).Run(func(args mock.Arguments) { 169 qc := args.Get(0).(*flow.QuorumCertificate) 170 // ensure that QC contains correct field 171 expectedQC := &flow.QuorumCertificate{ 172 View: s.proposal.Block.View, 173 BlockID: s.proposal.Block.BlockID, 174 SignerIndices: signerIndices, 175 SigData: expectedSigData, 176 } 177 require.Equal(s.T(), expectedQC, qc) 178 }).Return(nil).Once() 179 180 // add votes 181 for _, signer := range stakingSigners { 182 vote := unittest.VoteForBlockFixture(s.proposal.Block) 183 vote.SignerID = signer 184 expectedSig := crypto.Signature(vote.SigData) 185 s.stakingAggregator.On("Verify", vote.SignerID, expectedSig).Return(nil).Once() 186 s.stakingAggregator.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) { 187 s.stakingTotalWeight += s.sigWeight 188 }).Return(s.stakingTotalWeight, nil).Once() 189 err := s.processor.Process(vote) 190 require.NoError(s.T(), err) 191 } 192 193 require.True(s.T(), s.processor.done.Load()) 194 s.onQCCreatedState.AssertExpectations(s.T()) 195 s.stakingAggregator.AssertExpectations(s.T()) 196 197 // processing extra votes shouldn't result in creating new QCs 198 vote := unittest.VoteForBlockFixture(s.proposal.Block) 199 err = s.processor.Process(vote) 200 require.NoError(s.T(), err) 201 202 s.onQCCreatedState.AssertExpectations(s.T()) 203 } 204 205 // TestProcess_ConcurrentCreatingQC tests a scenario where multiple goroutines process vote at same time, 206 // we expect only one QC created in this scenario. 207 func (s *StakingVoteProcessorTestSuite) TestProcess_ConcurrentCreatingQC() { 208 stakingSigners := s.allParticipants[:10].NodeIDs() 209 mockAggregator := func(aggregator *mockhotstuff.WeightedSignatureAggregator) { 210 aggregator.On("Verify", mock.Anything, mock.Anything).Return(nil) 211 aggregator.On("TrustedAdd", mock.Anything, mock.Anything).Return(s.minRequiredWeight, nil) 212 aggregator.On("TotalWeight").Return(s.minRequiredWeight) 213 aggregator.On("Aggregate").Return(stakingSigners, unittest.RandomBytes(128), nil) 214 } 215 216 // mock aggregators, so we have enough weight and shares for creating QC 217 *s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{} 218 mockAggregator(s.stakingAggregator) 219 220 // at this point sending any vote should result in creating QC. 221 s.onQCCreatedState.On("onQCCreated", mock.Anything).Return(nil).Once() 222 223 var startupWg, shutdownWg sync.WaitGroup 224 225 vote := unittest.VoteForBlockFixture(s.proposal.Block) 226 startupWg.Add(1) 227 // prepare goroutines, so they are ready to submit a vote at roughly same time 228 for i := 0; i < 5; i++ { 229 shutdownWg.Add(1) 230 go func() { 231 defer shutdownWg.Done() 232 startupWg.Wait() 233 err := s.processor.Process(vote) 234 require.NoError(s.T(), err) 235 }() 236 } 237 238 startupWg.Done() 239 240 // wait for all routines to finish 241 shutdownWg.Wait() 242 243 s.onQCCreatedState.AssertNumberOfCalls(s.T(), "onQCCreated", 1) 244 } 245 246 // TestStakingVoteProcessorV2_BuildVerifyQC tests a complete path from creating votes to collecting votes and then 247 // building & verifying QC. 248 // We start with leader proposing a block, then new leader collects votes and builds a QC. 249 // Need to verify that QC that was produced is valid and can be embedded in new proposal. 250 func TestStakingVoteProcessorV2_BuildVerifyQC(t *testing.T) { 251 epochCounter := uint64(3) 252 epochLookup := &modulemock.EpochLookup{} 253 view := uint64(20) 254 epochLookup.On("EpochForViewWithFallback", view).Return(epochCounter, nil) 255 256 // signers hold objects that are created with private key and can sign votes and proposals 257 signers := make(map[flow.Identifier]*verification.StakingSigner) 258 // prepare staking signers, each signer has its own private/public key pair 259 stakingSigners := unittest.IdentityListFixture(7, func(identity *flow.Identity) { 260 stakingPriv := unittest.StakingPrivKeyFixture() 261 identity.StakingPubKey = stakingPriv.PublicKey() 262 263 me, err := local.New(identity.IdentitySkeleton, stakingPriv) 264 require.NoError(t, err) 265 266 signers[identity.NodeID] = verification.NewStakingSigner(me) 267 }).Sort(flow.Canonical[flow.Identity]) 268 269 leader := stakingSigners[0] 270 block := helper.MakeBlock(helper.WithBlockView(view), helper.WithBlockProposer(leader.NodeID)) 271 272 committee := &mockhotstuff.DynamicCommittee{} 273 committee.On("IdentitiesByEpoch", block.View).Return(stakingSigners.ToSkeleton(), nil) 274 committee.On("IdentitiesByBlock", block.BlockID).Return(stakingSigners, nil) 275 committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(stakingSigners.ToSkeleton().TotalWeight()), nil) 276 277 votes := make([]*model.Vote, 0, len(stakingSigners)) 278 279 // first staking signer will be leader collecting votes for proposal 280 // prepare votes for every member of committee except leader 281 for _, signer := range stakingSigners[1:] { 282 vote, err := signers[signer.NodeID].CreateVote(block) 283 require.NoError(t, err) 284 votes = append(votes, vote) 285 } 286 287 // create and sign proposal 288 proposal, err := signers[leader.NodeID].CreateProposal(block) 289 require.NoError(t, err) 290 291 qcCreated := false 292 onQCCreated := func(qc *flow.QuorumCertificate) { 293 // create verifier that will do crypto checks of created QC 294 verifier := verification.NewStakingVerifier() 295 // create validator which will do compliance and crypto checked of created QC 296 validator := hotstuffvalidator.New(committee, verifier) 297 // check if QC is valid against parent 298 err := validator.ValidateQC(qc) 299 require.NoError(t, err) 300 301 qcCreated = true 302 } 303 304 voteProcessorFactory := NewStakingVoteProcessorFactory(committee, onQCCreated) 305 voteProcessor, err := voteProcessorFactory.Create(unittest.Logger(), proposal) 306 require.NoError(t, err) 307 308 // process votes by new leader, this will result in producing new QC 309 for _, vote := range votes { 310 err := voteProcessor.Process(vote) 311 require.NoError(t, err) 312 } 313 314 require.True(t, qcCreated) 315 }