github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/timeoutcollector/timeout_collector_test.go (about) 1 package timeoutcollector 2 3 import ( 4 "errors" 5 "math/rand" 6 "sync" 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/mock" 11 "github.com/stretchr/testify/require" 12 "github.com/stretchr/testify/suite" 13 14 "github.com/onflow/flow-go/consensus/hotstuff/helper" 15 "github.com/onflow/flow-go/consensus/hotstuff/mocks" 16 "github.com/onflow/flow-go/consensus/hotstuff/model" 17 "github.com/onflow/flow-go/model/flow" 18 "github.com/onflow/flow-go/utils/unittest" 19 ) 20 21 func TestTimeoutCollector(t *testing.T) { 22 suite.Run(t, new(TimeoutCollectorTestSuite)) 23 } 24 25 // TimeoutCollectorTestSuite is a test suite for testing TimeoutCollector. It stores mocked 26 // state internally for testing behavior. 27 type TimeoutCollectorTestSuite struct { 28 suite.Suite 29 30 view uint64 31 notifier *mocks.TimeoutAggregationConsumer 32 processor *mocks.TimeoutProcessor 33 collector *TimeoutCollector 34 } 35 36 func (s *TimeoutCollectorTestSuite) SetupTest() { 37 s.view = 1000 38 s.notifier = mocks.NewTimeoutAggregationConsumer(s.T()) 39 s.processor = mocks.NewTimeoutProcessor(s.T()) 40 41 s.notifier.On("OnNewQcDiscovered", mock.Anything).Maybe() 42 s.notifier.On("OnNewTcDiscovered", mock.Anything).Maybe() 43 44 s.collector = NewTimeoutCollector(unittest.Logger(), s.view, s.notifier, s.processor) 45 } 46 47 // TestView tests that `View` returns the same value that was passed in constructor 48 func (s *TimeoutCollectorTestSuite) TestView() { 49 require.Equal(s.T(), s.view, s.collector.View()) 50 } 51 52 // TestAddTimeout_HappyPath tests that process in happy path executed by multiple workers deliver expected results 53 // all operations should be successful, no errors expected 54 func (s *TimeoutCollectorTestSuite) TestAddTimeout_HappyPath() { 55 var wg sync.WaitGroup 56 for i := 0; i < 20; i++ { 57 wg.Add(1) 58 go func() { 59 defer wg.Done() 60 timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view)) 61 s.notifier.On("OnTimeoutProcessed", timeout).Once() 62 s.processor.On("Process", timeout).Return(nil).Once() 63 err := s.collector.AddTimeout(timeout) 64 require.NoError(s.T(), err) 65 }() 66 } 67 68 unittest.AssertReturnsBefore(s.T(), wg.Wait, time.Second) 69 s.processor.AssertExpectations(s.T()) 70 } 71 72 // TestAddTimeout_DoubleTimeout tests that submitting two different timeouts for same view ends with reporting 73 // double timeout to notifier which can be slashed later. 74 func (s *TimeoutCollectorTestSuite) TestAddTimeout_DoubleTimeout() { 75 timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view)) 76 s.notifier.On("OnTimeoutProcessed", timeout).Once() 77 s.processor.On("Process", timeout).Return(nil).Once() 78 err := s.collector.AddTimeout(timeout) 79 require.NoError(s.T(), err) 80 81 otherTimeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view), 82 helper.WithTimeoutObjectSignerID(timeout.SignerID)) 83 84 s.notifier.On("OnDoubleTimeoutDetected", timeout, otherTimeout).Once() 85 86 err = s.collector.AddTimeout(otherTimeout) 87 require.NoError(s.T(), err) 88 s.notifier.AssertExpectations(s.T()) 89 s.processor.AssertNumberOfCalls(s.T(), "Process", 1) 90 } 91 92 // TestAddTimeout_RepeatedTimeout checks that repeated timeouts are silently dropped without any errors. 93 func (s *TimeoutCollectorTestSuite) TestAddTimeout_RepeatedTimeout() { 94 timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view)) 95 s.notifier.On("OnTimeoutProcessed", timeout).Once() 96 s.processor.On("Process", timeout).Return(nil).Once() 97 err := s.collector.AddTimeout(timeout) 98 require.NoError(s.T(), err) 99 err = s.collector.AddTimeout(timeout) 100 require.NoError(s.T(), err) 101 s.processor.AssertNumberOfCalls(s.T(), "Process", 1) 102 } 103 104 // TestAddTimeout_TimeoutCacheException tests that submitting timeout object for view which is not designated for this 105 // collector results in ErrTimeoutForIncompatibleView. 106 func (s *TimeoutCollectorTestSuite) TestAddTimeout_TimeoutCacheException() { 107 // incompatible view is an exception and not handled by timeout collector 108 timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view + 1)) 109 err := s.collector.AddTimeout(timeout) 110 require.ErrorIs(s.T(), err, ErrTimeoutForIncompatibleView) 111 s.processor.AssertNotCalled(s.T(), "Process") 112 } 113 114 // TestAddTimeout_InvalidTimeout tests that sentinel errors while processing timeouts are correctly handled and reported 115 // to notifier, but exceptions are propagated to caller. 116 func (s *TimeoutCollectorTestSuite) TestAddTimeout_InvalidTimeout() { 117 s.Run("invalid-timeout", func() { 118 timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view)) 119 s.processor.On("Process", timeout).Return(model.NewInvalidTimeoutErrorf(timeout, "")).Once() 120 s.notifier.On("OnInvalidTimeoutDetected", mock.Anything).Run(func(args mock.Arguments) { 121 invalidTimeoutErr := args.Get(0).(model.InvalidTimeoutError) 122 require.Equal(s.T(), timeout, invalidTimeoutErr.Timeout) 123 }).Once() 124 err := s.collector.AddTimeout(timeout) 125 require.NoError(s.T(), err) 126 127 s.notifier.AssertCalled(s.T(), "OnInvalidTimeoutDetected", mock.Anything) 128 }) 129 s.Run("process-exception", func() { 130 exception := errors.New("invalid-signature") 131 timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view)) 132 s.processor.On("Process", timeout).Return(exception).Once() 133 err := s.collector.AddTimeout(timeout) 134 require.ErrorIs(s.T(), err, exception) 135 }) 136 } 137 138 // TestAddTimeout_TONotifications tests that TimeoutCollector in happy path reports the newest discovered QC and TC 139 func (s *TimeoutCollectorTestSuite) TestAddTimeout_TONotifications() { 140 qcCount := 100 141 // generate QCs with increasing view numbers 142 if s.view < uint64(qcCount) { 143 s.T().Fatal("invalid test configuration") 144 } 145 146 *s.notifier = *mocks.NewTimeoutAggregationConsumer(s.T()) 147 148 var highestReportedQC *flow.QuorumCertificate 149 s.notifier.On("OnNewQcDiscovered", mock.Anything).Run(func(args mock.Arguments) { 150 qc := args.Get(0).(*flow.QuorumCertificate) 151 if highestReportedQC == nil || highestReportedQC.View < qc.View { 152 highestReportedQC = qc 153 } 154 }) 155 156 lastViewTC := helper.MakeTC(helper.WithTCView(s.view - 1)) 157 s.notifier.On("OnNewTcDiscovered", lastViewTC).Once() 158 159 timeouts := make([]*model.TimeoutObject, 0, qcCount) 160 for i := 0; i < qcCount; i++ { 161 qc := helper.MakeQC(helper.WithQCView(uint64(i))) 162 timeout := helper.TimeoutObjectFixture(func(timeout *model.TimeoutObject) { 163 timeout.View = s.view 164 timeout.NewestQC = qc 165 timeout.LastViewTC = lastViewTC 166 }) 167 timeouts = append(timeouts, timeout) 168 s.notifier.On("OnTimeoutProcessed", timeout).Once() 169 s.processor.On("Process", timeout).Return(nil).Once() 170 } 171 172 expectedHighestQC := timeouts[len(timeouts)-1].NewestQC 173 174 // shuffle timeouts in random order 175 rand.Shuffle(len(timeouts), func(i, j int) { 176 timeouts[i], timeouts[j] = timeouts[j], timeouts[i] 177 }) 178 179 var wg sync.WaitGroup 180 wg.Add(len(timeouts)) 181 for _, timeout := range timeouts { 182 go func(timeout *model.TimeoutObject) { 183 defer wg.Done() 184 err := s.collector.AddTimeout(timeout) 185 require.NoError(s.T(), err) 186 }(timeout) 187 } 188 wg.Wait() 189 190 require.Equal(s.T(), expectedHighestQC, highestReportedQC) 191 }