github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/timeoutcollector/timeout_processor_test.go (about)

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