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