github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/pacemaker/pacemaker_test.go (about) 1 package pacemaker 2 3 import ( 4 "context" 5 "errors" 6 "math/rand" 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/mock" 12 "github.com/stretchr/testify/require" 13 "github.com/stretchr/testify/suite" 14 15 "github.com/onflow/flow-go/consensus/hotstuff" 16 "github.com/onflow/flow-go/consensus/hotstuff/helper" 17 "github.com/onflow/flow-go/consensus/hotstuff/mocks" 18 "github.com/onflow/flow-go/consensus/hotstuff/model" 19 "github.com/onflow/flow-go/consensus/hotstuff/pacemaker/timeout" 20 "github.com/onflow/flow-go/model/flow" 21 "github.com/onflow/flow-go/utils/unittest" 22 ) 23 24 const ( 25 minRepTimeout float64 = 100.0 // Milliseconds 26 maxRepTimeout float64 = 600.0 // Milliseconds 27 multiplicativeIncrease float64 = 1.5 // multiplicative factor 28 happyPathMaxRoundFailures uint64 = 6 // number of failed rounds before first timeout increase 29 ) 30 31 func expectedTimerInfo(view uint64) interface{} { 32 return mock.MatchedBy( 33 func(timerInfo model.TimerInfo) bool { 34 return timerInfo.View == view 35 }) 36 } 37 38 func TestActivePaceMaker(t *testing.T) { 39 suite.Run(t, new(ActivePaceMakerTestSuite)) 40 } 41 42 type ActivePaceMakerTestSuite struct { 43 suite.Suite 44 45 initialView uint64 46 initialQC *flow.QuorumCertificate 47 initialTC *flow.TimeoutCertificate 48 49 notifier *mocks.Consumer 50 proposalDurationProvider hotstuff.ProposalDurationProvider 51 persist *mocks.Persister 52 paceMaker *ActivePaceMaker 53 stop context.CancelFunc 54 timeoutConf timeout.Config 55 } 56 57 func (s *ActivePaceMakerTestSuite) SetupTest() { 58 s.initialView = 3 59 s.initialQC = QC(2) 60 s.initialTC = nil 61 var err error 62 63 s.timeoutConf, err = timeout.NewConfig(time.Duration(minRepTimeout*1e6), time.Duration(maxRepTimeout*1e6), multiplicativeIncrease, happyPathMaxRoundFailures, time.Duration(maxRepTimeout*1e6)) 64 require.NoError(s.T(), err) 65 66 // init consumer for notifications emitted by PaceMaker 67 s.notifier = mocks.NewConsumer(s.T()) 68 s.notifier.On("OnStartingTimeout", expectedTimerInfo(s.initialView)).Return().Once() 69 70 // init Persister dependency for PaceMaker 71 // CAUTION: The Persister hands a pointer to `livenessData` to the PaceMaker, which means the PaceMaker 72 // could modify our struct in-place. `livenessData` should not be used by tests to determine expected values! 73 s.persist = mocks.NewPersister(s.T()) 74 livenessData := &hotstuff.LivenessData{ 75 CurrentView: 3, 76 LastViewTC: nil, 77 NewestQC: s.initialQC, 78 } 79 s.persist.On("GetLivenessData").Return(livenessData, nil) 80 81 // init PaceMaker and start 82 s.paceMaker, err = New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist) 83 require.NoError(s.T(), err) 84 85 var ctx context.Context 86 ctx, s.stop = context.WithCancel(context.Background()) 87 s.paceMaker.Start(ctx) 88 } 89 90 func (s *ActivePaceMakerTestSuite) TearDownTest() { 91 s.stop() 92 } 93 94 func QC(view uint64) *flow.QuorumCertificate { 95 return helper.MakeQC(helper.WithQCView(view)) 96 } 97 98 func LivenessData(qc *flow.QuorumCertificate) *hotstuff.LivenessData { 99 return &hotstuff.LivenessData{ 100 CurrentView: qc.View + 1, 101 LastViewTC: nil, 102 NewestQC: qc, 103 } 104 } 105 106 // TestProcessQC_SkipIncreaseViewThroughQC tests that ActivePaceMaker increases view when receiving QC, 107 // if applicable, by skipping views 108 func (s *ActivePaceMakerTestSuite) TestProcessQC_SkipIncreaseViewThroughQC() { 109 // seeing a QC for the current view should advance the view by one 110 qc := QC(s.initialView) 111 s.persist.On("PutLivenessData", LivenessData(qc)).Return(nil).Once() 112 s.notifier.On("OnStartingTimeout", expectedTimerInfo(4)).Return().Once() 113 s.notifier.On("OnQcTriggeredViewChange", s.initialView, uint64(4), qc).Return().Once() 114 s.notifier.On("OnViewChange", s.initialView, qc.View+1).Once() 115 nve, err := s.paceMaker.ProcessQC(qc) 116 require.NoError(s.T(), err) 117 require.Equal(s.T(), qc.View+1, s.paceMaker.CurView()) 118 require.True(s.T(), nve.View == qc.View+1) 119 require.Equal(s.T(), qc, s.paceMaker.NewestQC()) 120 require.Nil(s.T(), s.paceMaker.LastViewTC()) 121 122 // seeing a QC for 10 views in the future should advance to view +11 123 curView := s.paceMaker.CurView() 124 qc = QC(curView + 10) 125 s.persist.On("PutLivenessData", LivenessData(qc)).Return(nil).Once() 126 s.notifier.On("OnStartingTimeout", expectedTimerInfo(qc.View+1)).Return().Once() 127 s.notifier.On("OnQcTriggeredViewChange", curView, qc.View+1, qc).Return().Once() 128 s.notifier.On("OnViewChange", curView, qc.View+1).Once() 129 nve, err = s.paceMaker.ProcessQC(qc) 130 require.NoError(s.T(), err) 131 require.True(s.T(), nve.View == qc.View+1) 132 require.Equal(s.T(), qc, s.paceMaker.NewestQC()) 133 require.Nil(s.T(), s.paceMaker.LastViewTC()) 134 135 require.Equal(s.T(), qc.View+1, s.paceMaker.CurView()) 136 } 137 138 // TestProcessTC_SkipIncreaseViewThroughTC tests that ActivePaceMaker increases view when receiving TC, 139 // if applicable, by skipping views 140 func (s *ActivePaceMakerTestSuite) TestProcessTC_SkipIncreaseViewThroughTC() { 141 // seeing a TC for the current view should advance the view by one 142 tc := helper.MakeTC(helper.WithTCView(s.initialView), helper.WithTCNewestQC(s.initialQC)) 143 expectedLivenessData := &hotstuff.LivenessData{ 144 CurrentView: tc.View + 1, 145 LastViewTC: tc, 146 NewestQC: s.initialQC, 147 } 148 s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once() 149 s.notifier.On("OnStartingTimeout", expectedTimerInfo(tc.View+1)).Return().Once() 150 s.notifier.On("OnTcTriggeredViewChange", s.initialView, tc.View+1, tc).Return().Once() 151 s.notifier.On("OnViewChange", s.initialView, tc.View+1).Once() 152 nve, err := s.paceMaker.ProcessTC(tc) 153 require.NoError(s.T(), err) 154 require.Equal(s.T(), tc.View+1, s.paceMaker.CurView()) 155 require.True(s.T(), nve.View == tc.View+1) 156 require.Equal(s.T(), tc, s.paceMaker.LastViewTC()) 157 158 // seeing a TC for 10 views in the future should advance to view +11 159 curView := s.paceMaker.CurView() 160 tc = helper.MakeTC(helper.WithTCView(curView+10), helper.WithTCNewestQC(s.initialQC)) 161 expectedLivenessData = &hotstuff.LivenessData{ 162 CurrentView: tc.View + 1, 163 LastViewTC: tc, 164 NewestQC: s.initialQC, 165 } 166 s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once() 167 s.notifier.On("OnStartingTimeout", expectedTimerInfo(tc.View+1)).Return().Once() 168 s.notifier.On("OnTcTriggeredViewChange", curView, tc.View+1, tc).Return().Once() 169 s.notifier.On("OnViewChange", curView, tc.View+1).Once() 170 nve, err = s.paceMaker.ProcessTC(tc) 171 require.NoError(s.T(), err) 172 require.True(s.T(), nve.View == tc.View+1) 173 require.Equal(s.T(), tc, s.paceMaker.LastViewTC()) 174 require.Equal(s.T(), tc.NewestQC, s.paceMaker.NewestQC()) 175 176 require.Equal(s.T(), tc.View+1, s.paceMaker.CurView()) 177 } 178 179 // TestProcessTC_IgnoreOldTC tests that ActivePaceMaker ignores old TC and doesn't advance round. 180 func (s *ActivePaceMakerTestSuite) TestProcessTC_IgnoreOldTC() { 181 nve, err := s.paceMaker.ProcessTC(helper.MakeTC(helper.WithTCView(s.initialView-1), 182 helper.WithTCNewestQC(s.initialQC))) 183 require.NoError(s.T(), err) 184 require.Nil(s.T(), nve) 185 require.Equal(s.T(), s.initialView, s.paceMaker.CurView()) 186 } 187 188 // TestProcessTC_IgnoreNilTC tests that ActivePaceMaker accepts nil TC as allowed input but doesn't trigger a new view event 189 func (s *ActivePaceMakerTestSuite) TestProcessTC_IgnoreNilTC() { 190 nve, err := s.paceMaker.ProcessTC(nil) 191 require.NoError(s.T(), err) 192 require.Nil(s.T(), nve) 193 require.Equal(s.T(), s.initialView, s.paceMaker.CurView()) 194 } 195 196 // TestProcessQC_PersistException tests that ActivePaceMaker propagates exception 197 // when processing QC 198 func (s *ActivePaceMakerTestSuite) TestProcessQC_PersistException() { 199 exception := errors.New("persist-exception") 200 qc := QC(s.initialView) 201 s.persist.On("PutLivenessData", mock.Anything).Return(exception).Once() 202 nve, err := s.paceMaker.ProcessQC(qc) 203 require.Nil(s.T(), nve) 204 require.ErrorIs(s.T(), err, exception) 205 } 206 207 // TestProcessTC_PersistException tests that ActivePaceMaker propagates exception 208 // when processing TC 209 func (s *ActivePaceMakerTestSuite) TestProcessTC_PersistException() { 210 exception := errors.New("persist-exception") 211 tc := helper.MakeTC(helper.WithTCView(s.initialView)) 212 s.persist.On("PutLivenessData", mock.Anything).Return(exception).Once() 213 nve, err := s.paceMaker.ProcessTC(tc) 214 require.Nil(s.T(), nve) 215 require.ErrorIs(s.T(), err, exception) 216 } 217 218 // TestProcessQC_InvalidatesLastViewTC verifies that PaceMaker does not retain any old 219 // TC if the last view change was triggered by observing a QC from the previous view. 220 func (s *ActivePaceMakerTestSuite) TestProcessQC_InvalidatesLastViewTC() { 221 tc := helper.MakeTC(helper.WithTCView(s.initialView+1), helper.WithTCNewestQC(s.initialQC)) 222 s.persist.On("PutLivenessData", mock.Anything).Return(nil).Times(2) 223 s.notifier.On("OnStartingTimeout", mock.Anything).Return().Times(2) 224 s.notifier.On("OnTcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return().Once() 225 s.notifier.On("OnQcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return().Once() 226 s.notifier.On("OnViewChange", s.initialView, tc.View+1).Once() 227 nve, err := s.paceMaker.ProcessTC(tc) 228 require.NotNil(s.T(), nve) 229 require.NoError(s.T(), err) 230 require.NotNil(s.T(), s.paceMaker.LastViewTC()) 231 232 qc := QC(tc.View + 1) 233 s.notifier.On("OnViewChange", tc.View+1, qc.View+1).Once() 234 nve, err = s.paceMaker.ProcessQC(qc) 235 require.NotNil(s.T(), nve) 236 require.NoError(s.T(), err) 237 require.Nil(s.T(), s.paceMaker.LastViewTC()) 238 } 239 240 // TestProcessQC_IgnoreOldQC tests that ActivePaceMaker ignores old QC and doesn't advance round 241 func (s *ActivePaceMakerTestSuite) TestProcessQC_IgnoreOldQC() { 242 qc := QC(s.initialView - 1) 243 nve, err := s.paceMaker.ProcessQC(qc) 244 require.NoError(s.T(), err) 245 require.Nil(s.T(), nve) 246 require.Equal(s.T(), s.initialView, s.paceMaker.CurView()) 247 require.NotEqual(s.T(), qc, s.paceMaker.NewestQC()) 248 } 249 250 // TestProcessQC_UpdateNewestQC tests that ActivePaceMaker tracks the newest QC even if it has advanced past this view. 251 // In this test, we feed a newer QC as part of a TC into the PaceMaker. 252 func (s *ActivePaceMakerTestSuite) TestProcessQC_UpdateNewestQC() { 253 tc := helper.MakeTC(helper.WithTCView(s.initialView+10), helper.WithTCNewestQC(s.initialQC)) 254 expectedView := tc.View + 1 255 s.notifier.On("OnTcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return().Once() 256 s.notifier.On("OnViewChange", s.initialView, expectedView).Once() 257 s.notifier.On("OnStartingTimeout", mock.Anything).Return().Once() 258 s.persist.On("PutLivenessData", mock.Anything).Return(nil).Once() 259 nve, err := s.paceMaker.ProcessTC(tc) 260 require.NoError(s.T(), err) 261 require.NotNil(s.T(), nve) 262 263 qc := QC(s.initialView + 5) 264 expectedLivenessData := &hotstuff.LivenessData{ 265 CurrentView: expectedView, 266 LastViewTC: tc, 267 NewestQC: qc, 268 } 269 s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once() 270 271 nve, err = s.paceMaker.ProcessQC(qc) 272 require.NoError(s.T(), err) 273 require.Nil(s.T(), nve) 274 require.Equal(s.T(), qc, s.paceMaker.NewestQC()) 275 } 276 277 // TestProcessTC_UpdateNewestQC tests that ActivePaceMaker tracks the newest QC included in TC even if it has advanced past this view. 278 func (s *ActivePaceMakerTestSuite) TestProcessTC_UpdateNewestQC() { 279 tc := helper.MakeTC(helper.WithTCView(s.initialView+10), helper.WithTCNewestQC(s.initialQC)) 280 expectedView := tc.View + 1 281 s.notifier.On("OnTcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return().Once() 282 s.notifier.On("OnViewChange", s.initialView, expectedView).Once() 283 s.notifier.On("OnStartingTimeout", mock.Anything).Return().Once() 284 s.persist.On("PutLivenessData", mock.Anything).Return(nil).Once() 285 nve, err := s.paceMaker.ProcessTC(tc) 286 require.NoError(s.T(), err) 287 require.NotNil(s.T(), nve) 288 289 qc := QC(s.initialView + 5) 290 olderTC := helper.MakeTC(helper.WithTCView(s.paceMaker.CurView()-1), helper.WithTCNewestQC(qc)) 291 expectedLivenessData := &hotstuff.LivenessData{ 292 CurrentView: expectedView, 293 LastViewTC: tc, 294 NewestQC: qc, 295 } 296 s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once() 297 298 nve, err = s.paceMaker.ProcessTC(olderTC) 299 require.NoError(s.T(), err) 300 require.Nil(s.T(), nve) 301 require.Equal(s.T(), qc, s.paceMaker.NewestQC()) 302 } 303 304 // Test_Initialization tests QCs and TCs provided as optional constructor arguments. 305 // We want to test that nil, old and duplicate TCs & QCs are accepted in arbitrary order. 306 // The constructed PaceMaker should be in the state: 307 // - in view V+1, where V is the _largest view of _any_ of the ingested QCs and TCs 308 // - method `NewestQC` should report the QC with the highest View in _any_ of the inputs 309 func (s *ActivePaceMakerTestSuite) Test_Initialization() { 310 highestView := uint64(0) // highest View of any QC or TC constructed below 311 312 // Randomly create 80 TCs: 313 // * their view is randomly sampled from the range [3, 103) 314 // * as we sample 80 times, probability of creating 2 TCs for the same 315 // view is practically 1 (-> birthday problem) 316 // * we place the TCs in a slice of length 110, i.e. some elements are guaranteed to be nil 317 // * Note: we specifically allow for the TC to have the same view as the highest QC. 318 // This is useful as a fallback, because it allows replicas other than the designated 319 // leader to also collect votes and generate a QC. 320 tcs := make([]*flow.TimeoutCertificate, 110) 321 for i := 0; i < 80; i++ { 322 tcView := s.initialView + uint64(rand.Intn(100)) 323 qcView := 1 + uint64(rand.Intn(int(tcView))) 324 tcs[i] = helper.MakeTC(helper.WithTCView(tcView), helper.WithTCNewestQC(QC(qcView))) 325 highestView = max(highestView, tcView, qcView) 326 } 327 rand.Shuffle(len(tcs), func(i, j int) { 328 tcs[i], tcs[j] = tcs[j], tcs[i] 329 }) 330 331 // randomly create 80 QCs (same logic as above) 332 qcs := make([]*flow.QuorumCertificate, 110) 333 for i := 0; i < 80; i++ { 334 qcs[i] = QC(s.initialView + uint64(rand.Intn(100))) 335 highestView = max(highestView, qcs[i].View) 336 } 337 rand.Shuffle(len(qcs), func(i, j int) { 338 qcs[i], qcs[j] = qcs[j], qcs[i] 339 }) 340 341 // set up mocks 342 s.persist.On("PutLivenessData", mock.Anything).Return(nil) 343 344 // test that the constructor finds the newest QC and TC 345 s.Run("Random TCs and QCs combined", func() { 346 pm, err := New( 347 timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, 348 WithQCs(qcs...), WithTCs(tcs...), 349 ) 350 require.NoError(s.T(), err) 351 352 require.Equal(s.T(), highestView+1, pm.CurView()) 353 if tc := pm.LastViewTC(); tc != nil { 354 require.Equal(s.T(), highestView, tc.View) 355 } else { 356 require.Equal(s.T(), highestView, pm.NewestQC().View) 357 } 358 }) 359 360 // We specifically test an edge case: an outdated TC can still contain a QC that 361 // is newer than the newest QC the pacemaker knows so far. 362 s.Run("Newest QC in older TC", func() { 363 tcs[17] = helper.MakeTC(helper.WithTCView(highestView+20), helper.WithTCNewestQC(QC(highestView+5))) 364 tcs[45] = helper.MakeTC(helper.WithTCView(highestView+15), helper.WithTCNewestQC(QC(highestView+12))) 365 366 pm, err := New( 367 timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, 368 WithTCs(tcs...), WithQCs(qcs...), 369 ) 370 require.NoError(s.T(), err) 371 372 // * when observing tcs[17], which is newer than any other QC or TC, the pacemaker should enter view tcs[17].View + 1 373 // * when observing tcs[45], which is older than tcs[17], the PaceMaker should notice that the QC in tcs[45] 374 // is newer than its local QC and update it 375 require.Equal(s.T(), tcs[17].View+1, pm.CurView()) 376 require.Equal(s.T(), tcs[17], pm.LastViewTC()) 377 require.Equal(s.T(), tcs[45].NewestQC, pm.NewestQC()) 378 }) 379 380 // Another edge case: a TC from a past view contains QC for the same view. 381 // While is TC is outdated, the contained QC is still newer that the QC the pacemaker knows so far. 382 s.Run("Newest QC in older TC", func() { 383 tcs[17] = helper.MakeTC(helper.WithTCView(highestView+20), helper.WithTCNewestQC(QC(highestView+5))) 384 tcs[45] = helper.MakeTC(helper.WithTCView(highestView+15), helper.WithTCNewestQC(QC(highestView+15))) 385 386 pm, err := New( 387 timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, 388 WithTCs(tcs...), WithQCs(qcs...), 389 ) 390 require.NoError(s.T(), err) 391 392 // * when observing tcs[17], which is newer than any other QC or TC, the pacemaker should enter view tcs[17].View + 1 393 // * when observing tcs[45], which is older than tcs[17], the PaceMaker should notice that the QC in tcs[45] 394 // is newer than its local QC and update it 395 require.Equal(s.T(), tcs[17].View+1, pm.CurView()) 396 require.Equal(s.T(), tcs[17], pm.LastViewTC()) 397 require.Equal(s.T(), tcs[45].NewestQC, pm.NewestQC()) 398 }) 399 400 // Verify that WithTCs still works correctly if no TCs are given: 401 // the list of TCs is empty or all contained TCs are nil 402 s.Run("Only nil TCs", func() { 403 pm, err := New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, WithTCs()) 404 require.NoError(s.T(), err) 405 require.Equal(s.T(), s.initialView, pm.CurView()) 406 407 pm, err = New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, WithTCs(nil, nil, nil)) 408 require.NoError(s.T(), err) 409 require.Equal(s.T(), s.initialView, pm.CurView()) 410 }) 411 412 // Verify that WithQCs still works correctly if no QCs are given: 413 // the list of QCs is empty or all contained QCs are nil 414 s.Run("Only nil QCs", func() { 415 pm, err := New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, WithQCs()) 416 require.NoError(s.T(), err) 417 require.Equal(s.T(), s.initialView, pm.CurView()) 418 419 pm, err = New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, WithQCs(nil, nil, nil)) 420 require.NoError(s.T(), err) 421 require.Equal(s.T(), s.initialView, pm.CurView()) 422 }) 423 424 } 425 426 // TestProposalDuration tests that the active pacemaker forwards proposal duration values from the provider. 427 func (s *ActivePaceMakerTestSuite) TestProposalDuration() { 428 proposalDurationProvider := NewStaticProposalDurationProvider(time.Millisecond * 500) 429 pm, err := New(timeout.NewController(s.timeoutConf), &proposalDurationProvider, s.notifier, s.persist) 430 require.NoError(s.T(), err) 431 432 now := time.Now().UTC() 433 assert.Equal(s.T(), now.Add(time.Millisecond*500), pm.TargetPublicationTime(117, now, unittest.IdentifierFixture())) 434 proposalDurationProvider.dur = time.Second 435 assert.Equal(s.T(), now.Add(time.Second), pm.TargetPublicationTime(117, now, unittest.IdentifierFixture())) 436 } 437 438 func max(a uint64, values ...uint64) uint64 { 439 for _, v := range values { 440 if v > a { 441 a = v 442 } 443 } 444 return a 445 }