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  }