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