github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/timeoutcollector/timeout_processor_test.go (about) 1 package timeoutcollector 2 3 import ( 4 "errors" 5 "fmt" 6 "math/rand" 7 "sync" 8 "testing" 9 10 "github.com/onflow/crypto" 11 "github.com/stretchr/testify/mock" 12 "github.com/stretchr/testify/require" 13 "github.com/stretchr/testify/suite" 14 "go.uber.org/atomic" 15 16 "github.com/onflow/flow-go/consensus/hotstuff" 17 "github.com/onflow/flow-go/consensus/hotstuff/committees" 18 "github.com/onflow/flow-go/consensus/hotstuff/helper" 19 "github.com/onflow/flow-go/consensus/hotstuff/mocks" 20 "github.com/onflow/flow-go/consensus/hotstuff/model" 21 hotstuffvalidator "github.com/onflow/flow-go/consensus/hotstuff/validator" 22 "github.com/onflow/flow-go/consensus/hotstuff/verification" 23 "github.com/onflow/flow-go/consensus/hotstuff/votecollector" 24 "github.com/onflow/flow-go/model/flow" 25 "github.com/onflow/flow-go/module/local" 26 msig "github.com/onflow/flow-go/module/signature" 27 "github.com/onflow/flow-go/utils/unittest" 28 ) 29 30 func TestTimeoutProcessor(t *testing.T) { 31 suite.Run(t, new(TimeoutProcessorTestSuite)) 32 } 33 34 // TimeoutProcessorTestSuite is a test suite that holds mocked state for isolated testing of TimeoutProcessor. 35 type TimeoutProcessorTestSuite struct { 36 suite.Suite 37 38 participants flow.IdentitySkeletonList 39 signer *flow.IdentitySkeleton 40 view uint64 41 sigWeight uint64 42 totalWeight atomic.Uint64 43 committee *mocks.Replicas 44 validator *mocks.Validator 45 sigAggregator *mocks.TimeoutSignatureAggregator 46 notifier *mocks.TimeoutCollectorConsumer 47 processor *TimeoutProcessor 48 } 49 50 func (s *TimeoutProcessorTestSuite) SetupTest() { 51 var err error 52 s.sigWeight = 1000 53 s.committee = mocks.NewReplicas(s.T()) 54 s.validator = mocks.NewValidator(s.T()) 55 s.sigAggregator = mocks.NewTimeoutSignatureAggregator(s.T()) 56 s.notifier = mocks.NewTimeoutCollectorConsumer(s.T()) 57 s.participants = unittest.IdentityListFixture(11, unittest.WithInitialWeight(s.sigWeight)).Sort(flow.Canonical[flow.Identity]).ToSkeleton() 58 s.signer = s.participants[0] 59 s.view = (uint64)(rand.Uint32() + 100) 60 s.totalWeight = *atomic.NewUint64(0) 61 62 s.committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(s.participants.TotalWeight()), nil).Maybe() 63 s.committee.On("TimeoutThresholdForView", mock.Anything).Return(committees.WeightThresholdToTimeout(s.participants.TotalWeight()), nil).Maybe() 64 s.committee.On("IdentityByEpoch", mock.Anything, mock.Anything).Return(s.signer, nil).Maybe() 65 s.sigAggregator.On("View").Return(s.view).Maybe() 66 s.sigAggregator.On("VerifyAndAdd", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 67 s.totalWeight.Add(s.sigWeight) 68 }).Return(func(signerID flow.Identifier, sig crypto.Signature, newestQCView uint64) uint64 { 69 return s.totalWeight.Load() 70 }, func(signerID flow.Identifier, sig crypto.Signature, newestQCView uint64) error { 71 return nil 72 }).Maybe() 73 s.sigAggregator.On("TotalWeight").Return(func() uint64 { 74 return s.totalWeight.Load() 75 }).Maybe() 76 77 s.processor, err = NewTimeoutProcessor( 78 unittest.Logger(), 79 s.committee, 80 s.validator, 81 s.sigAggregator, 82 s.notifier, 83 ) 84 require.NoError(s.T(), err) 85 } 86 87 // TimeoutLastViewSuccessfulFixture creates a valid timeout if last view has ended with QC. 88 func (s *TimeoutProcessorTestSuite) TimeoutLastViewSuccessfulFixture(opts ...func(*model.TimeoutObject)) *model.TimeoutObject { 89 timeout := helper.TimeoutObjectFixture( 90 helper.WithTimeoutObjectView(s.view), 91 helper.WithTimeoutNewestQC(helper.MakeQC(helper.WithQCView(s.view-1))), 92 helper.WithTimeoutLastViewTC(nil), 93 ) 94 95 for _, opt := range opts { 96 opt(timeout) 97 } 98 99 return timeout 100 } 101 102 // TimeoutLastViewFailedFixture creates a valid timeout if last view has ended with TC. 103 func (s *TimeoutProcessorTestSuite) TimeoutLastViewFailedFixture(opts ...func(*model.TimeoutObject)) *model.TimeoutObject { 104 newestQC := helper.MakeQC(helper.WithQCView(s.view - 10)) 105 timeout := helper.TimeoutObjectFixture( 106 helper.WithTimeoutObjectView(s.view), 107 helper.WithTimeoutNewestQC(newestQC), 108 helper.WithTimeoutLastViewTC(helper.MakeTC( 109 helper.WithTCView(s.view-1), 110 helper.WithTCNewestQC(helper.MakeQC(helper.WithQCView(newestQC.View))))), 111 ) 112 113 for _, opt := range opts { 114 opt(timeout) 115 } 116 117 return timeout 118 } 119 120 // TestProcess_TimeoutNotForView tests that TimeoutProcessor accepts only timeouts for the view it was initialized with 121 // We expect dedicated sentinel errors for timeouts for different views (`ErrTimeoutForIncompatibleView`). 122 func (s *TimeoutProcessorTestSuite) TestProcess_TimeoutNotForView() { 123 err := s.processor.Process(s.TimeoutLastViewSuccessfulFixture(func(t *model.TimeoutObject) { 124 t.View++ 125 })) 126 require.ErrorIs(s.T(), err, ErrTimeoutForIncompatibleView) 127 require.False(s.T(), model.IsInvalidTimeoutError(err)) 128 129 s.sigAggregator.AssertNotCalled(s.T(), "Verify") 130 } 131 132 // TestProcess_TimeoutWithoutQC tests that TimeoutProcessor fails with model.InvalidTimeoutError if 133 // timeout doesn't contain QC. 134 func (s *TimeoutProcessorTestSuite) TestProcess_TimeoutWithoutQC() { 135 err := s.processor.Process(s.TimeoutLastViewSuccessfulFixture(func(t *model.TimeoutObject) { 136 t.NewestQC = nil 137 })) 138 require.True(s.T(), model.IsInvalidTimeoutError(err)) 139 } 140 141 // TestProcess_TimeoutNewerHighestQC tests that TimeoutProcessor fails with model.InvalidTimeoutError if 142 // timeout contains a QC with QC.View > timeout.View, QC can be only with lower view than timeout. 143 func (s *TimeoutProcessorTestSuite) TestProcess_TimeoutNewerHighestQC() { 144 s.Run("t.View == t.NewestQC.View", func() { 145 err := s.processor.Process(s.TimeoutLastViewSuccessfulFixture(func(t *model.TimeoutObject) { 146 t.NewestQC.View = t.View 147 })) 148 require.True(s.T(), model.IsInvalidTimeoutError(err)) 149 }) 150 s.Run("t.View < t.NewestQC.View", func() { 151 err := s.processor.Process(s.TimeoutLastViewSuccessfulFixture(func(t *model.TimeoutObject) { 152 t.NewestQC.View = t.View + 1 153 })) 154 require.True(s.T(), model.IsInvalidTimeoutError(err)) 155 }) 156 } 157 158 // TestProcess_LastViewTCWrongView tests that TimeoutProcessor fails with model.InvalidTimeoutError if 159 // timeout contains a proof that sender legitimately entered timeout.View but it has wrong view meaning he used TC from previous rounds. 160 func (s *TimeoutProcessorTestSuite) TestProcess_LastViewTCWrongView() { 161 // if TC is included it must have timeout.View == timeout.LastViewTC.View+1 162 err := s.processor.Process(s.TimeoutLastViewFailedFixture(func(t *model.TimeoutObject) { 163 t.LastViewTC.View = t.View - 10 164 })) 165 require.True(s.T(), model.IsInvalidTimeoutError(err)) 166 } 167 168 // TestProcess_LastViewHighestQCInvalidView tests that TimeoutProcessor fails with model.InvalidTimeoutError if 169 // timeout contains a proof that sender legitimately entered timeout.View but included HighestQC has older view 170 // than QC included in TC. For honest nodes this shouldn't happen. 171 func (s *TimeoutProcessorTestSuite) TestProcess_LastViewHighestQCInvalidView() { 172 err := s.processor.Process(s.TimeoutLastViewFailedFixture(func(t *model.TimeoutObject) { 173 t.LastViewTC.NewestQC.View = t.NewestQC.View + 1 // TC contains newer QC than Timeout Object 174 })) 175 require.True(s.T(), model.IsInvalidTimeoutError(err)) 176 } 177 178 // TestProcess_LastViewTCRequiredButNotPresent tests that TimeoutProcessor fails with model.InvalidTimeoutError if 179 // timeout must contain a proof that sender legitimately entered timeout.View but doesn't have it. 180 func (s *TimeoutProcessorTestSuite) TestProcess_LastViewTCRequiredButNotPresent() { 181 // if last view is not successful(timeout.View != timeout.HighestQC.View+1) then this 182 // timeout must contain valid timeout.LastViewTC 183 err := s.processor.Process(s.TimeoutLastViewFailedFixture(func(t *model.TimeoutObject) { 184 t.LastViewTC = nil 185 })) 186 require.True(s.T(), model.IsInvalidTimeoutError(err)) 187 } 188 189 // TestProcess_IncludedQCInvalid tests that TimeoutProcessor correctly handles validation errors if 190 // timeout is well-formed but included QC is invalid 191 func (s *TimeoutProcessorTestSuite) TestProcess_IncludedQCInvalid() { 192 timeout := s.TimeoutLastViewSuccessfulFixture() 193 194 s.Run("invalid-qc-sentinel", func() { 195 *s.validator = *mocks.NewValidator(s.T()) 196 s.validator.On("ValidateQC", timeout.NewestQC).Return(model.InvalidQCError{}).Once() 197 198 err := s.processor.Process(timeout) 199 require.True(s.T(), model.IsInvalidTimeoutError(err)) 200 require.True(s.T(), model.IsInvalidQCError(err)) 201 }) 202 s.Run("invalid-qc-exception", func() { 203 exception := errors.New("validate-qc-failed") 204 *s.validator = *mocks.NewValidator(s.T()) 205 s.validator.On("ValidateQC", timeout.NewestQC).Return(exception).Once() 206 207 err := s.processor.Process(timeout) 208 require.ErrorIs(s.T(), err, exception) 209 require.False(s.T(), model.IsInvalidTimeoutError(err)) 210 }) 211 s.Run("invalid-qc-err-view-for-unknown-epoch", func() { 212 *s.validator = *mocks.NewValidator(s.T()) 213 s.validator.On("ValidateQC", timeout.NewestQC).Return(model.ErrViewForUnknownEpoch).Once() 214 215 err := s.processor.Process(timeout) 216 require.False(s.T(), model.IsInvalidTimeoutError(err)) 217 require.NotErrorIs(s.T(), err, model.ErrViewForUnknownEpoch) 218 }) 219 } 220 221 // TestProcess_IncludedTCInvalid tests that TimeoutProcessor correctly handles validation errors if 222 // timeout is well-formed but included TC is invalid 223 func (s *TimeoutProcessorTestSuite) TestProcess_IncludedTCInvalid() { 224 timeout := s.TimeoutLastViewFailedFixture() 225 226 s.Run("invalid-tc-sentinel", func() { 227 *s.validator = *mocks.NewValidator(s.T()) 228 s.validator.On("ValidateQC", timeout.NewestQC).Return(nil) 229 s.validator.On("ValidateTC", timeout.LastViewTC).Return(model.InvalidTCError{}) 230 231 err := s.processor.Process(timeout) 232 require.True(s.T(), model.IsInvalidTimeoutError(err)) 233 require.True(s.T(), model.IsInvalidTCError(err)) 234 }) 235 s.Run("invalid-tc-exception", func() { 236 exception := errors.New("validate-tc-failed") 237 *s.validator = *mocks.NewValidator(s.T()) 238 s.validator.On("ValidateQC", timeout.NewestQC).Return(nil) 239 s.validator.On("ValidateTC", timeout.LastViewTC).Return(exception).Once() 240 241 err := s.processor.Process(timeout) 242 require.ErrorIs(s.T(), err, exception) 243 require.False(s.T(), model.IsInvalidTimeoutError(err)) 244 }) 245 s.Run("invalid-tc-err-view-for-unknown-epoch", func() { 246 *s.validator = *mocks.NewValidator(s.T()) 247 s.validator.On("ValidateQC", timeout.NewestQC).Return(nil) 248 s.validator.On("ValidateTC", timeout.LastViewTC).Return(model.ErrViewForUnknownEpoch).Once() 249 250 err := s.processor.Process(timeout) 251 require.False(s.T(), model.IsInvalidTimeoutError(err)) 252 require.NotErrorIs(s.T(), err, model.ErrViewForUnknownEpoch) 253 }) 254 } 255 256 // TestProcess_ValidTimeout tests that processing a valid timeout succeeds without error 257 func (s *TimeoutProcessorTestSuite) TestProcess_ValidTimeout() { 258 s.Run("happy-path", func() { 259 timeout := s.TimeoutLastViewSuccessfulFixture() 260 s.validator.On("ValidateQC", timeout.NewestQC).Return(nil).Once() 261 err := s.processor.Process(timeout) 262 require.NoError(s.T(), err) 263 s.sigAggregator.AssertCalled(s.T(), "VerifyAndAdd", timeout.SignerID, timeout.SigData, timeout.NewestQC.View) 264 }) 265 s.Run("recovery-path", func() { 266 timeout := s.TimeoutLastViewFailedFixture() 267 s.validator.On("ValidateQC", timeout.NewestQC).Return(nil).Once() 268 s.validator.On("ValidateTC", timeout.LastViewTC).Return(nil).Once() 269 err := s.processor.Process(timeout) 270 require.NoError(s.T(), err) 271 s.sigAggregator.AssertCalled(s.T(), "VerifyAndAdd", timeout.SignerID, timeout.SigData, timeout.NewestQC.View) 272 }) 273 } 274 275 // TestProcess_VerifyAndAddFailed tests different scenarios when TimeoutSignatureAggregator fails with error. 276 // We check all sentinel errors and exceptions in this scenario. 277 func (s *TimeoutProcessorTestSuite) TestProcess_VerifyAndAddFailed() { 278 timeout := s.TimeoutLastViewSuccessfulFixture() 279 s.validator.On("ValidateQC", timeout.NewestQC).Return(nil) 280 s.Run("invalid-signer", func() { 281 *s.sigAggregator = *mocks.NewTimeoutSignatureAggregator(s.T()) 282 s.sigAggregator.On("VerifyAndAdd", mock.Anything, mock.Anything, mock.Anything). 283 Return(uint64(0), model.NewInvalidSignerError(fmt.Errorf(""))).Once() 284 err := s.processor.Process(timeout) 285 require.True(s.T(), model.IsInvalidTimeoutError(err)) 286 require.True(s.T(), model.IsInvalidSignerError(err)) 287 }) 288 s.Run("invalid-signature", func() { 289 *s.sigAggregator = *mocks.NewTimeoutSignatureAggregator(s.T()) 290 s.sigAggregator.On("VerifyAndAdd", mock.Anything, mock.Anything, mock.Anything). 291 Return(uint64(0), model.ErrInvalidSignature).Once() 292 err := s.processor.Process(timeout) 293 require.True(s.T(), model.IsInvalidTimeoutError(err)) 294 require.ErrorIs(s.T(), err, model.ErrInvalidSignature) 295 }) 296 s.Run("duplicated-signer", func() { 297 *s.sigAggregator = *mocks.NewTimeoutSignatureAggregator(s.T()) 298 s.sigAggregator.On("VerifyAndAdd", mock.Anything, mock.Anything, mock.Anything). 299 Return(uint64(0), model.NewDuplicatedSignerErrorf("")).Once() 300 err := s.processor.Process(timeout) 301 require.True(s.T(), model.IsDuplicatedSignerError(err)) 302 // this shouldn't be wrapped in invalid timeout 303 require.False(s.T(), model.IsInvalidTimeoutError(err)) 304 }) 305 s.Run("verify-exception", func() { 306 *s.sigAggregator = *mocks.NewTimeoutSignatureAggregator(s.T()) 307 exception := errors.New("verify-exception") 308 s.sigAggregator.On("VerifyAndAdd", mock.Anything, mock.Anything, mock.Anything). 309 Return(uint64(0), exception).Once() 310 err := s.processor.Process(timeout) 311 require.False(s.T(), model.IsInvalidTimeoutError(err)) 312 require.ErrorIs(s.T(), err, exception) 313 }) 314 } 315 316 // TestProcess_CreatingTC is a test for happy path single threaded signature aggregation and TC creation 317 // Each replica commits unique timeout object, this object gets processed by TimeoutProcessor. After collecting 318 // enough weight we expect a TC to be created. All further operations should be no-op, only one TC should be created. 319 func (s *TimeoutProcessorTestSuite) TestProcess_CreatingTC() { 320 // consider next situation: 321 // last successful view was N, after this we weren't able to get a proposal with QC for 322 // len(participants) views, but in each view QC was created(but not distributed). 323 // In view N+len(participants) each replica contributes with unique highest QC. 324 lastSuccessfulQC := helper.MakeQC(helper.WithQCView(s.view - uint64(len(s.participants)))) 325 lastViewTC := helper.MakeTC(helper.WithTCView(s.view-1), 326 helper.WithTCNewestQC(lastSuccessfulQC)) 327 328 var highQCViews []uint64 329 var timeouts []*model.TimeoutObject 330 signers := s.participants[1:] 331 for i, signer := range signers { 332 qc := helper.MakeQC(helper.WithQCView(lastSuccessfulQC.View + uint64(i+1))) 333 highQCViews = append(highQCViews, qc.View) 334 335 timeout := helper.TimeoutObjectFixture( 336 helper.WithTimeoutObjectView(s.view), 337 helper.WithTimeoutNewestQC(qc), 338 helper.WithTimeoutObjectSignerID(signer.NodeID), 339 helper.WithTimeoutLastViewTC(lastViewTC), 340 ) 341 timeouts = append(timeouts, timeout) 342 } 343 344 // change tracker to require all except one signer to create TC 345 s.processor.tcTracker.minRequiredWeight = s.sigWeight * uint64(len(highQCViews)) 346 347 signerIndices, err := msig.EncodeSignersToIndices(s.participants.NodeIDs(), signers.NodeIDs()) 348 require.NoError(s.T(), err) 349 expectedSig := crypto.Signature(unittest.RandomBytes(128)) 350 s.validator.On("ValidateQC", mock.Anything).Return(nil) 351 s.validator.On("ValidateTC", mock.Anything).Return(nil) 352 s.notifier.On("OnPartialTcCreated", s.view, mock.Anything, lastViewTC).Return(nil).Once() 353 s.notifier.On("OnTcConstructedFromTimeouts", mock.Anything).Run(func(args mock.Arguments) { 354 newestQC := timeouts[len(timeouts)-1].NewestQC 355 tc := args.Get(0).(*flow.TimeoutCertificate) 356 // ensure that TC contains correct fields 357 expectedTC := &flow.TimeoutCertificate{ 358 View: s.view, 359 NewestQCViews: highQCViews, 360 NewestQC: newestQC, 361 SignerIndices: signerIndices, 362 SigData: expectedSig, 363 } 364 require.Equal(s.T(), expectedTC, tc) 365 }).Return(nil).Once() 366 367 signersData := make([]hotstuff.TimeoutSignerInfo, 0) 368 for i, signer := range signers.NodeIDs() { 369 signersData = append(signersData, hotstuff.TimeoutSignerInfo{ 370 NewestQCView: highQCViews[i], 371 Signer: signer, 372 }) 373 } 374 s.sigAggregator.On("Aggregate").Return(signersData, expectedSig, nil) 375 s.committee.On("IdentitiesByEpoch", s.view).Return(s.participants, nil) 376 377 for _, timeout := range timeouts { 378 err := s.processor.Process(timeout) 379 require.NoError(s.T(), err) 380 } 381 s.notifier.AssertExpectations(s.T()) 382 s.sigAggregator.AssertExpectations(s.T()) 383 384 // add extra timeout, make sure we don't create another TC 385 // should be no-op 386 timeout := helper.TimeoutObjectFixture( 387 helper.WithTimeoutObjectView(s.view), 388 helper.WithTimeoutNewestQC(helper.MakeQC(helper.WithQCView(lastSuccessfulQC.View))), 389 helper.WithTimeoutObjectSignerID(s.participants[0].NodeID), 390 helper.WithTimeoutLastViewTC(nil), 391 ) 392 err = s.processor.Process(timeout) 393 require.NoError(s.T(), err) 394 395 s.notifier.AssertExpectations(s.T()) 396 s.validator.AssertExpectations(s.T()) 397 } 398 399 // TestProcess_ConcurrentCreatingTC tests a scenario where multiple goroutines process timeout at same time, 400 // we expect only one TC created in this scenario. 401 func (s *TimeoutProcessorTestSuite) TestProcess_ConcurrentCreatingTC() { 402 s.validator.On("ValidateQC", mock.Anything).Return(nil) 403 s.notifier.On("OnPartialTcCreated", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() 404 s.notifier.On("OnTcConstructedFromTimeouts", mock.Anything).Return(nil).Once() 405 s.committee.On("IdentitiesByEpoch", mock.Anything).Return(s.participants, nil) 406 407 signersData := make([]hotstuff.TimeoutSignerInfo, 0, len(s.participants)) 408 for _, signer := range s.participants.NodeIDs() { 409 signersData = append(signersData, hotstuff.TimeoutSignerInfo{ 410 NewestQCView: 0, 411 Signer: signer, 412 }) 413 } 414 // don't care about actual data 415 s.sigAggregator.On("Aggregate").Return(signersData, crypto.Signature{}, nil) 416 417 var startupWg, shutdownWg sync.WaitGroup 418 419 newestQC := helper.MakeQC(helper.WithQCView(s.view - 1)) 420 421 startupWg.Add(1) 422 // prepare goroutines, so they are ready to submit a timeout at roughly same time 423 for i, signer := range s.participants { 424 shutdownWg.Add(1) 425 timeout := helper.TimeoutObjectFixture( 426 helper.WithTimeoutObjectView(s.view), 427 helper.WithTimeoutNewestQC(newestQC), 428 helper.WithTimeoutObjectSignerID(signer.NodeID), 429 helper.WithTimeoutLastViewTC(nil), 430 ) 431 go func(i int, timeout *model.TimeoutObject) { 432 defer shutdownWg.Done() 433 startupWg.Wait() 434 err := s.processor.Process(timeout) 435 require.NoError(s.T(), err) 436 }(i, timeout) 437 } 438 439 startupWg.Done() 440 441 // wait for all routines to finish 442 shutdownWg.Wait() 443 } 444 445 // TestTimeoutProcessor_BuildVerifyTC tests a complete path from creating timeouts to collecting timeouts and then 446 // building & verifying TC. 447 // This test emulates the most complex scenario where TC consists of TimeoutObjects that are structurally different. 448 // Let's consider a case where at some view N consensus committee generated both QC and TC, resulting in nodes differently entering view N+1. 449 // When constructing TC for view N+1 some replicas will contribute with TO{View:N+1, NewestQC.View: N, LastViewTC: nil} 450 // while others with TO{View:N+1, NewestQC.View: N-1, LastViewTC: TC{View: N, NewestQC.View: N-1}}. 451 // This results in multi-message BLS signature with messages picked from set M={N-1,N}. 452 // We have to be able to construct a valid TC for view N+1 and successfully validate it. 453 // We start by building a valid QC for view N-1, that will be included in every TimeoutObject at view N. 454 // Right after we create a valid QC for view N. We need to have valid QCs since TimeoutProcessor performs complete validation of TimeoutObject. 455 // Then we create a valid cryptographically signed timeout for each signer. Created timeouts are feed to TimeoutProcessor 456 // which eventually creates a TC after seeing processing enough objects. After we verify if TC was correctly constructed 457 // and if it doesn't violate protocol rules. At this point we have QC for view N-1, both QC and TC for view N. 458 // After constructing valid objects we will repeat TC creation process and create a TC for view N+1 where replicas contribute 459 // with structurally different TimeoutObjects to make sure that TC is correctly built and can be successfully validated. 460 func TestTimeoutProcessor_BuildVerifyTC(t *testing.T) { 461 // signers hold objects that are created with private key and can sign votes and proposals 462 signers := make(map[flow.Identifier]*verification.StakingSigner) 463 // prepare staking signers, each signer has its own private/public key pair 464 // identities must be in canonical order 465 stakingSigners := unittest.IdentityListFixture(11, func(identity *flow.Identity) { 466 stakingPriv := unittest.StakingPrivKeyFixture() 467 identity.StakingPubKey = stakingPriv.PublicKey() 468 469 me, err := local.New(identity.IdentitySkeleton, stakingPriv) 470 require.NoError(t, err) 471 472 signers[identity.NodeID] = verification.NewStakingSigner(me) 473 }).Sort(flow.Canonical[flow.Identity]) 474 475 // utility function which generates a valid timeout for every signer 476 createTimeouts := func(participants flow.IdentitySkeletonList, view uint64, newestQC *flow.QuorumCertificate, lastViewTC *flow.TimeoutCertificate) []*model.TimeoutObject { 477 timeouts := make([]*model.TimeoutObject, 0, len(participants)) 478 for _, signer := range participants { 479 timeout, err := signers[signer.NodeID].CreateTimeout(view, newestQC, lastViewTC) 480 require.NoError(t, err) 481 timeouts = append(timeouts, timeout) 482 } 483 return timeouts 484 } 485 486 leader := stakingSigners[0] 487 488 view := uint64(rand.Uint32() + 100) 489 block := helper.MakeBlock(helper.WithBlockView(view-1), 490 helper.WithBlockProposer(leader.NodeID)) 491 492 stakingSignersSkeleton := stakingSigners.ToSkeleton() 493 494 committee := mocks.NewDynamicCommittee(t) 495 committee.On("IdentitiesByEpoch", mock.Anything).Return(stakingSignersSkeleton, nil) 496 committee.On("IdentitiesByBlock", mock.Anything).Return(stakingSigners, nil) 497 committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(stakingSignersSkeleton.TotalWeight()), nil) 498 committee.On("TimeoutThresholdForView", mock.Anything).Return(committees.WeightThresholdToTimeout(stakingSignersSkeleton.TotalWeight()), nil) 499 500 // create first QC for view N-1, this will be our olderQC 501 olderQC := createRealQC(t, committee, stakingSignersSkeleton, signers, block) 502 // now create a second QC for view N, this will be our newest QC 503 nextBlock := helper.MakeBlock( 504 helper.WithBlockView(view), 505 helper.WithBlockProposer(leader.NodeID), 506 helper.WithBlockQC(olderQC)) 507 newestQC := createRealQC(t, committee, stakingSignersSkeleton, signers, nextBlock) 508 509 // At this point we have created two QCs for round N-1 and N. 510 // Next step is create a TC for view N. 511 512 // create verifier that will do crypto checks of created TC 513 verifier := verification.NewStakingVerifier() 514 // create validator which will do compliance and crypto checks of created TC 515 validator := hotstuffvalidator.New(committee, verifier) 516 517 var lastViewTC *flow.TimeoutCertificate 518 onTCCreated := func(args mock.Arguments) { 519 tc := args.Get(0).(*flow.TimeoutCertificate) 520 // check if resulted TC is valid 521 err := validator.ValidateTC(tc) 522 require.NoError(t, err) 523 lastViewTC = tc 524 } 525 526 aggregator, err := NewTimeoutSignatureAggregator(view, stakingSignersSkeleton, msig.CollectorTimeoutTag) 527 require.NoError(t, err) 528 529 notifier := mocks.NewTimeoutCollectorConsumer(t) 530 notifier.On("OnPartialTcCreated", view, olderQC, (*flow.TimeoutCertificate)(nil)).Return().Once() 531 notifier.On("OnTcConstructedFromTimeouts", mock.Anything).Run(onTCCreated).Return().Once() 532 processor, err := NewTimeoutProcessor(unittest.Logger(), committee, validator, aggregator, notifier) 533 require.NoError(t, err) 534 535 // last view was successful, no lastViewTC in this case 536 timeouts := createTimeouts(stakingSignersSkeleton, view, olderQC, nil) 537 for _, timeout := range timeouts { 538 err := processor.Process(timeout) 539 require.NoError(t, err) 540 } 541 542 notifier.AssertExpectations(t) 543 544 // at this point we have created QCs for view N-1 and N additionally a TC for view N, we can create TC for view N+1 545 // with timeout objects containing both QC and TC for view N 546 547 aggregator, err = NewTimeoutSignatureAggregator(view+1, stakingSignersSkeleton, msig.CollectorTimeoutTag) 548 require.NoError(t, err) 549 550 notifier = mocks.NewTimeoutCollectorConsumer(t) 551 notifier.On("OnPartialTcCreated", view+1, newestQC, (*flow.TimeoutCertificate)(nil)).Return().Once() 552 notifier.On("OnTcConstructedFromTimeouts", mock.Anything).Run(onTCCreated).Return().Once() 553 processor, err = NewTimeoutProcessor(unittest.Logger(), committee, validator, aggregator, notifier) 554 require.NoError(t, err) 555 556 // part of committee will use QC, another part TC, this will result in aggregated signature consisting 557 // of two types of messages with views N-1 and N representing the newest QC known to replicas. 558 timeoutsWithQC := createTimeouts(stakingSignersSkeleton[:len(stakingSignersSkeleton)/2], view+1, newestQC, nil) 559 timeoutsWithTC := createTimeouts(stakingSignersSkeleton[len(stakingSignersSkeleton)/2:], view+1, olderQC, lastViewTC) 560 timeouts = append(timeoutsWithQC, timeoutsWithTC...) 561 for _, timeout := range timeouts { 562 err := processor.Process(timeout) 563 require.NoError(t, err) 564 } 565 566 notifier.AssertExpectations(t) 567 } 568 569 // createRealQC is a helper function which generates a properly signed QC with real signatures for given block. 570 func createRealQC( 571 t *testing.T, 572 committee hotstuff.DynamicCommittee, 573 signers flow.IdentitySkeletonList, 574 signerObjects map[flow.Identifier]*verification.StakingSigner, 575 block *model.Block, 576 ) *flow.QuorumCertificate { 577 leader := signers[0] 578 proposal, err := signerObjects[leader.NodeID].CreateProposal(block) 579 require.NoError(t, err) 580 581 var createdQC *flow.QuorumCertificate 582 onQCCreated := func(qc *flow.QuorumCertificate) { 583 createdQC = qc 584 } 585 586 voteProcessorFactory := votecollector.NewStakingVoteProcessorFactory(committee, onQCCreated) 587 voteProcessor, err := voteProcessorFactory.Create(unittest.Logger(), proposal) 588 require.NoError(t, err) 589 590 for _, signer := range signers[1:] { 591 vote, err := signerObjects[signer.NodeID].CreateVote(block) 592 require.NoError(t, err) 593 err = voteProcessor.Process(vote) 594 require.NoError(t, err) 595 } 596 597 require.NotNil(t, createdQC, "vote processor must create a valid QC at this point") 598 return createdQC 599 }