github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/votecollector/combined_vote_processor_v2_test.go (about) 1 package votecollector 2 3 import ( 4 "errors" 5 "math/rand" 6 "sync" 7 "testing" 8 9 "github.com/onflow/crypto" 10 "github.com/stretchr/testify/mock" 11 "github.com/stretchr/testify/require" 12 "github.com/stretchr/testify/suite" 13 "go.uber.org/atomic" 14 "pgregory.net/rapid" 15 16 bootstrapDKG "github.com/onflow/flow-go/cmd/bootstrap/dkg" 17 "github.com/onflow/flow-go/consensus/hotstuff" 18 "github.com/onflow/flow-go/consensus/hotstuff/committees" 19 "github.com/onflow/flow-go/consensus/hotstuff/helper" 20 mockhotstuff "github.com/onflow/flow-go/consensus/hotstuff/mocks" 21 "github.com/onflow/flow-go/consensus/hotstuff/model" 22 "github.com/onflow/flow-go/consensus/hotstuff/signature" 23 hsig "github.com/onflow/flow-go/consensus/hotstuff/signature" 24 hotstuffvalidator "github.com/onflow/flow-go/consensus/hotstuff/validator" 25 "github.com/onflow/flow-go/consensus/hotstuff/verification" 26 "github.com/onflow/flow-go/model/encodable" 27 "github.com/onflow/flow-go/model/flow" 28 "github.com/onflow/flow-go/module/local" 29 modulemock "github.com/onflow/flow-go/module/mock" 30 msig "github.com/onflow/flow-go/module/signature" 31 "github.com/onflow/flow-go/state/protocol/inmem" 32 storagemock "github.com/onflow/flow-go/storage/mock" 33 "github.com/onflow/flow-go/utils/unittest" 34 ) 35 36 func TestCombinedVoteProcessorV2(t *testing.T) { 37 suite.Run(t, new(CombinedVoteProcessorV2TestSuite)) 38 } 39 40 // CombinedVoteProcessorV2TestSuite is a test suite that holds mocked state for isolated testing of CombinedVoteProcessorV2. 41 type CombinedVoteProcessorV2TestSuite struct { 42 VoteProcessorTestSuiteBase 43 44 rbSharesTotal uint64 45 46 packer *mockhotstuff.Packer 47 48 reconstructor *mockhotstuff.RandomBeaconReconstructor 49 50 minRequiredShares uint64 51 processor *CombinedVoteProcessorV2 52 } 53 54 func (s *CombinedVoteProcessorV2TestSuite) SetupTest() { 55 s.VoteProcessorTestSuiteBase.SetupTest() 56 57 s.reconstructor = &mockhotstuff.RandomBeaconReconstructor{} 58 s.packer = &mockhotstuff.Packer{} 59 s.proposal = helper.MakeProposal() 60 61 s.minRequiredShares = 9 // we require 9 RB shares to reconstruct signature 62 s.rbSharesTotal = 0 63 64 // setup rb reconstructor 65 s.reconstructor.On("TrustedAdd", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 66 s.rbSharesTotal++ 67 }).Return(func(signerID flow.Identifier, sig crypto.Signature) bool { 68 return s.rbSharesTotal >= s.minRequiredShares 69 }, func(signerID flow.Identifier, sig crypto.Signature) error { 70 return nil 71 }).Maybe() 72 s.reconstructor.On("EnoughShares").Return(func() bool { 73 return s.rbSharesTotal >= s.minRequiredShares 74 }).Maybe() 75 76 s.processor = &CombinedVoteProcessorV2{ 77 log: unittest.Logger(), 78 block: s.proposal.Block, 79 stakingSigAggtor: s.stakingAggregator, 80 rbRector: s.reconstructor, 81 onQCCreated: s.onQCCreated, 82 packer: s.packer, 83 minRequiredWeight: s.minRequiredWeight, 84 done: *atomic.NewBool(false), 85 } 86 } 87 88 // TestInitialState tests that Block() and Status() return correct values after calling constructor 89 func (s *CombinedVoteProcessorV2TestSuite) TestInitialState() { 90 require.Equal(s.T(), s.proposal.Block, s.processor.Block()) 91 require.Equal(s.T(), hotstuff.VoteCollectorStatusVerifying, s.processor.Status()) 92 } 93 94 // TestProcess_VoteNotForProposal tests that CombinedVoteProcessorV2 accepts only votes for the block it was initialized with 95 // according to interface specification of `VoteProcessor`, we expect dedicated sentinel errors for votes 96 // for different views (`VoteForIncompatibleViewError`) _or_ block (`VoteForIncompatibleBlockError`). 97 func (s *CombinedVoteProcessorV2TestSuite) TestProcess_VoteNotForProposal() { 98 err := s.processor.Process(unittest.VoteFixture(unittest.WithVoteView(s.proposal.Block.View))) 99 require.ErrorAs(s.T(), err, &VoteForIncompatibleBlockError) 100 require.False(s.T(), model.IsInvalidVoteError(err)) 101 102 err = s.processor.Process(unittest.VoteFixture(unittest.WithVoteBlockID(s.proposal.Block.BlockID))) 103 require.ErrorAs(s.T(), err, &VoteForIncompatibleViewError) 104 require.False(s.T(), model.IsInvalidVoteError(err)) 105 106 s.stakingAggregator.AssertNotCalled(s.T(), "Verify") 107 s.reconstructor.AssertNotCalled(s.T(), "Verify") 108 } 109 110 // TestProcess_InvalidSignatureFormat ensures that we process signatures only with valid format. 111 // If we have received vote with signature in invalid format we should return with sentinel error 112 func (s *CombinedVoteProcessorV2TestSuite) TestProcess_InvalidSignatureFormat() { 113 114 // valid length is SigLen or 2*SigLen 115 generator := rapid.IntRange(0, 128).Filter(func(value int) bool { 116 return value != msig.SigLen && value != 2*msig.SigLen 117 }) 118 rapid.Check(s.T(), func(t *rapid.T) { 119 // create a signature with invalid length 120 vote := unittest.VoteForBlockFixture(s.proposal.Block, func(vote *model.Vote) { 121 vote.SigData = unittest.RandomBytes(generator.Draw(t, "sig-size")) 122 }) 123 err := s.processor.Process(vote) 124 require.Error(s.T(), err) 125 require.True(s.T(), model.IsInvalidVoteError(err), err) 126 require.True(s.T(), errors.Is(err, msig.ErrInvalidSignatureFormat), err) 127 }) 128 } 129 130 // TestProcess_InvalidSignature tests that CombinedVoteProcessorV2 rejects invalid votes for the following scenarios: 131 // 1. vote where `SignerID` is not a valid consensus participant; 132 // we test correct handling of votes that only a staking signature as well as votes with staking+beacon signatures 133 // 2. vote from a valid consensus participant 134 // case 2a: vote contains only a staking signatures that is invalid 135 // case 2b: vote contains _invalid staking sig_, and valid random beacon sig 136 // case 2c: vote contains valid staking sig, but _invalid beacon sig_ 137 // 138 // In all cases, the CombinedVoteProcessor should interpret these failure cases as invalid votes 139 // and return an InvalidVoteError. 140 func (s *CombinedVoteProcessorV2TestSuite) TestProcess_InvalidSignature() { 141 // Scenario 1) vote where `SignerID` is not a valid consensus participant; 142 // sentinel error `InvalidSignerError` from WeightedSignatureAggregator should be wrapped as `InvalidVoteError` 143 s.Run("vote with invalid signerID", func() { 144 // vote with only a staking signature 145 stakingOnlyVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig()) 146 s.stakingAggregator.On("Verify", stakingOnlyVote.SignerID, mock.Anything).Return(model.NewInvalidSignerErrorf("")).Once() 147 err := s.processor.Process(stakingOnlyVote) 148 require.Error(s.T(), err) 149 require.True(s.T(), model.IsInvalidVoteError(err)) 150 require.True(s.T(), model.IsInvalidSignerError(err)) 151 152 // vote with staking+beacon signatures 153 doubleSigVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig()) 154 s.stakingAggregator.On("Verify", doubleSigVote.SignerID, mock.Anything).Return(model.NewInvalidSignerErrorf("")).Maybe() 155 s.reconstructor.On("Verify", doubleSigVote.SignerID, mock.Anything).Return(model.NewInvalidSignerErrorf("")).Maybe() 156 err = s.processor.Process(doubleSigVote) 157 require.Error(s.T(), err) 158 require.True(s.T(), model.IsInvalidVoteError(err)) 159 require.True(s.T(), model.IsInvalidSignerError(err)) 160 161 s.stakingAggregator.AssertNotCalled(s.T(), "TrustedAdd") 162 s.reconstructor.AssertNotCalled(s.T(), "TrustedAdd") 163 }) 164 165 // Scenario 2) vote from a valid consensus participant but included sig(s) are cryptographically invalid; 166 // sentinel error `ErrInvalidSignature` from WeightedSignatureAggregator should be wrapped as `InvalidVoteError` 167 s.Run("vote from valid participant but with invalid signature(s)", func() { 168 // case 2a: vote contains only a staking signatures that is invalid 169 voteA := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig()) 170 s.stakingAggregator.On("Verify", voteA.SignerID, mock.Anything).Return(model.ErrInvalidSignature).Once() 171 err := s.processor.Process(voteA) 172 require.Error(s.T(), err) 173 require.True(s.T(), model.IsInvalidVoteError(err)) 174 require.ErrorAs(s.T(), err, &model.ErrInvalidSignature) 175 176 // case 2b: vote contains _invalid staking sig_, and valid random beacon sig 177 voteB := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig()) 178 s.stakingAggregator.On("Verify", voteB.SignerID, mock.Anything).Return(model.ErrInvalidSignature).Once() 179 s.reconstructor.On("Verify", voteB.SignerID, mock.Anything).Return(nil).Maybe() 180 err = s.processor.Process(voteB) 181 require.Error(s.T(), err) 182 require.True(s.T(), model.IsInvalidVoteError(err)) 183 require.ErrorAs(s.T(), err, &model.ErrInvalidSignature) 184 185 // case 2c: vote contains valid staking sig, but _invalid beacon sig_ 186 voteC := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig()) 187 s.stakingAggregator.On("Verify", voteC.SignerID, mock.Anything).Return(nil).Maybe() 188 s.reconstructor.On("Verify", voteC.SignerID, mock.Anything).Return(model.ErrInvalidSignature).Once() 189 err = s.processor.Process(voteC) 190 require.Error(s.T(), err) 191 require.True(s.T(), model.IsInvalidVoteError(err)) 192 require.ErrorAs(s.T(), err, &model.ErrInvalidSignature) 193 194 s.stakingAggregator.AssertNotCalled(s.T(), "TrustedAdd") 195 s.reconstructor.AssertNotCalled(s.T(), "TrustedAdd") 196 }) 197 } 198 199 // TestProcess_TrustedAdd_Exception tests that unexpected exceptions returned by 200 // WeightedSignatureAggregator.Verify(..) are _not_ interpreted as invalid votes 201 func (s *CombinedVoteProcessorV2TestSuite) TestProcess_Verify_Exception() { 202 exception := errors.New("unexpected-exception") 203 204 s.Run("vote with staking-sig only", func() { 205 stakingVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig()) 206 s.stakingAggregator.On("Verify", stakingVote.SignerID, mock.Anything).Return(exception).Once() 207 err := s.processor.Process(stakingVote) 208 require.ErrorIs(s.T(), err, exception) 209 require.False(s.T(), model.IsInvalidVoteError(err)) 210 }) 211 212 s.Run("vote with staking+beacon sig", func() { 213 // verifying staking sig leads to unexpected exception 214 doubleSigVoteA := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig()) 215 s.stakingAggregator.On("Verify", doubleSigVoteA.SignerID, mock.Anything).Return(exception).Once() 216 s.reconstructor.On("Verify", doubleSigVoteA.SignerID, mock.Anything).Return(nil).Maybe() 217 err := s.processor.Process(doubleSigVoteA) 218 require.ErrorIs(s.T(), err, exception) 219 require.False(s.T(), model.IsInvalidVoteError(err)) 220 221 // verifying beacon sig leads to unexpected exception 222 doubleSigVoteB := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig()) 223 s.stakingAggregator.On("Verify", doubleSigVoteB.SignerID, mock.Anything).Return(nil).Maybe() 224 s.reconstructor.On("Verify", doubleSigVoteB.SignerID, mock.Anything).Return(exception).Once() 225 err = s.processor.Process(doubleSigVoteB) 226 require.Error(s.T(), err) 227 require.ErrorIs(s.T(), err, exception) 228 require.False(s.T(), model.IsInvalidVoteError(err)) 229 }) 230 } 231 232 // TestProcess_TrustedAdd_Exception tests that unexpected exceptions returned by 233 // WeightedSignatureAggregator.Verify(..) are propagated, but _not_ interpreted as invalid votes 234 func (s *CombinedVoteProcessorV2TestSuite) TestProcess_TrustedAdd_Exception() { 235 exception := errors.New("unexpected-exception") 236 s.Run("staking-sig", func() { 237 stakingVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig()) 238 *s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{} 239 s.stakingAggregator.On("Verify", stakingVote.SignerID, mock.Anything).Return(nil).Once() 240 s.stakingAggregator.On("TrustedAdd", stakingVote.SignerID, mock.Anything).Return(uint64(0), exception).Once() 241 err := s.processor.Process(stakingVote) 242 require.ErrorIs(s.T(), err, exception) 243 require.False(s.T(), model.IsInvalidVoteError(err)) 244 }) 245 s.Run("beacon-sig", func() { 246 doubleSigVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig()) 247 *s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{} 248 *s.reconstructor = mockhotstuff.RandomBeaconReconstructor{} 249 250 // first we will collect staking sig 251 s.stakingAggregator.On("Verify", doubleSigVote.SignerID, mock.Anything).Return(nil).Once() 252 s.stakingAggregator.On("TrustedAdd", doubleSigVote.SignerID, mock.Anything).Return(uint64(0), nil).Once() 253 254 // then beacon sig 255 s.reconstructor.On("Verify", doubleSigVote.SignerID, mock.Anything).Return(nil).Once() 256 s.reconstructor.On("TrustedAdd", doubleSigVote.SignerID, mock.Anything).Return(false, exception).Once() 257 258 err := s.processor.Process(doubleSigVote) 259 require.ErrorIs(s.T(), err, exception) 260 require.False(s.T(), model.IsInvalidVoteError(err)) 261 }) 262 } 263 264 // TestProcess_BuildQCError tests all error paths during process of building QC. 265 // Building QC is a one time operation, we need to make sure that failing in one of the steps leads to exception. 266 // Since it's a one time operation we need a complicated test to test all conditions. 267 func (s *CombinedVoteProcessorV2TestSuite) TestProcess_BuildQCError() { 268 mockAggregator := func(aggregator *mockhotstuff.WeightedSignatureAggregator) { 269 aggregator.On("Verify", mock.Anything, mock.Anything).Return(nil) 270 aggregator.On("TrustedAdd", mock.Anything, mock.Anything).Return(s.minRequiredWeight, nil) 271 aggregator.On("TotalWeight").Return(s.minRequiredWeight) 272 } 273 274 stakingSigAggregator := &mockhotstuff.WeightedSignatureAggregator{} 275 reconstructor := &mockhotstuff.RandomBeaconReconstructor{} 276 packer := &mockhotstuff.Packer{} 277 278 identities := unittest.IdentifierListFixture(5) 279 280 // In this test we will mock all dependencies for happy path, and replace some branches with unhappy path 281 // to simulate errors along the branches. 282 283 mockAggregator(stakingSigAggregator) 284 stakingSigAggregator.On("Aggregate").Return(identities, unittest.RandomBytes(128), nil) 285 286 reconstructor.On("EnoughShares").Return(true) 287 reconstructor.On("Reconstruct").Return(unittest.SignatureFixture(), nil) 288 289 packer.On("Pack", mock.Anything, mock.Anything).Return(unittest.RandomBytes(100), unittest.RandomBytes(128), nil) 290 291 // Helper factory function to create processors. We need new processor for every test case 292 // because QC creation is one time operation and is triggered as soon as we have collected enough weight and shares. 293 createProcessor := func(stakingAggregator *mockhotstuff.WeightedSignatureAggregator, 294 rbReconstructor *mockhotstuff.RandomBeaconReconstructor, 295 packer *mockhotstuff.Packer) *CombinedVoteProcessorV2 { 296 return &CombinedVoteProcessorV2{ 297 log: unittest.Logger(), 298 block: s.proposal.Block, 299 stakingSigAggtor: stakingAggregator, 300 rbRector: rbReconstructor, 301 onQCCreated: s.onQCCreated, 302 packer: packer, 303 minRequiredWeight: s.minRequiredWeight, 304 done: *atomic.NewBool(false), 305 } 306 } 307 308 vote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig()) 309 310 // in this test case we aren't able to aggregate staking signature 311 s.Run("staking-sig-aggregate", func() { 312 exception := errors.New("staking-aggregate-exception") 313 stakingSigAggregator := &mockhotstuff.WeightedSignatureAggregator{} 314 mockAggregator(stakingSigAggregator) 315 stakingSigAggregator.On("Aggregate").Return(nil, nil, exception) 316 processor := createProcessor(stakingSigAggregator, reconstructor, packer) 317 err := processor.Process(vote) 318 require.ErrorIs(s.T(), err, exception) 319 require.False(s.T(), model.IsInvalidVoteError(err)) 320 }) 321 // in this test case we aren't able to reconstruct signature 322 s.Run("reconstruct", func() { 323 exception := errors.New("reconstruct-exception") 324 reconstructor := &mockhotstuff.RandomBeaconReconstructor{} 325 reconstructor.On("EnoughShares").Return(true) 326 reconstructor.On("Reconstruct").Return(nil, exception) 327 processor := createProcessor(stakingSigAggregator, reconstructor, packer) 328 err := processor.Process(vote) 329 require.ErrorIs(s.T(), err, exception) 330 require.False(s.T(), model.IsInvalidVoteError(err)) 331 }) 332 // in this test case we aren't able to pack signatures 333 s.Run("pack", func() { 334 exception := errors.New("pack-qc-exception") 335 packer := &mockhotstuff.Packer{} 336 packer.On("Pack", mock.Anything, mock.Anything).Return(nil, nil, exception) 337 processor := createProcessor(stakingSigAggregator, reconstructor, packer) 338 err := processor.Process(vote) 339 require.ErrorIs(s.T(), err, exception) 340 require.False(s.T(), model.IsInvalidVoteError(err)) 341 }) 342 } 343 344 // TestProcess_EnoughWeightNotEnoughShares tests a scenario where we first don't have enough weight, 345 // then we iteratively increase it to the point where we have enough staking weight. No QC should be created 346 // in this scenario since there is not enough random beacon shares. 347 func (s *CombinedVoteProcessorV2TestSuite) TestProcess_EnoughWeightNotEnoughShares() { 348 for i := uint64(0); i < s.minRequiredWeight; i += s.sigWeight { 349 vote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig()) 350 s.stakingAggregator.On("Verify", vote.SignerID, mock.Anything).Return(nil) 351 err := s.processor.Process(vote) 352 require.NoError(s.T(), err) 353 } 354 355 require.False(s.T(), s.processor.done.Load()) 356 s.reconstructor.AssertCalled(s.T(), "EnoughShares") 357 s.onQCCreatedState.AssertNotCalled(s.T(), "onQCCreated") 358 } 359 360 // TestProcess_EnoughSharesNotEnoughWeight tests a scenario where we are collecting votes with staking and beacon sigs 361 // to the point where we have enough shares to reconstruct RB signature. No QC should be created 362 // in this scenario since there is not enough staking weight. 363 func (s *CombinedVoteProcessorV2TestSuite) TestProcess_EnoughSharesNotEnoughWeight() { 364 // change sig weight to be really low, so we don't reach min staking weight while collecting 365 // beacon signatures 366 s.sigWeight = 10 367 for i := uint64(0); i < s.minRequiredShares; i++ { 368 vote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig()) 369 s.stakingAggregator.On("Verify", vote.SignerID, mock.Anything).Return(nil).Once() 370 s.reconstructor.On("Verify", vote.SignerID, mock.Anything).Return(nil).Once() 371 err := s.processor.Process(vote) 372 require.NoError(s.T(), err) 373 } 374 375 require.False(s.T(), s.processor.done.Load()) 376 s.reconstructor.AssertNotCalled(s.T(), "EnoughShares") 377 s.onQCCreatedState.AssertNotCalled(s.T(), "onQCCreated") 378 // verify if we indeed have enough shares 379 require.True(s.T(), s.reconstructor.EnoughShares()) 380 } 381 382 // TestProcess_ConcurrentCreatingQC tests a scenario where multiple goroutines process vote at same time, 383 // we expect only one QC created in this scenario. 384 func (s *CombinedVoteProcessorV2TestSuite) TestProcess_ConcurrentCreatingQC() { 385 stakingSigners := unittest.IdentifierListFixture(10) 386 mockAggregator := func(aggregator *mockhotstuff.WeightedSignatureAggregator) { 387 aggregator.On("Verify", mock.Anything, mock.Anything).Return(nil) 388 aggregator.On("TrustedAdd", mock.Anything, mock.Anything).Return(s.minRequiredWeight, nil) 389 aggregator.On("TotalWeight").Return(s.minRequiredWeight) 390 aggregator.On("Aggregate").Return(stakingSigners, unittest.RandomBytes(128), nil) 391 } 392 393 // mock aggregators, so we have enough weight and shares for creating QC 394 *s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{} 395 mockAggregator(s.stakingAggregator) 396 *s.reconstructor = mockhotstuff.RandomBeaconReconstructor{} 397 s.reconstructor.On("Verify", mock.Anything, mock.Anything).Return(nil) 398 s.reconstructor.On("Reconstruct").Return(unittest.SignatureFixture(), nil) 399 s.reconstructor.On("EnoughShares").Return(true) 400 401 // at this point sending any vote should result in creating QC. 402 s.packer.On("Pack", s.proposal.Block.View, mock.Anything).Return(unittest.RandomBytes(100), unittest.RandomBytes(128), nil) 403 s.onQCCreatedState.On("onQCCreated", mock.Anything).Return(nil).Once() 404 405 var startupWg, shutdownWg sync.WaitGroup 406 407 vote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig()) 408 startupWg.Add(1) 409 // prepare goroutines, so they are ready to submit a vote at roughly same time 410 for i := 0; i < 5; i++ { 411 shutdownWg.Add(1) 412 go func() { 413 defer shutdownWg.Done() 414 startupWg.Wait() 415 err := s.processor.Process(vote) 416 require.NoError(s.T(), err) 417 }() 418 } 419 420 startupWg.Done() 421 422 // wait for all routines to finish 423 shutdownWg.Wait() 424 425 s.onQCCreatedState.AssertNumberOfCalls(s.T(), "onQCCreated", 1) 426 } 427 428 // TestCombinedVoteProcessorV2_PropertyCreatingQCCorrectness uses property testing to test correctness of concurrent votes processing. 429 // We randomly draw a committee with some number of staking, random beacon and byzantine nodes. 430 // Values are drawn in a way that 1 <= honestParticipants <= participants <= maxParticipants 431 // In each test iteration we expect to create a valid QC with all provided data as part of constructed QC. 432 func TestCombinedVoteProcessorV2_PropertyCreatingQCCorrectness(testifyT *testing.T) { 433 maxParticipants := uint64(53) 434 435 rapid.Check(testifyT, func(t *rapid.T) { 436 // draw participants in range 1 <= participants <= maxParticipants 437 participants := rapid.Uint64Range(1, maxParticipants).Draw(t, "participants") 438 beaconSignersCount := rapid.Uint64Range(participants/2+1, participants).Draw(t, "beaconSigners") 439 stakingSignersCount := participants - beaconSignersCount 440 require.Equal(t, participants, stakingSignersCount+beaconSignersCount) 441 442 // setup how many votes we need to create a QC 443 // 1 <= honestParticipants <= participants <= maxParticipants 444 honestParticipants := participants*2/3 + 1 445 sigWeight := uint64(100) 446 minRequiredWeight := honestParticipants * sigWeight 447 448 // proposing block 449 block := helper.MakeBlock() 450 451 t.Logf("running conf\n\t"+ 452 "staking signers: %v, beacon signers: %v\n\t"+ 453 "required weight: %v", stakingSignersCount, beaconSignersCount, minRequiredWeight) 454 455 stakingTotalWeight, collectedShares := *atomic.NewUint64(0), *atomic.NewUint64(0) 456 457 // setup aggregators and reconstructor 458 stakingAggregator := &mockhotstuff.WeightedSignatureAggregator{} 459 reconstructor := &mockhotstuff.RandomBeaconReconstructor{} 460 461 stakingSigners := unittest.IdentifierListFixture(int(stakingSignersCount)) 462 beaconSigners := unittest.IdentifierListFixture(int(beaconSignersCount)) 463 464 // lists to track signers that actually contributed their signatures 465 var ( 466 aggregatedStakingSigners flow.IdentifierList 467 ) 468 469 // need separate locks to safely update vectors of voted signers 470 stakingAggregatorLock := &sync.Mutex{} 471 472 stakingAggregator.On("TotalWeight").Return(func() uint64 { 473 return stakingTotalWeight.Load() 474 }) 475 reconstructor.On("EnoughShares").Return(func() bool { 476 return collectedShares.Load() >= beaconSignersCount 477 }) 478 479 // mock expected calls to aggregators and reconstructor 480 combinedSigs := unittest.SignaturesFixture(2) 481 stakingAggregator.On("Aggregate").Return( 482 func() flow.IdentifierList { 483 stakingAggregatorLock.Lock() 484 defer stakingAggregatorLock.Unlock() 485 return aggregatedStakingSigners 486 }, 487 func() []byte { return combinedSigs[0] }, 488 func() error { return nil }).Once() 489 reconstructor.On("Reconstruct").Return(combinedSigs[1], nil).Once() 490 491 // mock expected call to Packer 492 mergedSignerIDs := make(flow.IdentifierList, 0) 493 packedSigData := unittest.RandomBytes(128) 494 pcker := &mockhotstuff.Packer{} 495 pcker.On("Pack", block.View, mock.Anything).Run(func(args mock.Arguments) { 496 blockSigData := args.Get(1).(*hotstuff.BlockSignatureData) 497 498 // check that aggregated signers are part of all votes signers 499 // due to concurrent processing it is possible that Aggregate will return less that we have actually aggregated 500 // but still enough to construct the QC 501 stakingAggregatorLock.Lock() 502 require.Subset(t, aggregatedStakingSigners, blockSigData.StakingSigners) 503 stakingAggregatorLock.Unlock() 504 require.Nil(t, blockSigData.RandomBeaconSigners) 505 require.Nil(t, blockSigData.AggregatedRandomBeaconSig) 506 require.GreaterOrEqual(t, uint64(len(blockSigData.StakingSigners)), 507 honestParticipants) 508 509 expectedBlockSigData := &hotstuff.BlockSignatureData{ 510 StakingSigners: blockSigData.StakingSigners, 511 RandomBeaconSigners: nil, 512 AggregatedStakingSig: []byte(combinedSigs[0]), 513 AggregatedRandomBeaconSig: nil, 514 ReconstructedRandomBeaconSig: combinedSigs[1], 515 } 516 517 require.Equal(t, expectedBlockSigData, blockSigData) 518 519 // fill merged signers with collected signers 520 mergedSignerIDs = append(expectedBlockSigData.StakingSigners, expectedBlockSigData.RandomBeaconSigners...) 521 }).Return( 522 func(uint64, *hotstuff.BlockSignatureData) []byte { 523 signerIndices, _ := msig.EncodeSignersToIndices(mergedSignerIDs, mergedSignerIDs) 524 return signerIndices 525 }, 526 func(uint64, *hotstuff.BlockSignatureData) []byte { return packedSigData }, 527 func(uint64, *hotstuff.BlockSignatureData) error { return nil }).Once() 528 529 // track if QC was created 530 qcCreated := atomic.NewBool(false) 531 532 // expected QC 533 onQCCreated := func(qc *flow.QuorumCertificate) { 534 // QC should be created only once 535 if !qcCreated.CompareAndSwap(false, true) { 536 t.Fatalf("QC created more than once") 537 } 538 539 signerIndices, err := msig.EncodeSignersToIndices(mergedSignerIDs, mergedSignerIDs) 540 require.NoError(t, err) 541 542 // ensure that QC contains correct field 543 expectedQC := &flow.QuorumCertificate{ 544 View: block.View, 545 BlockID: block.BlockID, 546 SignerIndices: signerIndices, 547 SigData: packedSigData, 548 } 549 require.Equalf(t, expectedQC, qc, "QC should be equal to what we expect") 550 } 551 552 processor := &CombinedVoteProcessorV2{ 553 log: unittest.Logger(), 554 block: block, 555 stakingSigAggtor: stakingAggregator, 556 rbRector: reconstructor, 557 onQCCreated: onQCCreated, 558 packer: pcker, 559 minRequiredWeight: minRequiredWeight, 560 done: *atomic.NewBool(false), 561 } 562 563 votes := make([]*model.Vote, 0, stakingSignersCount+beaconSignersCount) 564 565 expectStakingAggregatorCalls := func(vote *model.Vote) { 566 expectedSig := crypto.Signature(vote.SigData[:msig.SigLen]) 567 stakingAggregator.On("Verify", vote.SignerID, expectedSig).Return(nil).Maybe() 568 stakingAggregator.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) { 569 signerID := args.Get(0).(flow.Identifier) 570 stakingAggregatorLock.Lock() 571 defer stakingAggregatorLock.Unlock() 572 stakingTotalWeight.Add(sigWeight) 573 aggregatedStakingSigners = append(aggregatedStakingSigners, signerID) 574 }).Return(uint64(0), nil).Maybe() 575 } 576 577 // prepare votes 578 for _, signer := range stakingSigners { 579 vote := unittest.VoteForBlockFixture(processor.Block(), VoteWithStakingSig()) 580 vote.SignerID = signer 581 // this will set up mock 582 expectStakingAggregatorCalls(vote) 583 votes = append(votes, vote) 584 } 585 for _, signer := range beaconSigners { 586 vote := unittest.VoteForBlockFixture(processor.Block(), VoteWithDoubleSig()) 587 vote.SignerID = signer 588 expectStakingAggregatorCalls(vote) 589 expectedSig := crypto.Signature(vote.SigData[msig.SigLen:]) 590 reconstructor.On("Verify", vote.SignerID, expectedSig).Return(nil).Maybe() 591 reconstructor.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) { 592 collectedShares.Inc() 593 }).Return(true, nil).Maybe() 594 votes = append(votes, vote) 595 } 596 597 // shuffle votes in random order 598 rand.Shuffle(len(votes), func(i, j int) { 599 votes[i], votes[j] = votes[j], votes[i] 600 }) 601 602 var startProcessing, finishProcessing sync.WaitGroup 603 startProcessing.Add(1) 604 // process votes concurrently by multiple workers 605 for _, vote := range votes { 606 finishProcessing.Add(1) 607 go func(vote *model.Vote) { 608 defer finishProcessing.Done() 609 startProcessing.Wait() 610 err := processor.Process(vote) 611 require.NoError(t, err) 612 }(vote) 613 } 614 615 // start all goroutines at the same time 616 startProcessing.Done() 617 finishProcessing.Wait() 618 619 passed := processor.done.Load() 620 passed = passed && qcCreated.Load() 621 passed = passed && stakingAggregator.AssertExpectations(t) 622 passed = passed && reconstructor.AssertExpectations(t) 623 624 if !passed { 625 t.Fatalf("Assertions weren't met, staking weight: %v, shares collected: %v", stakingTotalWeight, collectedShares.Load()) 626 } 627 628 //processing extra votes shouldn't result in creating new QCs 629 vote := unittest.VoteForBlockFixture(block, VoteWithDoubleSig()) 630 err := processor.Process(vote) 631 require.NoError(t, err) 632 }) 633 } 634 635 // TestCombinedVoteProcessorV2_PropertyCreatingQCLiveness uses property testing to test liveness of concurrent votes processing. 636 // We randomly draw a committee and check if we are able to create a QC with minimal number of nodes. 637 // In each test iteration we expect to create a QC, we don't check correctness of data since it's checked by another test. 638 func TestCombinedVoteProcessorV2_PropertyCreatingQCLiveness(testifyT *testing.T) { 639 rapid.Check(testifyT, func(t *rapid.T) { 640 // draw beacon signers in range 1 <= beaconSignersCount <= 53 641 beaconSignersCount := rapid.Uint64Range(1, 53).Draw(t, "beaconSigners") 642 // draw staking signers in range 0 <= stakingSignersCount <= 10 643 stakingSignersCount := rapid.Uint64Range(0, 10).Draw(t, "stakingSigners") 644 645 stakingWeightRange, beaconWeightRange := rapid.Uint64Range(1, 10), rapid.Uint64Range(1, 10) 646 647 minRequiredWeight := uint64(0) 648 // draw weight for each signer randomly 649 stakingSigners := unittest.IdentityListFixture(int(stakingSignersCount), func(identity *flow.Identity) { 650 identity.InitialWeight = stakingWeightRange.Draw(t, identity.String()) 651 minRequiredWeight += identity.InitialWeight 652 }) 653 beaconSigners := unittest.IdentityListFixture(int(beaconSignersCount), func(identity *flow.Identity) { 654 identity.InitialWeight = beaconWeightRange.Draw(t, identity.String()) 655 minRequiredWeight += identity.InitialWeight 656 }) 657 658 // proposing block 659 block := helper.MakeBlock() 660 661 t.Logf("running conf\n\t"+ 662 "staking signers: %v, beacon signers: %v\n\t"+ 663 "required weight: %v", stakingSignersCount, beaconSignersCount, minRequiredWeight) 664 665 stakingTotalWeight, collectedShares := atomic.NewUint64(0), atomic.NewUint64(0) 666 667 // setup aggregators and reconstructor 668 stakingAggregator := &mockhotstuff.WeightedSignatureAggregator{} 669 reconstructor := &mockhotstuff.RandomBeaconReconstructor{} 670 671 stakingAggregator.On("TotalWeight").Return(func() uint64 { 672 return stakingTotalWeight.Load() 673 }) 674 // don't require shares 675 reconstructor.On("EnoughShares").Return(func() bool { 676 return collectedShares.Load() >= beaconSignersCount 677 }) 678 679 // mock expected calls to aggregator and reconstructor 680 combinedSigs := unittest.SignaturesFixture(2) 681 stakingAggregator.On("Aggregate").Return(stakingSigners.NodeIDs(), []byte(combinedSigs[0]), nil).Once() 682 reconstructor.On("Reconstruct").Return(combinedSigs[1], nil).Once() 683 684 // mock expected call to Packer 685 mergedSignerIDs := append(stakingSigners.NodeIDs(), beaconSigners.NodeIDs()...) 686 packedSigData := unittest.RandomBytes(128) 687 pcker := &mockhotstuff.Packer{} 688 689 signerIndices, err := msig.EncodeSignersToIndices(mergedSignerIDs, mergedSignerIDs) 690 require.NoError(t, err) 691 pcker.On("Pack", block.View, mock.Anything).Return(signerIndices, packedSigData, nil) 692 693 // track if QC was created 694 qcCreated := atomic.NewBool(false) 695 696 // expected QC 697 onQCCreated := func(qc *flow.QuorumCertificate) { 698 // QC should be created only once 699 if !qcCreated.CompareAndSwap(false, true) { 700 t.Fatalf("QC created more than once") 701 } 702 } 703 704 processor := &CombinedVoteProcessorV2{ 705 log: unittest.Logger(), 706 block: block, 707 stakingSigAggtor: stakingAggregator, 708 rbRector: reconstructor, 709 onQCCreated: onQCCreated, 710 packer: pcker, 711 minRequiredWeight: minRequiredWeight, 712 done: *atomic.NewBool(false), 713 } 714 715 votes := make([]*model.Vote, 0, stakingSignersCount+beaconSignersCount) 716 717 expectStakingAggregatorCalls := func(vote *model.Vote, weight uint64) { 718 expectedSig := crypto.Signature(vote.SigData[:msig.SigLen]) 719 stakingAggregator.On("Verify", vote.SignerID, expectedSig).Return(nil).Maybe() 720 stakingAggregator.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) { 721 stakingTotalWeight.Add(weight) 722 }).Return(uint64(0), nil).Maybe() 723 } 724 725 // prepare votes 726 for _, signer := range stakingSigners { 727 vote := unittest.VoteForBlockFixture(processor.Block(), VoteWithStakingSig()) 728 vote.SignerID = signer.ID() 729 expectStakingAggregatorCalls(vote, signer.InitialWeight) 730 votes = append(votes, vote) 731 } 732 for _, signer := range beaconSigners { 733 vote := unittest.VoteForBlockFixture(processor.Block(), VoteWithDoubleSig()) 734 vote.SignerID = signer.ID() 735 expectStakingAggregatorCalls(vote, signer.InitialWeight) 736 expectedSig := crypto.Signature(vote.SigData[msig.SigLen:]) 737 reconstructor.On("Verify", vote.SignerID, expectedSig).Return(nil).Maybe() 738 reconstructor.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) { 739 collectedShares.Inc() 740 }).Return(true, nil).Maybe() 741 votes = append(votes, vote) 742 } 743 744 // shuffle votes in random order 745 rand.Shuffle(len(votes), func(i, j int) { 746 votes[i], votes[j] = votes[j], votes[i] 747 }) 748 749 var startProcessing, finishProcessing sync.WaitGroup 750 startProcessing.Add(1) 751 // process votes concurrently by multiple workers 752 for _, vote := range votes { 753 finishProcessing.Add(1) 754 go func(vote *model.Vote) { 755 defer finishProcessing.Done() 756 startProcessing.Wait() 757 err := processor.Process(vote) 758 require.NoError(t, err) 759 }(vote) 760 } 761 762 // start all goroutines at the same time 763 startProcessing.Done() 764 finishProcessing.Wait() 765 766 passed := processor.done.Load() 767 passed = passed && qcCreated.Load() 768 passed = passed && stakingAggregator.AssertExpectations(t) 769 passed = passed && reconstructor.AssertExpectations(t) 770 771 if !passed { 772 t.Fatalf("Assertions weren't met, staking weight: %v, collected shares: %v", stakingTotalWeight.Load(), collectedShares.Load()) 773 } 774 }) 775 } 776 777 // TestCombinedVoteProcessorV2_BuildVerifyQC tests a complete path from creating votes to collecting votes and then 778 // building & verifying QC. 779 // We start with leader proposing a block, then new leader collects votes and builds a QC. 780 // Need to verify that QC that was produced is valid and can be embedded in new proposal. 781 func TestCombinedVoteProcessorV2_BuildVerifyQC(t *testing.T) { 782 epochCounter := uint64(3) 783 epochLookup := &modulemock.EpochLookup{} 784 view := uint64(20) 785 epochLookup.On("EpochForViewWithFallback", view).Return(epochCounter, nil) 786 787 // all committee members run DKG 788 dkgData, err := bootstrapDKG.RandomBeaconKG(11, unittest.RandomBytes(32)) 789 require.NoError(t, err) 790 791 // signers hold objects that are created with private key and can sign votes and proposals 792 signers := make(map[flow.Identifier]*verification.CombinedSigner) 793 794 // prepare staking signers, each signer has it's own private/public key pair 795 // stakingSigners sign only with staking key, meaning they have failed DKG 796 stakingSigners := unittest.IdentityListFixture(3) 797 beaconSigners := unittest.IdentityListFixture(8) 798 allIdentities := append(stakingSigners, beaconSigners...) 799 require.Equal(t, len(dkgData.PubKeyShares), len(allIdentities)) 800 dkgParticipants := make(map[flow.Identifier]flow.DKGParticipant) 801 // fill dkg participants data 802 for index, identity := range allIdentities { 803 dkgParticipants[identity.NodeID] = flow.DKGParticipant{ 804 Index: uint(index), 805 KeyShare: dkgData.PubKeyShares[index], 806 } 807 } 808 809 for _, identity := range stakingSigners { 810 stakingPriv := unittest.StakingPrivKeyFixture() 811 identity.StakingPubKey = stakingPriv.PublicKey() 812 813 keys := &storagemock.SafeBeaconKeys{} 814 // there is no DKG key for this epoch 815 keys.On("RetrieveMyBeaconPrivateKey", epochCounter).Return(nil, false, nil) 816 817 beaconSignerStore := hsig.NewEpochAwareRandomBeaconKeyStore(epochLookup, keys) 818 819 me, err := local.New(identity.IdentitySkeleton, stakingPriv) 820 require.NoError(t, err) 821 822 signers[identity.NodeID] = verification.NewCombinedSigner(me, beaconSignerStore) 823 } 824 825 for _, identity := range beaconSigners { 826 stakingPriv := unittest.StakingPrivKeyFixture() 827 identity.StakingPubKey = stakingPriv.PublicKey() 828 829 participantData := dkgParticipants[identity.NodeID] 830 831 dkgKey := encodable.RandomBeaconPrivKey{ 832 PrivateKey: dkgData.PrivKeyShares[participantData.Index], 833 } 834 835 keys := &storagemock.SafeBeaconKeys{} 836 // there is DKG key for this epoch 837 keys.On("RetrieveMyBeaconPrivateKey", epochCounter).Return(dkgKey, true, nil) 838 839 beaconSignerStore := hsig.NewEpochAwareRandomBeaconKeyStore(epochLookup, keys) 840 841 me, err := local.New(identity.IdentitySkeleton, stakingPriv) 842 require.NoError(t, err) 843 844 signers[identity.NodeID] = verification.NewCombinedSigner(me, beaconSignerStore) 845 } 846 847 leader := stakingSigners[0] 848 849 block := helper.MakeBlock(helper.WithBlockView(view), 850 helper.WithBlockProposer(leader.NodeID)) 851 852 inmemDKG, err := inmem.DKGFromEncodable(inmem.EncodableDKG{ 853 GroupKey: encodable.RandomBeaconPubKey{ 854 PublicKey: dkgData.PubGroupKey, 855 }, 856 Participants: dkgParticipants, 857 }) 858 require.NoError(t, err) 859 860 committee := &mockhotstuff.DynamicCommittee{} 861 committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(allIdentities.ToSkeleton().TotalWeight()), nil) 862 committee.On("IdentitiesByEpoch", block.View).Return(allIdentities.ToSkeleton(), nil) 863 committee.On("IdentitiesByBlock", block.BlockID).Return(allIdentities, nil) 864 committee.On("DKG", block.View).Return(inmemDKG, nil) 865 866 votes := make([]*model.Vote, 0, len(allIdentities)) 867 868 // first staking signer will be leader collecting votes for proposal 869 // prepare votes for every member of committee except leader 870 for _, signer := range allIdentities[1:] { 871 vote, err := signers[signer.NodeID].CreateVote(block) 872 require.NoError(t, err) 873 votes = append(votes, vote) 874 } 875 876 // create and sign proposal 877 proposal, err := signers[leader.NodeID].CreateProposal(block) 878 require.NoError(t, err) 879 880 qcCreated := false 881 onQCCreated := func(qc *flow.QuorumCertificate) { 882 packer := hsig.NewConsensusSigDataPacker(committee) 883 884 // create verifier that will do crypto checks of created QC 885 verifier := verification.NewCombinedVerifier(committee, packer) 886 // create validator which will do compliance and crypto checked of created QC 887 validator := hotstuffvalidator.New(committee, verifier) 888 // check if QC is valid against parent 889 err := validator.ValidateQC(qc) 890 require.NoError(t, err) 891 892 qcCreated = true 893 } 894 895 voteProcessorFactory := NewCombinedVoteProcessorFactory(committee, onQCCreated) 896 voteProcessor, err := voteProcessorFactory.Create(unittest.Logger(), proposal) 897 require.NoError(t, err) 898 899 // process votes by new leader, this will result in producing new QC 900 for _, vote := range votes { 901 err := voteProcessor.Process(vote) 902 require.NoError(t, err) 903 } 904 905 require.True(t, qcCreated) 906 } 907 908 func VoteWithStakingSig() func(*model.Vote) { 909 return func(vote *model.Vote) { 910 vote.SigData = unittest.RandomBytes(msig.SigLen) 911 } 912 } 913 914 func VoteWithDoubleSig() func(*model.Vote) { 915 return func(vote *model.Vote) { 916 vote.SigData = unittest.RandomBytes(96) 917 } 918 } 919 920 // TestReadRandomSourceFromPackedQCV2 tests that a constructed QC can be unpacked and from which the random source can be 921 // retrieved 922 func TestReadRandomSourceFromPackedQCV2(t *testing.T) { 923 924 // for V2 there is no aggregated random beacon sig or random beacon signers 925 allSigners := unittest.IdentityListFixture(5) 926 // staking signers are only subset of all signers 927 stakingSigners := allSigners.NodeIDs()[:3] 928 929 aggregatedStakingSig := unittest.SignatureFixture() 930 reconstructedBeaconSig := unittest.SignatureFixture() 931 932 blockSigData := buildBlockSignatureDataForV2(stakingSigners, aggregatedStakingSig, reconstructedBeaconSig) 933 934 // making a mock block 935 header := unittest.BlockHeaderFixture() 936 block := model.BlockFromFlow(header) 937 938 // create a packer 939 committee := &mockhotstuff.DynamicCommittee{} 940 committee.On("IdentitiesByBlock", block.BlockID).Return(allSigners, nil) 941 committee.On("IdentitiesByEpoch", block.View).Return(allSigners.ToSkeleton(), nil) 942 packer := signature.NewConsensusSigDataPacker(committee) 943 944 qc, err := buildQCWithPackerAndSigData(packer, block, blockSigData) 945 require.NoError(t, err) 946 947 randomSource, err := model.BeaconSignature(qc) 948 require.NoError(t, err) 949 950 randomSourceAgain, err := model.BeaconSignature(qc) 951 require.NoError(t, err) 952 953 // verify the random source is deterministic 954 require.Equal(t, randomSource, randomSourceAgain) 955 }