github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/eventhandler/event_handler_test.go (about) 1 package eventhandler 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "testing" 9 "time" 10 11 "github.com/rs/zerolog" 12 "github.com/rs/zerolog/log" 13 "github.com/stretchr/testify/mock" 14 "github.com/stretchr/testify/require" 15 "github.com/stretchr/testify/suite" 16 17 "github.com/onflow/flow-go/consensus/hotstuff" 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 "github.com/onflow/flow-go/consensus/hotstuff/pacemaker" 22 "github.com/onflow/flow-go/consensus/hotstuff/pacemaker/timeout" 23 "github.com/onflow/flow-go/model/flow" 24 "github.com/onflow/flow-go/utils/unittest" 25 ) 26 27 const ( 28 minRepTimeout float64 = 100.0 // Milliseconds 29 maxRepTimeout float64 = 600.0 // Milliseconds 30 multiplicativeIncrease float64 = 1.5 // multiplicative factor 31 happyPathMaxRoundFailures uint64 = 6 // number of failed rounds before first timeout increase 32 ) 33 34 // TestPaceMaker is a real pacemaker module with logging for view changes 35 type TestPaceMaker struct { 36 hotstuff.PaceMaker 37 } 38 39 var _ hotstuff.PaceMaker = (*TestPaceMaker)(nil) 40 41 func NewTestPaceMaker( 42 timeoutController *timeout.Controller, 43 proposalDelayProvider hotstuff.ProposalDurationProvider, 44 notifier hotstuff.Consumer, 45 persist hotstuff.Persister, 46 ) *TestPaceMaker { 47 p, err := pacemaker.New(timeoutController, proposalDelayProvider, notifier, persist) 48 if err != nil { 49 panic(err) 50 } 51 return &TestPaceMaker{p} 52 } 53 54 func (p *TestPaceMaker) ProcessQC(qc *flow.QuorumCertificate) (*model.NewViewEvent, error) { 55 oldView := p.CurView() 56 newView, err := p.PaceMaker.ProcessQC(qc) 57 log.Info().Msgf("pacemaker.ProcessQC old view: %v, new view: %v\n", oldView, p.CurView()) 58 return newView, err 59 } 60 61 func (p *TestPaceMaker) ProcessTC(tc *flow.TimeoutCertificate) (*model.NewViewEvent, error) { 62 oldView := p.CurView() 63 newView, err := p.PaceMaker.ProcessTC(tc) 64 log.Info().Msgf("pacemaker.ProcessTC old view: %v, new view: %v\n", oldView, p.CurView()) 65 return newView, err 66 } 67 68 func (p *TestPaceMaker) NewestQC() *flow.QuorumCertificate { 69 return p.PaceMaker.NewestQC() 70 } 71 72 func (p *TestPaceMaker) LastViewTC() *flow.TimeoutCertificate { 73 return p.PaceMaker.LastViewTC() 74 } 75 76 // using a real pacemaker for testing event handler 77 func initPaceMaker(t require.TestingT, ctx context.Context, livenessData *hotstuff.LivenessData) hotstuff.PaceMaker { 78 notifier := &mocks.Consumer{} 79 tc, err := timeout.NewConfig(time.Duration(minRepTimeout*1e6), time.Duration(maxRepTimeout*1e6), multiplicativeIncrease, happyPathMaxRoundFailures, time.Duration(maxRepTimeout*1e6)) 80 require.NoError(t, err) 81 persist := &mocks.Persister{} 82 persist.On("PutLivenessData", mock.Anything).Return(nil).Maybe() 83 persist.On("GetLivenessData").Return(livenessData, nil).Once() 84 pm := NewTestPaceMaker(timeout.NewController(tc), pacemaker.NoProposalDelay(), notifier, persist) 85 notifier.On("OnStartingTimeout", mock.Anything).Return() 86 notifier.On("OnQcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return() 87 notifier.On("OnTcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return() 88 notifier.On("OnViewChange", mock.Anything, mock.Anything).Maybe() 89 pm.Start(ctx) 90 return pm 91 } 92 93 // Committee mocks hotstuff.DynamicCommittee and allows to easily control leader for some view. 94 type Committee struct { 95 *mocks.Replicas 96 // to mock I'm the leader of a certain view, add the view into the keys of leaders field 97 leaders map[uint64]struct{} 98 } 99 100 func NewCommittee(t *testing.T) *Committee { 101 committee := &Committee{ 102 Replicas: mocks.NewReplicas(t), 103 leaders: make(map[uint64]struct{}), 104 } 105 self := unittest.IdentityFixture(unittest.WithNodeID(flow.Identifier{0x01})) 106 committee.On("LeaderForView", mock.Anything).Return(func(view uint64) flow.Identifier { 107 _, isLeader := committee.leaders[view] 108 if isLeader { 109 return self.NodeID 110 } 111 return flow.Identifier{0x00} 112 }, func(view uint64) error { 113 return nil 114 }).Maybe() 115 116 committee.On("Self").Return(self.NodeID).Maybe() 117 118 return committee 119 } 120 121 // The SafetyRules mock will not vote for any block unless the block's ID exists in votable field's key 122 type SafetyRules struct { 123 *mocks.SafetyRules 124 votable map[flow.Identifier]struct{} 125 } 126 127 func NewSafetyRules(t *testing.T) *SafetyRules { 128 safetyRules := &SafetyRules{ 129 SafetyRules: mocks.NewSafetyRules(t), 130 votable: make(map[flow.Identifier]struct{}), 131 } 132 133 // SafetyRules will not vote for any block, unless the blockID exists in votable map 134 safetyRules.On("ProduceVote", mock.Anything, mock.Anything).Return( 135 func(block *model.Proposal, _ uint64) *model.Vote { 136 _, ok := safetyRules.votable[block.Block.BlockID] 137 if !ok { 138 return nil 139 } 140 return createVote(block.Block) 141 }, 142 func(block *model.Proposal, _ uint64) error { 143 _, ok := safetyRules.votable[block.Block.BlockID] 144 if !ok { 145 return model.NewNoVoteErrorf("block not found") 146 } 147 return nil 148 }).Maybe() 149 150 safetyRules.On("ProduceTimeout", mock.Anything, mock.Anything, mock.Anything).Return( 151 func(curView uint64, newestQC *flow.QuorumCertificate, lastViewTC *flow.TimeoutCertificate) *model.TimeoutObject { 152 return helper.TimeoutObjectFixture(func(timeout *model.TimeoutObject) { 153 timeout.View = curView 154 timeout.NewestQC = newestQC 155 timeout.LastViewTC = lastViewTC 156 }) 157 }, 158 func(uint64, *flow.QuorumCertificate, *flow.TimeoutCertificate) error { return nil }).Maybe() 159 160 return safetyRules 161 } 162 163 // Forks mock allows to customize the AddBlock function by specifying the addProposal callbacks 164 type Forks struct { 165 *mocks.Forks 166 // proposals stores all the proposals that have been added to the forks 167 proposals map[flow.Identifier]*model.Block 168 finalized uint64 169 t require.TestingT 170 // addProposal is to customize the logic to change finalized view 171 addProposal func(block *model.Block) error 172 } 173 174 func NewForks(t *testing.T, finalized uint64) *Forks { 175 f := &Forks{ 176 Forks: mocks.NewForks(t), 177 proposals: make(map[flow.Identifier]*model.Block), 178 finalized: finalized, 179 } 180 181 f.On("AddValidatedBlock", mock.Anything).Return(func(proposal *model.Block) error { 182 log.Info().Msgf("forks.AddValidatedBlock received Proposal for view: %v, QC: %v\n", proposal.View, proposal.QC.View) 183 return f.addProposal(proposal) 184 }).Maybe() 185 186 f.On("FinalizedView").Return(func() uint64 { 187 return f.finalized 188 }).Maybe() 189 190 f.On("GetBlock", mock.Anything).Return(func(blockID flow.Identifier) *model.Block { 191 b := f.proposals[blockID] 192 return b 193 }, func(blockID flow.Identifier) bool { 194 b, ok := f.proposals[blockID] 195 var view uint64 196 if ok { 197 view = b.View 198 } 199 log.Info().Msgf("forks.GetBlock found %v: view: %v\n", ok, view) 200 return ok 201 }).Maybe() 202 203 f.On("GetBlocksForView", mock.Anything).Return(func(view uint64) []*model.Block { 204 proposals := make([]*model.Block, 0) 205 for _, b := range f.proposals { 206 if b.View == view { 207 proposals = append(proposals, b) 208 } 209 } 210 log.Info().Msgf("forks.GetBlocksForView found %v block(s) for view %v\n", len(proposals), view) 211 return proposals 212 }).Maybe() 213 214 f.addProposal = func(block *model.Block) error { 215 f.proposals[block.BlockID] = block 216 if block.QC == nil { 217 panic(fmt.Sprintf("block has no QC: %v", block.View)) 218 } 219 return nil 220 } 221 222 return f 223 } 224 225 // BlockProducer mock will always make a valid block 226 type BlockProducer struct { 227 proposerID flow.Identifier 228 } 229 230 func (b *BlockProducer) MakeBlockProposal(view uint64, qc *flow.QuorumCertificate, lastViewTC *flow.TimeoutCertificate) (*flow.Header, error) { 231 return model.ProposalToFlow(&model.Proposal{ 232 Block: helper.MakeBlock( 233 helper.WithBlockView(view), 234 helper.WithBlockQC(qc), 235 helper.WithBlockProposer(b.proposerID), 236 ), 237 LastViewTC: lastViewTC, 238 }), nil 239 } 240 241 func TestEventHandler(t *testing.T) { 242 suite.Run(t, new(EventHandlerSuite)) 243 } 244 245 // EventHandlerSuite contains mocked state for testing event handler under different scenarios. 246 type EventHandlerSuite struct { 247 suite.Suite 248 249 eventhandler *EventHandler 250 251 paceMaker hotstuff.PaceMaker 252 forks *Forks 253 persist *mocks.Persister 254 blockProducer *BlockProducer 255 committee *Committee 256 notifier *mocks.Consumer 257 safetyRules *SafetyRules 258 259 initView uint64 // the current view at the beginning of the test case 260 endView uint64 // the expected current view at the end of the test case 261 parentProposal *model.Proposal 262 votingProposal *model.Proposal 263 qc *flow.QuorumCertificate 264 tc *flow.TimeoutCertificate 265 newview *model.NewViewEvent 266 ctx context.Context 267 stop context.CancelFunc 268 } 269 270 func (es *EventHandlerSuite) SetupTest() { 271 finalized := uint64(3) 272 273 es.parentProposal = createProposal(4, 3) 274 newestQC := createQC(es.parentProposal.Block) 275 276 livenessData := &hotstuff.LivenessData{ 277 CurrentView: newestQC.View + 1, 278 NewestQC: newestQC, 279 } 280 281 es.ctx, es.stop = context.WithCancel(context.Background()) 282 283 es.committee = NewCommittee(es.T()) 284 es.paceMaker = initPaceMaker(es.T(), es.ctx, livenessData) 285 es.forks = NewForks(es.T(), finalized) 286 es.persist = mocks.NewPersister(es.T()) 287 es.persist.On("PutStarted", mock.Anything).Return(nil).Maybe() 288 es.blockProducer = &BlockProducer{proposerID: es.committee.Self()} 289 es.safetyRules = NewSafetyRules(es.T()) 290 es.notifier = mocks.NewConsumer(es.T()) 291 es.notifier.On("OnEventProcessed").Maybe() 292 es.notifier.On("OnEnteringView", mock.Anything, mock.Anything).Maybe() 293 es.notifier.On("OnStart", mock.Anything).Maybe() 294 es.notifier.On("OnReceiveProposal", mock.Anything, mock.Anything).Maybe() 295 es.notifier.On("OnReceiveQc", mock.Anything, mock.Anything).Maybe() 296 es.notifier.On("OnReceiveTc", mock.Anything, mock.Anything).Maybe() 297 es.notifier.On("OnPartialTc", mock.Anything, mock.Anything).Maybe() 298 es.notifier.On("OnLocalTimeout", mock.Anything).Maybe() 299 es.notifier.On("OnCurrentViewDetails", mock.Anything, mock.Anything, mock.Anything).Maybe() 300 301 eventhandler, err := NewEventHandler( 302 zerolog.New(os.Stderr), 303 es.paceMaker, 304 es.blockProducer, 305 es.forks, 306 es.persist, 307 es.committee, 308 es.safetyRules, 309 es.notifier) 310 require.NoError(es.T(), err) 311 312 es.eventhandler = eventhandler 313 314 es.initView = livenessData.CurrentView 315 es.endView = livenessData.CurrentView 316 // voting block is a block for the current view, which will trigger view change 317 es.votingProposal = createProposal(es.paceMaker.CurView(), es.parentProposal.Block.View) 318 es.qc = helper.MakeQC(helper.WithQCBlock(es.votingProposal.Block)) 319 320 // create a TC that will trigger view change for current view, based on newest QC 321 es.tc = helper.MakeTC(helper.WithTCView(es.paceMaker.CurView()), 322 helper.WithTCNewestQC(es.votingProposal.Block.QC)) 323 es.newview = &model.NewViewEvent{ 324 View: es.votingProposal.Block.View + 1, // the vote for the voting proposals will trigger a view change to the next view 325 } 326 327 // add es.parentProposal into forks, otherwise we won't vote or propose based on it's QC sicne the parent is unknown 328 es.forks.proposals[es.parentProposal.Block.BlockID] = es.parentProposal.Block 329 } 330 331 // TestStartNewView_ParentProposalNotFound tests next scenario: constructed TC, it contains NewestQC that references block that we 332 // don't know about, proposal can't be generated because we can't be sure that resulting block payload is valid. 333 func (es *EventHandlerSuite) TestStartNewView_ParentProposalNotFound() { 334 newestQC := helper.MakeQC(helper.WithQCView(es.initView + 10)) 335 tc := helper.MakeTC(helper.WithTCView(newestQC.View+1), 336 helper.WithTCNewestQC(newestQC)) 337 338 es.endView = tc.View + 1 339 340 // I'm leader for next block 341 es.committee.leaders[es.endView] = struct{}{} 342 343 err := es.eventhandler.OnReceiveTc(tc) 344 require.NoError(es.T(), err) 345 346 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 347 es.forks.AssertCalled(es.T(), "GetBlock", newestQC.BlockID) 348 es.notifier.AssertNotCalled(es.T(), "OnOwnProposal", mock.Anything, mock.Anything) 349 } 350 351 // TestOnReceiveProposal_StaleProposal test that proposals lower than finalized view are not processed at all 352 // we are not interested in this data because we already performed finalization of that height. 353 func (es *EventHandlerSuite) TestOnReceiveProposal_StaleProposal() { 354 proposal := createProposal(es.forks.FinalizedView()-1, es.forks.FinalizedView()-2) 355 err := es.eventhandler.OnReceiveProposal(proposal) 356 require.NoError(es.T(), err) 357 es.forks.AssertNotCalled(es.T(), "AddBlock", proposal) 358 } 359 360 // TestOnReceiveProposal_QCOlderThanCurView tests scenario: received a valid proposal with QC that has older view, 361 // the proposal's QC shouldn't trigger view change. 362 func (es *EventHandlerSuite) TestOnReceiveProposal_QCOlderThanCurView() { 363 proposal := createProposal(es.initView-1, es.initView-2) 364 365 // should not trigger view change 366 err := es.eventhandler.OnReceiveProposal(proposal) 367 require.NoError(es.T(), err) 368 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 369 es.forks.AssertCalled(es.T(), "AddValidatedBlock", proposal.Block) 370 } 371 372 // TestOnReceiveProposal_TCOlderThanCurView tests scenario: received a valid proposal with QC and TC that has older view, 373 // the proposal's QC shouldn't trigger view change. 374 func (es *EventHandlerSuite) TestOnReceiveProposal_TCOlderThanCurView() { 375 proposal := createProposal(es.initView-1, es.initView-3) 376 proposal.LastViewTC = helper.MakeTC(helper.WithTCView(proposal.Block.View-1), helper.WithTCNewestQC(proposal.Block.QC)) 377 378 // should not trigger view change 379 err := es.eventhandler.OnReceiveProposal(proposal) 380 require.NoError(es.T(), err) 381 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 382 es.forks.AssertCalled(es.T(), "AddValidatedBlock", proposal.Block) 383 } 384 385 // TestOnReceiveProposal_NoVote tests scenario: received a valid proposal for cur view, but not a safe node to vote, and I'm the next leader 386 // should not vote. 387 func (es *EventHandlerSuite) TestOnReceiveProposal_NoVote() { 388 proposal := createProposal(es.initView, es.initView-1) 389 390 // I'm the next leader 391 es.committee.leaders[es.initView+1] = struct{}{} 392 // no vote for this proposal 393 err := es.eventhandler.OnReceiveProposal(proposal) 394 require.NoError(es.T(), err) 395 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 396 es.forks.AssertCalled(es.T(), "AddValidatedBlock", proposal.Block) 397 } 398 399 // TestOnReceiveProposal_NoVote_ParentProposalNotFound tests scenario: received a valid proposal for cur view, no parent for this proposal found 400 // should not vote. 401 func (es *EventHandlerSuite) TestOnReceiveProposal_NoVote_ParentProposalNotFound() { 402 proposal := createProposal(es.initView, es.initView-1) 403 404 // remove parent from known proposals 405 delete(es.forks.proposals, proposal.Block.QC.BlockID) 406 407 // no vote for this proposal, no parent found 408 err := es.eventhandler.OnReceiveProposal(proposal) 409 require.Error(es.T(), err) 410 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 411 es.forks.AssertCalled(es.T(), "AddValidatedBlock", proposal.Block) 412 } 413 414 // TestOnReceiveProposal_Vote_NextLeader tests scenario: received a valid proposal for cur view, safe to vote, I'm the next leader 415 // should vote and add vote to VoteAggregator. 416 func (es *EventHandlerSuite) TestOnReceiveProposal_Vote_NextLeader() { 417 proposal := createProposal(es.initView, es.initView-1) 418 419 // I'm the next leader 420 es.committee.leaders[es.initView+1] = struct{}{} 421 422 // proposal is safe to vote 423 es.safetyRules.votable[proposal.Block.BlockID] = struct{}{} 424 425 es.notifier.On("OnOwnVote", proposal.Block.BlockID, proposal.Block.View, mock.Anything, mock.Anything).Once() 426 427 // vote should be created for this proposal 428 err := es.eventhandler.OnReceiveProposal(proposal) 429 require.NoError(es.T(), err) 430 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 431 } 432 433 // TestOnReceiveProposal_Vote_NotNextLeader tests scenario: received a valid proposal for cur view, safe to vote, I'm not the next leader 434 // should vote and send vote to next leader. 435 func (es *EventHandlerSuite) TestOnReceiveProposal_Vote_NotNextLeader() { 436 proposal := createProposal(es.initView, es.initView-1) 437 438 // proposal is safe to vote 439 es.safetyRules.votable[proposal.Block.BlockID] = struct{}{} 440 441 es.notifier.On("OnOwnVote", proposal.Block.BlockID, mock.Anything, mock.Anything, mock.Anything).Once() 442 443 // vote should be created for this proposal 444 err := es.eventhandler.OnReceiveProposal(proposal) 445 require.NoError(es.T(), err) 446 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 447 } 448 449 // TestOnReceiveProposal_ProposeAfterReceivingTC tests a scenario where we have received TC which advances to view where we are 450 // leader but no proposal can be created because we don't have parent proposal. After receiving missing parent proposal we have 451 // all available data to construct a valid proposal. We need to ensure this. 452 func (es *EventHandlerSuite) TestOnReceiveProposal_ProposeAfterReceivingQC() { 453 454 qc := es.qc 455 456 // first process QC this should advance view 457 err := es.eventhandler.OnReceiveQc(qc) 458 require.NoError(es.T(), err) 459 require.Equal(es.T(), qc.View+1, es.paceMaker.CurView(), "expect a view change") 460 es.notifier.AssertNotCalled(es.T(), "OnOwnProposal", mock.Anything, mock.Anything) 461 462 // we are leader for current view 463 es.committee.leaders[es.paceMaker.CurView()] = struct{}{} 464 465 es.notifier.On("OnOwnProposal", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 466 header, ok := args[0].(*flow.Header) 467 require.True(es.T(), ok) 468 // it should broadcast a header as the same as current view 469 require.Equal(es.T(), es.paceMaker.CurView(), header.View) 470 }).Once() 471 472 // processing this proposal shouldn't trigger view change since we have already seen QC. 473 // we have used QC to advance rounds, but no proposal was made because we were missing parent block 474 // when we have received parent block we can try proposing again. 475 err = es.eventhandler.OnReceiveProposal(es.votingProposal) 476 require.NoError(es.T(), err) 477 478 require.Equal(es.T(), qc.View+1, es.paceMaker.CurView(), "expect a view change") 479 } 480 481 // TestOnReceiveProposal_ProposeAfterReceivingTC tests a scenario where we have received TC which advances to view where we are 482 // leader but no proposal can be created because we don't have parent proposal. After receiving missing parent proposal we have 483 // all available data to construct a valid proposal. We need to ensure this. 484 func (es *EventHandlerSuite) TestOnReceiveProposal_ProposeAfterReceivingTC() { 485 486 // TC contains a QC.BlockID == es.votingProposal 487 tc := helper.MakeTC(helper.WithTCView(es.votingProposal.Block.View+1), 488 helper.WithTCNewestQC(es.qc)) 489 490 // first process TC this should advance view 491 err := es.eventhandler.OnReceiveTc(tc) 492 require.NoError(es.T(), err) 493 require.Equal(es.T(), tc.View+1, es.paceMaker.CurView(), "expect a view change") 494 es.notifier.AssertNotCalled(es.T(), "OnOwnProposal", mock.Anything, mock.Anything) 495 496 // we are leader for current view 497 es.committee.leaders[es.paceMaker.CurView()] = struct{}{} 498 499 es.notifier.On("OnOwnProposal", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 500 header, ok := args[0].(*flow.Header) 501 require.True(es.T(), ok) 502 // it should broadcast a header as the same as current view 503 require.Equal(es.T(), es.paceMaker.CurView(), header.View) 504 }).Once() 505 506 // processing this proposal shouldn't trigger view change, since we have already seen QC. 507 // we have used QC to advance rounds, but no proposal was made because we were missing parent block 508 // when we have received parent block we can try proposing again. 509 err = es.eventhandler.OnReceiveProposal(es.votingProposal) 510 require.NoError(es.T(), err) 511 512 require.Equal(es.T(), tc.View+1, es.paceMaker.CurView(), "expect a view change") 513 } 514 515 // TestOnReceiveQc_HappyPath tests that building a QC for current view triggers view change. We are not leader for next 516 // round, so no proposal is expected. 517 func (es *EventHandlerSuite) TestOnReceiveQc_HappyPath() { 518 // voting block exists 519 es.forks.proposals[es.votingProposal.Block.BlockID] = es.votingProposal.Block 520 521 // a qc is built 522 qc := createQC(es.votingProposal.Block) 523 524 // new qc is added to forks 525 // view changed 526 // I'm not the next leader 527 // haven't received block for next view 528 // goes to the new view 529 es.endView++ 530 // not the leader of the newview 531 // don't have block for the newview 532 533 err := es.eventhandler.OnReceiveQc(qc) 534 require.NoError(es.T(), err, "if a vote can trigger a QC to be built,"+ 535 "and the QC triggered a view change, then start new view") 536 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 537 es.notifier.AssertNotCalled(es.T(), "OnOwnProposal", mock.Anything, mock.Anything) 538 } 539 540 // TestOnReceiveQc_FutureView tests that building a QC for future view triggers view change 541 func (es *EventHandlerSuite) TestOnReceiveQc_FutureView() { 542 // voting block exists 543 curView := es.paceMaker.CurView() 544 545 // b1 is for current view 546 // b2 and b3 is for future view, but branched out from the same parent as b1 547 b1 := createProposal(curView, curView-1) 548 b2 := createProposal(curView+1, curView-1) 549 b3 := createProposal(curView+2, curView-1) 550 551 // a qc is built 552 // qc3 is for future view 553 // qc2 is an older than qc3 554 // since vote aggregator can concurrently process votes and build qcs, 555 // we prepare qcs at different view to be processed, and verify the view change. 556 qc1 := createQC(b1.Block) 557 qc2 := createQC(b2.Block) 558 qc3 := createQC(b3.Block) 559 560 // all three proposals are known 561 es.forks.proposals[b1.Block.BlockID] = b1.Block 562 es.forks.proposals[b2.Block.BlockID] = b2.Block 563 es.forks.proposals[b3.Block.BlockID] = b3.Block 564 565 // test that qc for future view should trigger view change 566 err := es.eventhandler.OnReceiveQc(qc3) 567 endView := b3.Block.View + 1 // next view 568 require.NoError(es.T(), err, "if a vote can trigger a QC to be built,"+ 569 "and the QC triggered a view change, then start new view") 570 require.Equal(es.T(), endView, es.paceMaker.CurView(), "incorrect view change") 571 572 // the same qc would not trigger view change 573 err = es.eventhandler.OnReceiveQc(qc3) 574 endView = b3.Block.View + 1 // next view 575 require.NoError(es.T(), err, "same qc should not trigger view change") 576 require.Equal(es.T(), endView, es.paceMaker.CurView(), "incorrect view change") 577 578 // old QCs won't trigger view change 579 err = es.eventhandler.OnReceiveQc(qc2) 580 require.NoError(es.T(), err) 581 require.Equal(es.T(), endView, es.paceMaker.CurView(), "incorrect view change") 582 583 err = es.eventhandler.OnReceiveQc(qc1) 584 require.NoError(es.T(), err) 585 require.Equal(es.T(), endView, es.paceMaker.CurView(), "incorrect view change") 586 } 587 588 // TestOnReceiveQc_NextLeaderProposes tests that after receiving a valid proposal for cur view, and I'm the next leader, 589 // a QC can be built for the block, triggered view change, and I will propose 590 func (es *EventHandlerSuite) TestOnReceiveQc_NextLeaderProposes() { 591 proposal := createProposal(es.initView, es.initView-1) 592 qc := createQC(proposal.Block) 593 // I'm the next leader 594 es.committee.leaders[es.initView+1] = struct{}{} 595 // qc triggered view change 596 es.endView++ 597 // I'm the leader of cur view (7) 598 // I'm not the leader of next view (8), trigger view change 599 600 err := es.eventhandler.OnReceiveProposal(proposal) 601 require.NoError(es.T(), err) 602 603 es.notifier.On("OnOwnProposal", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 604 header, ok := args[0].(*flow.Header) 605 require.True(es.T(), ok) 606 // it should broadcast a header as the same as endView 607 require.Equal(es.T(), es.endView, header.View) 608 }).Once() 609 610 // after receiving proposal build QC and deliver it to event handler 611 err = es.eventhandler.OnReceiveQc(qc) 612 require.NoError(es.T(), err) 613 614 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 615 es.forks.AssertCalled(es.T(), "AddValidatedBlock", proposal.Block) 616 } 617 618 // TestOnReceiveQc_ProposeOnce tests that after constructing proposal we don't attempt to create another 619 // proposal for same view. 620 func (es *EventHandlerSuite) TestOnReceiveQc_ProposeOnce() { 621 // I'm the next leader 622 es.committee.leaders[es.initView+1] = struct{}{} 623 624 es.endView++ 625 626 es.notifier.On("OnOwnProposal", mock.Anything, mock.Anything).Once() 627 628 err := es.eventhandler.OnReceiveProposal(es.votingProposal) 629 require.NoError(es.T(), err) 630 631 // constructing QC triggers making block proposal 632 err = es.eventhandler.OnReceiveQc(es.qc) 633 require.NoError(es.T(), err) 634 635 // receiving same proposal again triggers proposing logic 636 err = es.eventhandler.OnReceiveProposal(es.votingProposal) 637 require.NoError(es.T(), err) 638 639 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 640 es.notifier.AssertNumberOfCalls(es.T(), "OnOwnProposal", 1) 641 } 642 643 // TestOnTCConstructed_HappyPath tests that building a TC for current view triggers view change 644 func (es *EventHandlerSuite) TestOnReceiveTc_HappyPath() { 645 // voting block exists 646 es.forks.proposals[es.votingProposal.Block.BlockID] = es.votingProposal.Block 647 648 // a tc is built 649 tc := helper.MakeTC(helper.WithTCView(es.initView), helper.WithTCNewestQC(es.votingProposal.Block.QC)) 650 651 // expect a view change 652 es.endView++ 653 654 err := es.eventhandler.OnReceiveTc(tc) 655 require.NoError(es.T(), err, "TC should trigger a view change and start of new view") 656 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 657 } 658 659 // TestOnTCConstructed_NextLeaderProposes tests that after receiving TC and advancing view we as next leader create a proposal 660 // and broadcast it 661 func (es *EventHandlerSuite) TestOnReceiveTc_NextLeaderProposes() { 662 es.committee.leaders[es.tc.View+1] = struct{}{} 663 es.endView++ 664 665 es.notifier.On("OnOwnProposal", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 666 header, ok := args[0].(*flow.Header) 667 require.True(es.T(), ok) 668 // it should broadcast a header as the same as endView 669 require.Equal(es.T(), es.endView, header.View) 670 671 // proposed block should contain valid newest QC and lastViewTC 672 expectedNewestQC := es.paceMaker.NewestQC() 673 proposal := model.ProposalFromFlow(header) 674 require.Equal(es.T(), expectedNewestQC, proposal.Block.QC) 675 require.Equal(es.T(), es.paceMaker.LastViewTC(), proposal.LastViewTC) 676 }).Once() 677 678 err := es.eventhandler.OnReceiveTc(es.tc) 679 require.NoError(es.T(), err) 680 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "TC didn't trigger view change") 681 } 682 683 // TestOnTimeout tests that event handler produces TimeoutObject and broadcasts it to other members of consensus 684 // committee. Additionally, It has to contribute TimeoutObject to timeout aggregation process by sending it to TimeoutAggregator. 685 func (es *EventHandlerSuite) TestOnTimeout() { 686 es.notifier.On("OnOwnTimeout", mock.Anything).Run(func(args mock.Arguments) { 687 timeoutObject, ok := args[0].(*model.TimeoutObject) 688 require.True(es.T(), ok) 689 // it should broadcast a TO with same view as endView 690 require.Equal(es.T(), es.endView, timeoutObject.View) 691 }).Once() 692 693 err := es.eventhandler.OnLocalTimeout() 694 require.NoError(es.T(), err) 695 696 // TimeoutObject shouldn't trigger view change 697 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 698 } 699 700 // TestOnTimeout_SanityChecks tests a specific scenario where pacemaker have seen both QC and TC for previous view 701 // and EventHandler tries to produce a timeout object, such timeout object is invalid if both QC and TC is present, we 702 // need to make sure that EventHandler filters out TC for last view if we know about QC for same view. 703 func (es *EventHandlerSuite) TestOnTimeout_SanityChecks() { 704 // voting block exists 705 es.forks.proposals[es.votingProposal.Block.BlockID] = es.votingProposal.Block 706 707 // a tc is built 708 tc := helper.MakeTC(helper.WithTCView(es.initView), helper.WithTCNewestQC(es.votingProposal.Block.QC)) 709 710 // expect a view change 711 es.endView++ 712 713 err := es.eventhandler.OnReceiveTc(tc) 714 require.NoError(es.T(), err, "TC should trigger a view change and start of new view") 715 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 716 717 // receive a QC for the same view as the TC 718 qc := helper.MakeQC(helper.WithQCView(tc.View)) 719 err = es.eventhandler.OnReceiveQc(qc) 720 require.NoError(es.T(), err) 721 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "QC shouldn't trigger view change") 722 require.Equal(es.T(), tc, es.paceMaker.LastViewTC(), "invalid last view TC") 723 require.Equal(es.T(), qc, es.paceMaker.NewestQC(), "invalid newest QC") 724 725 es.notifier.On("OnOwnTimeout", mock.Anything).Run(func(args mock.Arguments) { 726 timeoutObject, ok := args[0].(*model.TimeoutObject) 727 require.True(es.T(), ok) 728 require.Equal(es.T(), es.endView, timeoutObject.View) 729 require.Equal(es.T(), qc, timeoutObject.NewestQC) 730 require.Nil(es.T(), timeoutObject.LastViewTC) 731 }).Once() 732 733 err = es.eventhandler.OnLocalTimeout() 734 require.NoError(es.T(), err) 735 } 736 737 // TestOnTimeout_ReplicaEjected tests that EventHandler correctly handles possible errors from SafetyRules and doesn't broadcast 738 // timeout objects when replica is ejected. 739 func (es *EventHandlerSuite) TestOnTimeout_ReplicaEjected() { 740 es.Run("no-timeout", func() { 741 *es.safetyRules.SafetyRules = *mocks.NewSafetyRules(es.T()) 742 es.safetyRules.On("ProduceTimeout", mock.Anything, mock.Anything, mock.Anything).Return(nil, model.NewNoTimeoutErrorf("")) 743 err := es.eventhandler.OnLocalTimeout() 744 require.NoError(es.T(), err, "should be handled as sentinel error") 745 }) 746 es.Run("create-timeout-exception", func() { 747 *es.safetyRules.SafetyRules = *mocks.NewSafetyRules(es.T()) 748 exception := errors.New("produce-timeout-exception") 749 es.safetyRules.On("ProduceTimeout", mock.Anything, mock.Anything, mock.Anything).Return(nil, exception) 750 err := es.eventhandler.OnLocalTimeout() 751 require.ErrorIs(es.T(), err, exception, "expect a wrapped exception") 752 }) 753 es.notifier.AssertNotCalled(es.T(), "OnOwnTimeout", mock.Anything) 754 } 755 756 // Test100Timeout tests that receiving 100 TCs for increasing views advances rounds 757 func (es *EventHandlerSuite) Test100Timeout() { 758 for i := 0; i < 100; i++ { 759 tc := helper.MakeTC(helper.WithTCView(es.initView + uint64(i))) 760 err := es.eventhandler.OnReceiveTc(tc) 761 es.endView++ 762 require.NoError(es.T(), err) 763 } 764 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 765 } 766 767 // TestLeaderBuild100Blocks tests scenario where leader builds 100 proposals one after another 768 func (es *EventHandlerSuite) TestLeaderBuild100Blocks() { 769 // I'm the leader for the first view 770 es.committee.leaders[es.initView] = struct{}{} 771 772 totalView := 100 773 for i := 0; i < totalView; i++ { 774 // I'm the leader for 100 views 775 // I'm the next leader 776 es.committee.leaders[es.initView+uint64(i+1)] = struct{}{} 777 // I can build qc for all 100 views 778 proposal := createProposal(es.initView+uint64(i), es.initView+uint64(i)-1) 779 qc := createQC(proposal.Block) 780 781 // for first proposal we need to store the parent otherwise it won't be voted for 782 if i == 0 { 783 parentBlock := helper.MakeBlock(func(block *model.Block) { 784 block.BlockID = proposal.Block.QC.BlockID 785 block.View = proposal.Block.QC.View 786 }) 787 es.forks.proposals[parentBlock.BlockID] = parentBlock 788 } 789 790 es.safetyRules.votable[proposal.Block.BlockID] = struct{}{} 791 // should trigger 100 view change 792 es.endView++ 793 794 es.notifier.On("OnOwnProposal", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 795 header, ok := args[0].(*flow.Header) 796 require.True(es.T(), ok) 797 require.Equal(es.T(), proposal.Block.View+1, header.View) 798 }).Once() 799 es.notifier.On("OnOwnVote", proposal.Block.BlockID, proposal.Block.View, mock.Anything, mock.Anything).Once() 800 801 err := es.eventhandler.OnReceiveProposal(proposal) 802 require.NoError(es.T(), err) 803 err = es.eventhandler.OnReceiveQc(qc) 804 require.NoError(es.T(), err) 805 } 806 807 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 808 require.Equal(es.T(), totalView, (len(es.forks.proposals)-1)/2) 809 } 810 811 // TestFollowerFollows100Blocks tests scenario where follower receives 100 proposals one after another 812 func (es *EventHandlerSuite) TestFollowerFollows100Blocks() { 813 // add parent proposal otherwise we can't propose 814 parentProposal := createProposal(es.initView, es.initView-1) 815 es.forks.proposals[parentProposal.Block.BlockID] = parentProposal.Block 816 for i := 0; i < 100; i++ { 817 // create each proposal as if they are created by some leader 818 proposal := createProposal(es.initView+uint64(i)+1, es.initView+uint64(i)) 819 // as a follower, I receive these proposals 820 err := es.eventhandler.OnReceiveProposal(proposal) 821 require.NoError(es.T(), err) 822 es.endView++ 823 } 824 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 825 require.Equal(es.T(), 100, len(es.forks.proposals)-2) 826 } 827 828 // TestFollowerReceives100Forks tests scenario where follower receives 100 forks built on top of the same block 829 func (es *EventHandlerSuite) TestFollowerReceives100Forks() { 830 for i := 0; i < 100; i++ { 831 // create each proposal as if they are created by some leader 832 proposal := createProposal(es.initView+uint64(i)+1, es.initView-1) 833 proposal.LastViewTC = helper.MakeTC(helper.WithTCView(es.initView+uint64(i)), 834 helper.WithTCNewestQC(proposal.Block.QC)) 835 // expect a view change since fork can be made only if last view has ended with TC. 836 es.endView++ 837 // as a follower, I receive these proposals 838 err := es.eventhandler.OnReceiveProposal(proposal) 839 require.NoError(es.T(), err) 840 } 841 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 842 require.Equal(es.T(), 100, len(es.forks.proposals)-1) 843 } 844 845 // TestStart_ProposeOnce tests that after starting event handler we don't create proposal in case we have already proposed 846 // for this view. 847 func (es *EventHandlerSuite) TestStart_ProposeOnce() { 848 // I'm the next leader 849 es.committee.leaders[es.initView+1] = struct{}{} 850 es.endView++ 851 852 // STEP 1: simulating events _before_ a crash: EventHandler receives proposal and then a QC for the proposal (from VoteAggregator) 853 es.notifier.On("OnOwnProposal", mock.Anything, mock.Anything).Once() 854 err := es.eventhandler.OnReceiveProposal(es.votingProposal) 855 require.NoError(es.T(), err) 856 857 // constructing QC triggers making block proposal 858 err = es.eventhandler.OnReceiveQc(es.qc) 859 require.NoError(es.T(), err) 860 es.notifier.AssertNumberOfCalls(es.T(), "OnOwnProposal", 1) 861 862 // Here, a hypothetical crash would happen. 863 // During crash recovery, Forks and PaceMaker are recovered to have exactly the same in-memory state as before 864 // Start triggers proposing logic. But as our own proposal for the view is already in Forks, we should not propose again. 865 err = es.eventhandler.Start(es.ctx) 866 require.NoError(es.T(), err) 867 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 868 869 // assert that broadcast wasn't trigger again, i.e. there should have been only one event `OnOwnProposal` in total 870 es.notifier.AssertNumberOfCalls(es.T(), "OnOwnProposal", 1) 871 } 872 873 // TestCreateProposal_SanityChecks tests that proposing logic performs sanity checks when creating new block proposal. 874 // Specifically it tests a case where TC contains QC which: TC.View == TC.NewestQC.View 875 func (es *EventHandlerSuite) TestCreateProposal_SanityChecks() { 876 // round ended with TC where TC.View == TC.NewestQC.View 877 tc := helper.MakeTC(helper.WithTCView(es.initView), 878 helper.WithTCNewestQC(helper.MakeQC(helper.WithQCBlock(es.votingProposal.Block)))) 879 880 es.forks.proposals[es.votingProposal.Block.BlockID] = es.votingProposal.Block 881 882 // I'm the next leader 883 es.committee.leaders[tc.View+1] = struct{}{} 884 885 es.notifier.On("OnOwnProposal", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 886 header, ok := args[0].(*flow.Header) 887 require.True(es.T(), ok) 888 // we need to make sure that produced proposal contains only QC even if there is TC for previous view as well 889 require.Nil(es.T(), header.LastViewTC) 890 }).Once() 891 892 err := es.eventhandler.OnReceiveTc(tc) 893 require.NoError(es.T(), err) 894 895 require.Equal(es.T(), tc.NewestQC, es.paceMaker.NewestQC()) 896 require.Equal(es.T(), tc, es.paceMaker.LastViewTC()) 897 require.Equal(es.T(), tc.View+1, es.paceMaker.CurView(), "incorrect view change") 898 } 899 900 // TestOnReceiveProposal_ProposalForActiveView tests that when receiving proposal for active we don't attempt to create a proposal 901 // Receiving proposal can trigger proposing logic only in case we have received missing block for past views. 902 func (es *EventHandlerSuite) TestOnReceiveProposal_ProposalForActiveView() { 903 // receive proposal where we are leader, meaning that we have produced this proposal 904 es.committee.leaders[es.votingProposal.Block.View] = struct{}{} 905 906 err := es.eventhandler.OnReceiveProposal(es.votingProposal) 907 require.NoError(es.T(), err) 908 909 es.notifier.AssertNotCalled(es.T(), "OnOwnProposal", mock.Anything, mock.Anything) 910 } 911 912 // TestOnPartialTcCreated_ProducedTimeout tests that when receiving partial TC for active view we will create a timeout object 913 // immediately. 914 func (es *EventHandlerSuite) TestOnPartialTcCreated_ProducedTimeout() { 915 partialTc := &hotstuff.PartialTcCreated{ 916 View: es.initView, 917 NewestQC: es.parentProposal.Block.QC, 918 LastViewTC: nil, 919 } 920 921 es.notifier.On("OnOwnTimeout", mock.Anything).Run(func(args mock.Arguments) { 922 timeoutObject, ok := args[0].(*model.TimeoutObject) 923 require.True(es.T(), ok) 924 // it should broadcast a TO with same view as partialTc.View 925 require.Equal(es.T(), partialTc.View, timeoutObject.View) 926 }).Once() 927 928 err := es.eventhandler.OnPartialTcCreated(partialTc) 929 require.NoError(es.T(), err) 930 931 // partial TC shouldn't trigger view change 932 require.Equal(es.T(), partialTc.View, es.paceMaker.CurView(), "incorrect view change") 933 } 934 935 // TestOnPartialTcCreated_NotActiveView tests that we don't create timeout object if partial TC was delivered for a past, non-current view. 936 // NOTE: it is not possible to receive a partial timeout for a FUTURE view, unless the partial timeout contains 937 // either a QC/TC allowing us to enter that view, therefore that case is not covered here. 938 // See TestOnPartialTcCreated_QcAndTcProcessing instead. 939 func (es *EventHandlerSuite) TestOnPartialTcCreated_NotActiveView() { 940 partialTc := &hotstuff.PartialTcCreated{ 941 View: es.initView - 1, 942 NewestQC: es.parentProposal.Block.QC, 943 } 944 945 err := es.eventhandler.OnPartialTcCreated(partialTc) 946 require.NoError(es.T(), err) 947 948 // partial TC shouldn't trigger view change 949 require.Equal(es.T(), es.initView, es.paceMaker.CurView(), "incorrect view change") 950 // we don't want to create timeout if partial TC was delivered for view different than active one. 951 es.notifier.AssertNotCalled(es.T(), "OnOwnTimeout", mock.Anything) 952 } 953 954 // TestOnPartialTcCreated_QcAndTcProcessing tests that EventHandler processes QC and TC included in hotstuff.PartialTcCreated 955 // data structure. This tests cases like the following example: 956 // * the pacemaker is in view 10 957 // * we observe a partial timeout for view 11 with a QC for view 10 958 // * we should change to view 11 using the QC, then broadcast a timeout for view 11 959 func (es *EventHandlerSuite) TestOnPartialTcCreated_QcAndTcProcessing() { 960 961 testOnPartialTcCreated := func(partialTc *hotstuff.PartialTcCreated) { 962 es.endView++ 963 964 es.notifier.On("OnOwnTimeout", mock.Anything).Run(func(args mock.Arguments) { 965 timeoutObject, ok := args[0].(*model.TimeoutObject) 966 require.True(es.T(), ok) 967 // it should broadcast a TO with same view as partialTc.View 968 require.Equal(es.T(), partialTc.View, timeoutObject.View) 969 }).Once() 970 971 err := es.eventhandler.OnPartialTcCreated(partialTc) 972 require.NoError(es.T(), err) 973 974 require.Equal(es.T(), es.endView, es.paceMaker.CurView(), "incorrect view change") 975 } 976 977 es.Run("qc-triggered-view-change", func() { 978 partialTc := &hotstuff.PartialTcCreated{ 979 View: es.qc.View + 1, 980 NewestQC: es.qc, 981 } 982 testOnPartialTcCreated(partialTc) 983 }) 984 es.Run("tc-triggered-view-change", func() { 985 tc := helper.MakeTC(helper.WithTCView(es.endView), helper.WithTCNewestQC(es.qc)) 986 partialTc := &hotstuff.PartialTcCreated{ 987 View: tc.View + 1, 988 NewestQC: tc.NewestQC, 989 LastViewTC: tc, 990 } 991 testOnPartialTcCreated(partialTc) 992 }) 993 } 994 995 func createBlock(view uint64) *model.Block { 996 blockID := flow.MakeID(struct { 997 BlockID uint64 998 }{ 999 BlockID: view, 1000 }) 1001 return &model.Block{ 1002 BlockID: blockID, 1003 View: view, 1004 } 1005 } 1006 1007 func createBlockWithQC(view uint64, qcview uint64) *model.Block { 1008 block := createBlock(view) 1009 parent := createBlock(qcview) 1010 block.QC = createQC(parent) 1011 return block 1012 } 1013 1014 func createQC(parent *model.Block) *flow.QuorumCertificate { 1015 qc := &flow.QuorumCertificate{ 1016 BlockID: parent.BlockID, 1017 View: parent.View, 1018 SignerIndices: nil, 1019 SigData: nil, 1020 } 1021 return qc 1022 } 1023 1024 func createVote(block *model.Block) *model.Vote { 1025 return &model.Vote{ 1026 View: block.View, 1027 BlockID: block.BlockID, 1028 SignerID: flow.ZeroID, 1029 SigData: nil, 1030 } 1031 } 1032 1033 func createProposal(view uint64, qcview uint64) *model.Proposal { 1034 block := createBlockWithQC(view, qcview) 1035 return &model.Proposal{ 1036 Block: block, 1037 SigData: nil, 1038 } 1039 }