github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/cruisectl/block_time_controller_test.go (about) 1 package cruisectl 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/mock" 11 "github.com/stretchr/testify/require" 12 "github.com/stretchr/testify/suite" 13 "go.uber.org/atomic" 14 15 "github.com/onflow/flow-go/consensus/hotstuff/model" 16 "github.com/onflow/flow-go/model/flow" 17 "github.com/onflow/flow-go/module/irrecoverable" 18 mockmodule "github.com/onflow/flow-go/module/mock" 19 mockprotocol "github.com/onflow/flow-go/state/protocol/mock" 20 "github.com/onflow/flow-go/utils/unittest" 21 "github.com/onflow/flow-go/utils/unittest/mocks" 22 ) 23 24 // BlockTimeControllerSuite encapsulates tests for the BlockTimeController. 25 type BlockTimeControllerSuite struct { 26 suite.Suite 27 28 initialView uint64 29 epochCounter uint64 30 curEpochFirstView uint64 31 curEpochFinalView uint64 32 epochFallbackTriggered bool 33 34 metrics mockmodule.CruiseCtlMetrics 35 state mockprotocol.State 36 params mockprotocol.Params 37 snapshot mockprotocol.Snapshot 38 epochs mocks.EpochQuery 39 curEpoch mockprotocol.Epoch 40 41 config *Config 42 ctx irrecoverable.SignalerContext 43 cancel context.CancelFunc 44 ctl *BlockTimeController 45 } 46 47 func TestBlockTimeController(t *testing.T) { 48 suite.Run(t, new(BlockTimeControllerSuite)) 49 } 50 51 // SetupTest initializes mocks and default values. 52 func (bs *BlockTimeControllerSuite) SetupTest() { 53 bs.config = DefaultConfig() 54 bs.config.Enabled.Store(true) 55 bs.initialView = 0 56 bs.epochCounter = uint64(0) 57 bs.curEpochFirstView = uint64(0) 58 bs.curEpochFinalView = uint64(604_800) // 1 view/sec 59 bs.epochFallbackTriggered = false 60 setupMocks(bs) 61 } 62 63 func setupMocks(bs *BlockTimeControllerSuite) { 64 bs.metrics = *mockmodule.NewCruiseCtlMetrics(bs.T()) 65 bs.metrics.On("PIDError", mock.Anything, mock.Anything, mock.Anything).Maybe() 66 bs.metrics.On("TargetProposalDuration", mock.Anything).Maybe() 67 bs.metrics.On("ControllerOutput", mock.Anything).Maybe() 68 69 bs.state = *mockprotocol.NewState(bs.T()) 70 bs.params = *mockprotocol.NewParams(bs.T()) 71 bs.snapshot = *mockprotocol.NewSnapshot(bs.T()) 72 bs.epochs = *mocks.NewEpochQuery(bs.T(), bs.epochCounter) 73 bs.curEpoch = *mockprotocol.NewEpoch(bs.T()) 74 75 bs.state.On("Final").Return(&bs.snapshot) 76 bs.state.On("AtHeight", mock.Anything).Return(&bs.snapshot).Maybe() 77 bs.state.On("Params").Return(&bs.params) 78 bs.params.On("EpochFallbackTriggered").Return( 79 func() bool { return bs.epochFallbackTriggered }, 80 func() error { return nil }) 81 bs.snapshot.On("Phase").Return( 82 func() flow.EpochPhase { return bs.epochs.Phase() }, 83 func() error { return nil }) 84 bs.snapshot.On("Head").Return(unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView+11)), nil).Maybe() 85 bs.snapshot.On("Epochs").Return(&bs.epochs) 86 bs.curEpoch.On("Counter").Return(bs.epochCounter, nil) 87 bs.curEpoch.On("FirstView").Return(bs.curEpochFirstView, nil) 88 bs.curEpoch.On("FinalView").Return(bs.curEpochFinalView, nil) 89 bs.epochs.Add(&bs.curEpoch) 90 91 bs.ctx, bs.cancel = irrecoverable.NewMockSignalerContextWithCancel(bs.T(), context.Background()) 92 } 93 94 // CreateAndStartController creates and starts the BlockTimeController. 95 // Should be called only once per test case. 96 func (bs *BlockTimeControllerSuite) CreateAndStartController() { 97 ctl, err := NewBlockTimeController(unittest.Logger(), &bs.metrics, bs.config, &bs.state, bs.initialView) 98 require.NoError(bs.T(), err) 99 bs.ctl = ctl 100 bs.ctl.Start(bs.ctx) 101 unittest.RequireCloseBefore(bs.T(), bs.ctl.Ready(), time.Second, "component did not start") 102 } 103 104 // StopController stops the BlockTimeController. 105 func (bs *BlockTimeControllerSuite) StopController() { 106 bs.cancel() 107 unittest.RequireCloseBefore(bs.T(), bs.ctl.Done(), time.Second, "component did not stop") 108 } 109 110 // AssertCorrectInitialization checks that the controller is configured as expected after construction. 111 func (bs *BlockTimeControllerSuite) AssertCorrectInitialization() { 112 // at initialization, controller should be set up to release blocks without delay 113 controllerTiming := bs.ctl.GetProposalTiming() 114 now := time.Now().UTC() 115 116 if bs.ctl.epochFallbackTriggered || !bs.ctl.config.Enabled.Load() { 117 // if epoch fallback is triggered or controller is disabled, use fallback timing 118 assert.Equal(bs.T(), now.Add(bs.ctl.config.FallbackProposalDelay.Load()), controllerTiming.TargetPublicationTime(7, now, unittest.IdentifierFixture())) 119 } else { 120 // otherwise should publish immediately 121 assert.Equal(bs.T(), now, controllerTiming.TargetPublicationTime(7, now, unittest.IdentifierFixture())) 122 } 123 if bs.ctl.epochFallbackTriggered { 124 return 125 } 126 127 // should initialize epoch info 128 epoch := bs.ctl.epochInfo 129 expectedEndTime := bs.config.TargetTransition.inferTargetEndTime(time.Now(), epoch.fractionComplete(bs.initialView)) 130 assert.Equal(bs.T(), bs.curEpochFirstView, epoch.curEpochFirstView) 131 assert.Equal(bs.T(), bs.curEpochFinalView, epoch.curEpochFinalView) 132 assert.Equal(bs.T(), expectedEndTime, epoch.curEpochTargetEndTime) 133 134 // if next epoch is set up, final view should be set 135 if phase := bs.epochs.Phase(); phase > flow.EpochPhaseStaking { 136 finalView, err := bs.epochs.Next().FinalView() 137 require.NoError(bs.T(), err) 138 assert.Equal(bs.T(), finalView, *epoch.nextEpochFinalView) 139 } else { 140 assert.Nil(bs.T(), epoch.nextEpochFinalView) 141 } 142 143 // should create an initial measurement 144 assert.Equal(bs.T(), bs.initialView, controllerTiming.ObservationView()) 145 assert.WithinDuration(bs.T(), time.Now(), controllerTiming.ObservationTime(), time.Minute) 146 // errors should be initialized to zero 147 assert.Equal(bs.T(), float64(0), bs.ctl.proportionalErr.Value()) 148 assert.Equal(bs.T(), float64(0), bs.ctl.integralErr.Value()) 149 } 150 151 // SanityCheckSubsequentMeasurements checks that two consecutive states of the BlockTimeController are different or equal and 152 // broadly reasonable. It does not assert exact values, because part of the measurements depend on timing in the worker. 153 func (bs *BlockTimeControllerSuite) SanityCheckSubsequentMeasurements(d1, d2 *controllerStateDigest, expectedEqual bool) { 154 if expectedEqual { 155 // later input should have left state invariant, including the Observation 156 assert.Equal(bs.T(), d1.latestProposalTiming.ObservationTime(), d2.latestProposalTiming.ObservationTime()) 157 assert.Equal(bs.T(), d1.latestProposalTiming.ObservationView(), d2.latestProposalTiming.ObservationView()) 158 // new measurement should have same error 159 assert.Equal(bs.T(), d1.proportionalErr.Value(), d2.proportionalErr.Value()) 160 assert.Equal(bs.T(), d1.integralErr.Value(), d2.integralErr.Value()) 161 } else { 162 // later input should have caused a new Observation to be recorded 163 assert.True(bs.T(), d1.latestProposalTiming.ObservationTime().Before(d2.latestProposalTiming.ObservationTime())) 164 // new measurement should have different error 165 assert.NotEqual(bs.T(), d1.proportionalErr.Value(), d2.proportionalErr.Value()) 166 assert.NotEqual(bs.T(), d1.integralErr.Value(), d2.integralErr.Value()) 167 } 168 } 169 170 // PrintMeasurement prints the current state of the controller and the last measurement. 171 func (bs *BlockTimeControllerSuite) PrintMeasurement(parentBlockId flow.Identifier) { 172 ctl := bs.ctl 173 m := ctl.GetProposalTiming() 174 tpt := m.TargetPublicationTime(m.ObservationView()+1, m.ObservationTime(), parentBlockId) 175 fmt.Printf("v=%d\tt=%s\tPD=%s\te_N=%.3f\tI_M=%.3f\n", 176 m.ObservationView(), m.ObservationTime(), tpt.Sub(m.ObservationTime()), 177 ctl.proportionalErr.Value(), ctl.integralErr.Value()) 178 } 179 180 // TestStartStop tests that the component can be started and stopped gracefully. 181 func (bs *BlockTimeControllerSuite) TestStartStop() { 182 bs.CreateAndStartController() 183 bs.StopController() 184 } 185 186 // TestInit_EpochStakingPhase tests initializing the component in the EpochStaking phase. 187 // Measurement and epoch info should be initialized, next epoch final view should be nil. 188 func (bs *BlockTimeControllerSuite) TestInit_EpochStakingPhase() { 189 bs.CreateAndStartController() 190 defer bs.StopController() 191 bs.AssertCorrectInitialization() 192 } 193 194 // TestInit_EpochStakingPhase tests initializing the component in the EpochSetup phase. 195 // Measurement and epoch info should be initialized, next epoch final view should be set. 196 func (bs *BlockTimeControllerSuite) TestInit_EpochSetupPhase() { 197 nextEpoch := mockprotocol.NewEpoch(bs.T()) 198 nextEpoch.On("Counter").Return(bs.epochCounter+1, nil) 199 nextEpoch.On("FinalView").Return(bs.curEpochFinalView+100_000, nil) 200 bs.epochs.Add(nextEpoch) 201 202 bs.CreateAndStartController() 203 defer bs.StopController() 204 bs.AssertCorrectInitialization() 205 } 206 207 // TestInit_EpochFallbackTriggered tests initializing the component when epoch fallback is triggered. 208 // Default GetProposalTiming should be set. 209 func (bs *BlockTimeControllerSuite) TestInit_EpochFallbackTriggered() { 210 bs.epochFallbackTriggered = true 211 bs.CreateAndStartController() 212 defer bs.StopController() 213 bs.AssertCorrectInitialization() 214 } 215 216 // TestEpochFallbackTriggered tests epoch fallback: 217 // - the GetProposalTiming should revert to default 218 // - duplicate events should be no-ops 219 func (bs *BlockTimeControllerSuite) TestEpochFallbackTriggered() { 220 bs.CreateAndStartController() 221 defer bs.StopController() 222 223 // update error so that GetProposalTiming is non-default 224 bs.ctl.proportionalErr.AddObservation(20.0) 225 bs.ctl.integralErr.AddObservation(20.0) 226 err := bs.ctl.measureViewDuration(makeTimedBlock(bs.initialView+1, unittest.IdentifierFixture(), time.Now())) 227 require.NoError(bs.T(), err) 228 assert.NotEqual(bs.T(), bs.config.FallbackProposalDelay, bs.ctl.GetProposalTiming()) 229 230 // send the event 231 bs.ctl.EpochEmergencyFallbackTriggered() 232 // async: should revert to default GetProposalTiming 233 require.Eventually(bs.T(), func() bool { 234 now := time.Now().UTC() 235 return now.Add(bs.config.FallbackProposalDelay.Load()) == bs.ctl.GetProposalTiming().TargetPublicationTime(7, now, unittest.IdentifierFixture()) 236 }, time.Second, time.Millisecond) 237 238 // additional EpochEmergencyFallbackTriggered events should be no-ops 239 // (send capacity+1 events to guarantee one is processed) 240 for i := 0; i <= cap(bs.ctl.epochFallbacks); i++ { 241 bs.ctl.EpochEmergencyFallbackTriggered() 242 } 243 // state should be unchanged 244 now := time.Now().UTC() 245 assert.Equal(bs.T(), now.Add(bs.config.FallbackProposalDelay.Load()), bs.ctl.GetProposalTiming().TargetPublicationTime(12, now, unittest.IdentifierFixture())) 246 247 // additional OnBlockIncorporated events should be no-ops 248 for i := 0; i <= cap(bs.ctl.incorporatedBlocks); i++ { 249 header := unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView + 1 + uint64(i))) 250 header.ParentID = unittest.IdentifierFixture() 251 bs.ctl.OnBlockIncorporated(model.BlockFromFlow(header)) 252 } 253 // wait for the channel to drain, since OnBlockIncorporated doesn't block on sending 254 require.Eventually(bs.T(), func() bool { 255 return len(bs.ctl.incorporatedBlocks) == 0 256 }, time.Second, time.Millisecond) 257 // state should be unchanged 258 now = time.Now().UTC() 259 assert.Equal(bs.T(), now.Add(bs.config.FallbackProposalDelay.Load()), bs.ctl.GetProposalTiming().TargetPublicationTime(17, now, unittest.IdentifierFixture())) 260 } 261 262 // TestOnBlockIncorporated_UpdateProposalDelay tests that a new measurement is taken and 263 // GetProposalTiming updated upon receiving an OnBlockIncorporated event. 264 func (bs *BlockTimeControllerSuite) TestOnBlockIncorporated_UpdateProposalDelay() { 265 bs.CreateAndStartController() 266 defer bs.StopController() 267 268 initialControllerState := captureControllerStateDigest(bs.ctl) // copy initial controller state 269 initialProposalDelay := bs.ctl.GetProposalTiming() 270 block := model.BlockFromFlow(unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView + 1))) 271 bs.ctl.OnBlockIncorporated(block) 272 require.Eventually(bs.T(), func() bool { 273 return bs.ctl.GetProposalTiming().ObservationView() > bs.initialView 274 }, time.Second, time.Millisecond) 275 nextControllerState := captureControllerStateDigest(bs.ctl) 276 nextProposalDelay := bs.ctl.GetProposalTiming() 277 278 bs.SanityCheckSubsequentMeasurements(initialControllerState, nextControllerState, false) 279 // new measurement should update GetProposalTiming 280 now := time.Now().UTC() 281 assert.NotEqual(bs.T(), 282 initialProposalDelay.TargetPublicationTime(bs.initialView+2, now, unittest.IdentifierFixture()), 283 nextProposalDelay.TargetPublicationTime(bs.initialView+2, now, block.BlockID)) 284 285 // duplicate events should be no-ops 286 for i := 0; i <= cap(bs.ctl.incorporatedBlocks); i++ { 287 bs.ctl.OnBlockIncorporated(block) 288 } 289 // wait for the channel to drain, since OnBlockIncorporated doesn't block on sending 290 require.Eventually(bs.T(), func() bool { 291 return len(bs.ctl.incorporatedBlocks) == 0 292 }, time.Second, time.Millisecond) 293 294 // state should be unchanged 295 finalControllerState := captureControllerStateDigest(bs.ctl) 296 bs.SanityCheckSubsequentMeasurements(nextControllerState, finalControllerState, true) 297 assert.Equal(bs.T(), nextProposalDelay, bs.ctl.GetProposalTiming()) 298 } 299 300 // TestEnableDisable tests that the controller responds to enabling and disabling. 301 func (bs *BlockTimeControllerSuite) TestEnableDisable() { 302 // start in a disabled state 303 err := bs.config.SetEnabled(false) 304 require.NoError(bs.T(), err) 305 bs.CreateAndStartController() 306 defer bs.StopController() 307 308 now := time.Now() 309 310 initialControllerState := captureControllerStateDigest(bs.ctl) 311 initialProposalDelay := bs.ctl.GetProposalTiming() 312 // the initial proposal timing should use fallback timing 313 assert.Equal(bs.T(), now.Add(bs.ctl.config.FallbackProposalDelay.Load()), initialProposalDelay.TargetPublicationTime(bs.initialView+1, now, unittest.IdentifierFixture())) 314 315 block := model.BlockFromFlow(unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView + 1))) 316 bs.ctl.OnBlockIncorporated(block) 317 require.Eventually(bs.T(), func() bool { 318 return bs.ctl.GetProposalTiming().ObservationView() > bs.initialView 319 }, time.Second, time.Millisecond) 320 secondProposalDelay := bs.ctl.GetProposalTiming() 321 322 // new measurement should not change GetProposalTiming 323 assert.Equal(bs.T(), 324 initialProposalDelay.TargetPublicationTime(bs.initialView+2, now, unittest.IdentifierFixture()), 325 secondProposalDelay.TargetPublicationTime(bs.initialView+2, now, block.BlockID)) 326 327 // now, enable the controller 328 err = bs.ctl.config.SetEnabled(true) 329 require.NoError(bs.T(), err) 330 331 // send another block 332 block = model.BlockFromFlow(unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView + 2))) 333 bs.ctl.OnBlockIncorporated(block) 334 require.Eventually(bs.T(), func() bool { 335 return bs.ctl.GetProposalTiming().ObservationView() > bs.initialView 336 }, time.Second, time.Millisecond) 337 338 thirdControllerState := captureControllerStateDigest(bs.ctl) 339 thirdProposalDelay := bs.ctl.GetProposalTiming() 340 341 // new measurement should change GetProposalTiming 342 bs.SanityCheckSubsequentMeasurements(initialControllerState, thirdControllerState, false) 343 assert.NotEqual(bs.T(), 344 initialProposalDelay.TargetPublicationTime(bs.initialView+3, now, unittest.IdentifierFixture()), 345 thirdProposalDelay.TargetPublicationTime(bs.initialView+3, now, block.BlockID)) 346 347 } 348 349 // TestOnBlockIncorporated_EpochTransition_Enabled tests epoch transition with controller enabled. 350 func (bs *BlockTimeControllerSuite) TestOnBlockIncorporated_EpochTransition_Enabled() { 351 err := bs.ctl.config.SetEnabled(true) 352 require.NoError(bs.T(), err) 353 bs.testOnBlockIncorporated_EpochTransition() 354 } 355 356 // TestOnBlockIncorporated_EpochTransition_Disabled tests epoch transition with controller disabled. 357 func (bs *BlockTimeControllerSuite) TestOnBlockIncorporated_EpochTransition_Disabled() { 358 err := bs.ctl.config.SetEnabled(false) 359 require.NoError(bs.T(), err) 360 bs.testOnBlockIncorporated_EpochTransition() 361 } 362 363 // testOnBlockIncorporated_EpochTransition tests that a view change into the next epoch 364 // updates the local state to reflect the new epoch. 365 func (bs *BlockTimeControllerSuite) testOnBlockIncorporated_EpochTransition() { 366 nextEpoch := mockprotocol.NewEpoch(bs.T()) 367 nextEpoch.On("Counter").Return(bs.epochCounter+1, nil) 368 nextEpoch.On("FinalView").Return(bs.curEpochFinalView+100_000, nil) 369 bs.epochs.Add(nextEpoch) 370 bs.CreateAndStartController() 371 defer bs.StopController() 372 373 initialControllerState := captureControllerStateDigest(bs.ctl) 374 bs.epochs.Transition() 375 timedBlock := makeTimedBlock(bs.curEpochFinalView+1, unittest.IdentifierFixture(), time.Now().UTC()) 376 err := bs.ctl.processIncorporatedBlock(timedBlock) 377 require.True(bs.T(), bs.ctl.GetProposalTiming().ObservationView() > bs.initialView) 378 require.NoError(bs.T(), err) 379 nextControllerState := captureControllerStateDigest(bs.ctl) 380 381 bs.SanityCheckSubsequentMeasurements(initialControllerState, nextControllerState, false) 382 // epoch boundaries should be updated 383 assert.Equal(bs.T(), bs.curEpochFinalView+1, bs.ctl.epochInfo.curEpochFirstView) 384 assert.Equal(bs.T(), bs.ctl.epochInfo.curEpochFinalView, bs.curEpochFinalView+100_000) 385 assert.Nil(bs.T(), bs.ctl.nextEpochFinalView) 386 } 387 388 // TestOnEpochSetupPhaseStarted ensures that the epoch info is updated when the next epoch is set up. 389 func (bs *BlockTimeControllerSuite) TestOnEpochSetupPhaseStarted() { 390 nextEpoch := mockprotocol.NewEpoch(bs.T()) 391 nextEpoch.On("Counter").Return(bs.epochCounter+1, nil) 392 nextEpoch.On("FinalView").Return(bs.curEpochFinalView+100_000, nil) 393 bs.epochs.Add(nextEpoch) 394 bs.CreateAndStartController() 395 defer bs.StopController() 396 397 header := unittest.BlockHeaderFixture() 398 bs.ctl.EpochSetupPhaseStarted(bs.epochCounter, header) 399 require.Eventually(bs.T(), func() bool { 400 return bs.ctl.nextEpochFinalView != nil 401 }, time.Second, time.Millisecond) 402 403 assert.Equal(bs.T(), bs.curEpochFinalView+100_000, *bs.ctl.nextEpochFinalView) 404 405 // duplicate events should be no-ops 406 for i := 0; i <= cap(bs.ctl.epochSetups); i++ { 407 bs.ctl.EpochSetupPhaseStarted(bs.epochCounter, header) 408 } 409 assert.Equal(bs.T(), bs.curEpochFinalView+100_000, *bs.ctl.nextEpochFinalView) 410 } 411 412 // TestProposalDelay_AfterTargetTransitionTime tests the behaviour of the controller 413 // when we have passed the target end time for the current epoch. 414 // We should approach the min GetProposalTiming (increase view rate as much as possible) 415 func (bs *BlockTimeControllerSuite) TestProposalDelay_AfterTargetTransitionTime() { 416 // we are near the end of the epoch in view terms 417 bs.initialView = uint64(float64(bs.curEpochFinalView) * .95) 418 bs.CreateAndStartController() 419 defer bs.StopController() 420 421 lastProposalDelay := time.Hour // start with large dummy value 422 for view := bs.initialView + 1; view < bs.ctl.curEpochFinalView; view++ { 423 // we have passed the target end time of the epoch 424 receivedParentBlockAt := bs.ctl.curEpochTargetEndTime.Add(time.Duration(view) * time.Second) 425 timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), receivedParentBlockAt) 426 err := bs.ctl.measureViewDuration(timedBlock) 427 require.NoError(bs.T(), err) 428 429 // compute proposal delay: 430 pubTime := bs.ctl.GetProposalTiming().TargetPublicationTime(view+1, time.Now().UTC(), timedBlock.Block.BlockID) // simulate building a child of `timedBlock` 431 delay := pubTime.Sub(receivedParentBlockAt) 432 433 assert.LessOrEqual(bs.T(), delay, lastProposalDelay) 434 lastProposalDelay = delay 435 436 // transition views until the end of the epoch, or for 100 views 437 if view-bs.initialView >= 100 { 438 break 439 } 440 } 441 } 442 443 // TestProposalDelay_BehindSchedule tests the behaviour of the controller when the 444 // projected epoch switchover is LATER than the target switchover time, i.e. 445 // we are behind schedule. 446 // We should respond by lowering the GetProposalTiming (increasing view rate) 447 func (bs *BlockTimeControllerSuite) TestProposalDelay_BehindSchedule() { 448 // we are 50% of the way through the epoch in view terms 449 bs.initialView = uint64(float64(bs.curEpochFinalView) * .5) 450 bs.CreateAndStartController() 451 defer bs.StopController() 452 453 lastProposalDelay := time.Hour // start with large dummy value 454 idealEnteredViewTime := bs.ctl.curEpochTargetEndTime.Add(-epochLength / 2) 455 456 // 1s behind of schedule 457 receivedParentBlockAt := idealEnteredViewTime.Add(time.Second) 458 for view := bs.initialView + 1; view < bs.ctl.curEpochFinalView; view++ { 459 // hold the instantaneous error constant for each view 460 receivedParentBlockAt = receivedParentBlockAt.Add(seconds2Duration(bs.ctl.targetViewTime())) 461 timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), receivedParentBlockAt) 462 err := bs.ctl.measureViewDuration(timedBlock) 463 require.NoError(bs.T(), err) 464 465 // compute proposal delay: 466 pubTime := bs.ctl.GetProposalTiming().TargetPublicationTime(view+1, time.Now().UTC(), timedBlock.Block.BlockID) // simulate building a child of `timedBlock` 467 delay := pubTime.Sub(receivedParentBlockAt) 468 // expecting decreasing GetProposalTiming 469 assert.LessOrEqual(bs.T(), delay, lastProposalDelay) 470 lastProposalDelay = delay 471 472 // transition views until the end of the epoch, or for 100 views 473 if view-bs.initialView >= 100 { 474 break 475 } 476 } 477 } 478 479 // TestProposalDelay_AheadOfSchedule tests the behaviour of the controller when the 480 // projected epoch switchover is EARLIER than the target switchover time, i.e. 481 // we are ahead of schedule. 482 // We should respond by increasing the GetProposalTiming (lowering view rate) 483 func (bs *BlockTimeControllerSuite) TestProposalDelay_AheadOfSchedule() { 484 // we are 50% of the way through the epoch in view terms 485 bs.initialView = uint64(float64(bs.curEpochFinalView) * .5) 486 bs.CreateAndStartController() 487 defer bs.StopController() 488 489 lastProposalDelay := time.Duration(0) // start with large dummy value 490 idealEnteredViewTime := bs.ctl.curEpochTargetEndTime.Add(-epochLength / 2) 491 // 1s ahead of schedule 492 receivedParentBlockAt := idealEnteredViewTime.Add(-time.Second) 493 for view := bs.initialView + 1; view < bs.ctl.curEpochFinalView; view++ { 494 // hold the instantaneous error constant for each view 495 receivedParentBlockAt = receivedParentBlockAt.Add(seconds2Duration(bs.ctl.targetViewTime())) 496 timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), receivedParentBlockAt) 497 err := bs.ctl.measureViewDuration(timedBlock) 498 require.NoError(bs.T(), err) 499 500 // compute proposal delay: 501 pubTime := bs.ctl.GetProposalTiming().TargetPublicationTime(view+1, time.Now().UTC(), timedBlock.Block.BlockID) // simulate building a child of `timedBlock` 502 delay := pubTime.Sub(receivedParentBlockAt) 503 504 // expecting increasing GetProposalTiming 505 assert.GreaterOrEqual(bs.T(), delay, lastProposalDelay) 506 lastProposalDelay = delay 507 508 // transition views until the end of the epoch, or for 100 views 509 if view-bs.initialView >= 100 { 510 break 511 } 512 } 513 } 514 515 // TestMetrics tests that correct metrics are tracked when expected. 516 func (bs *BlockTimeControllerSuite) TestMetrics() { 517 bs.metrics = *mockmodule.NewCruiseCtlMetrics(bs.T()) 518 // should set metrics upon initialization 519 bs.metrics.On("PIDError", float64(0), float64(0), float64(0)).Once() 520 bs.metrics.On("TargetProposalDuration", time.Duration(0)).Once() 521 bs.metrics.On("ControllerOutput", time.Duration(0)).Once() 522 bs.CreateAndStartController() 523 defer bs.StopController() 524 bs.metrics.AssertExpectations(bs.T()) 525 526 // we are at view 1 of the epoch, but the time is suddenly the target end time 527 enteredViewAt := bs.ctl.curEpochTargetEndTime 528 view := bs.initialView + 1 529 // we should observe a large error 530 bs.metrics.On("PIDError", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 531 p := args[0].(float64) 532 i := args[1].(float64) 533 d := args[2].(float64) 534 assert.Greater(bs.T(), p, float64(0)) 535 assert.Greater(bs.T(), i, float64(0)) 536 assert.Greater(bs.T(), d, float64(0)) 537 }).Once() 538 // should immediately use min proposal duration 539 bs.metrics.On("TargetProposalDuration", bs.config.MinViewDuration.Load()).Once() 540 // should have a large negative controller output 541 bs.metrics.On("ControllerOutput", mock.Anything).Run(func(args mock.Arguments) { 542 output := args[0].(time.Duration) 543 assert.Greater(bs.T(), output, time.Duration(0)) 544 }).Once() 545 546 timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), enteredViewAt) 547 err := bs.ctl.measureViewDuration(timedBlock) 548 require.NoError(bs.T(), err) 549 } 550 551 // Test_vs_PythonSimulation performs a regression test. We implemented the controller in python 552 // together with a statistical model for the view duration. We used the python implementation to tune 553 // the PID controller parameters which we are using here. 554 // In this test, we feed values pre-generated with the python simulation into the Go implementation 555 // and compare the outputs to the pre-generated outputs from the python controller implementation. 556 func (bs *BlockTimeControllerSuite) Test_vs_PythonSimulation() { 557 // PART 1: setup system to mirror python simulation 558 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 559 totalEpochViews := 483000 560 bs.initialView = 0 561 bs.curEpochFirstView, bs.curEpochFinalView = uint64(0), uint64(totalEpochViews-1) // views [0, .., totalEpochViews-1] 562 bs.epochFallbackTriggered = false 563 564 refT := time.Now().UTC() 565 refT = time.Date(refT.Year(), refT.Month(), refT.Day(), refT.Hour(), refT.Minute(), 0, 0, time.UTC) // truncate to past minute 566 bs.config = &Config{ 567 TimingConfig: TimingConfig{ 568 TargetTransition: EpochTransitionTime{day: refT.Weekday(), hour: uint8(refT.Hour()), minute: uint8(refT.Minute())}, 569 FallbackProposalDelay: atomic.NewDuration(500 * time.Millisecond), // irrelevant for this test, as controller should never enter fallback mode 570 MinViewDuration: atomic.NewDuration(470 * time.Millisecond), 571 MaxViewDuration: atomic.NewDuration(2010 * time.Millisecond), 572 Enabled: atomic.NewBool(true), 573 }, 574 ControllerParams: ControllerParams{KP: 2.0, KI: 0.06, KD: 3.0, N_ewma: 5, N_itg: 50}, 575 } 576 577 setupMocks(bs) 578 bs.CreateAndStartController() 579 defer bs.StopController() 580 581 // PART 2: timing generated from python simulation and corresponding controller response 582 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 583 ref := struct { 584 // targetViewTime is the idealized view duration of a perfect system. 585 // In Python simulation, this is the array `EpochSimulation.ideal_view_time` 586 targetViewTime float64 // units: seconds 587 588 // observedMinViewTimes[i] is the minimal time required time to execute the protocol for view i 589 // - Duration from the primary observing the parent block (indexed by i) to having its child proposal (block for view i+1) ready for publication. 590 // - This is the minimal time required to execute the protocol. Nodes can only delay their proposal but not progress any faster. 591 // - in Python simulation, this is the array `EpochSimulation.min_view_times + EpochSimulation.observation_noise` 592 // with is returned by function `EpochSimulation.current_view_observation()` 593 // Note that this is generally different than the time it takes the committee as a whole to transition 594 // through views. This is because the primary changes from view to view, and nodes observe blocks at slightly 595 // different times (small noise term). The real world (as well as the simulation) depend on collective swarm 596 // behaviour of the consensus committee, which is not observable by nodes individually. 597 // In contrast, our `observedMinViewTimes` here contains an additional noise term, to emulate 598 // the observations of a node in the real world. 599 observedMinViewTimes []float64 // units: seconds 600 601 // controllerTargetedViewDuration[i] is the duration targeted by the Python controller : 602 // - measured from observing the parent until publishing the child block for view i+1 603 controllerTargetedViewDuration []float64 // units: seconds 604 605 // realWorldViewDuration[i] is the duration of the ith view for the entire committee. 606 // This value occurs in response to the controller output and is not observable by nodes individually. 607 // - in Python simulation, this is the array `EpochSimulation.real_world_view_duration` 608 // with is recorded by the environment upon the call of `EpochSimulation.delay()` 609 realWorldViewDuration []float64 // units: seconds 610 }{ 611 targetViewTime: 1.2521739130434784, 612 observedMinViewTimes: []float64{0.8139115907362099, 0.7093851608587579, 0.7370057913407495, 0.8378050305605419, 0.8221876685439506, 0.8129097289534515, 0.7835810854212116, 0.7419219104134447, 0.7122331139614623, 0.7263645183403751, 1.2481399484109290, 0.8741906105412369, 0.7082127929564489, 0.8175969272012624, 0.8040687048886446, 0.8163336940928989, 0.6354390018677689, 1.0568897015119771, 0.8283653995502240, 0.8649826738831023, 0.7249163864295024, 0.6572694879104934, 0.8796994117267707, 0.8251533370085626, 0.8383599333817994, 0.7561765091071196, 1.4239532706257330, 2.3848404271162811, 0.6997792104740760, 0.6783155065018911, 0.7397146999404549, 0.7568604144415827, 0.8224399309953295, 0.8635091458596464, 0.6292564656694590, 0.6399775559845721, 0.7551854294536755, 0.7493031513209824, 0.7916989850940226, 0.8584875376770561, 0.5733027665412744, 0.8190610271623866, 0.6664088123579012, 0.6856899641942998, 0.8235905136098289, 0.7673984464333541, 0.7514768668170753, 0.7145945518569533, 0.8076879859786521, 0.6890844388873341, 0.7782307638665685, 1.0031597171903470, 0.8056874789572074, 1.1894678554682030, 0.7751504335630999, 0.6598342159237116, 0.7198783916113262, 0.7231184452829420, 0.7291287772166142, 0.8941150065282033, 0.8216597987064465, 0.7074775436893693, 0.7886375844003763, 0.8028714839193359, 0.6473851384702657, 0.8247230728633490, 0.8268367270238434, 0.7776181863431995, 1.2870341252966155, 0.9022036087098005, 0.8608476621564736, 0.7448392402085238, 0.7030664985775897, 0.7343372879803260, 0.8501776646839836, 0.7949969493471933, 0.7030853022640485, 0.8506339844198412, 0.8520038195041865, 1.2159232403369129, 0.9501009619276108, 0.7063032843664507, 0.7676066345629766, 0.8050982844953996, 0.7460373897798731, 0.7531147127154058, 0.8276552672727131, 0.6777639708691676, 0.7759833549063068, 0.8861636486602165, 0.8272606701022402, 0.6742194284453155, 0.8270012408910985, 1.0799793512385585, 0.8343711941947437, 0.6424938240651709, 0.8314721058034046, 0.8687591599744876, 0.7681132139163648, 0.7993270549538212}, 613 realWorldViewDuration: []float64{1.2707067231074189, 1.3797713099533957, 1.1803368837187869, 1.0710943548975358, 1.3055277182347431, 1.3142312827952587, 1.2748087784689972, 1.2580713757160862, 1.2389594986278398, 1.2839951451881206, 0.8404551372521588, 1.7402295383244093, 1.2486807727203340, 1.1529076722170450, 1.2303564416007062, 1.1919067015405667, 1.4317417513319299, 0.8851802701506968, 1.4621618954558588, 1.2629599000198048, 1.3845528649513363, 1.3083813148510797, 1.0320875660949032, 1.2138806234836066, 1.2922205615230111, 1.3530469860253094, 1.5124780338765653, 2.4800000000000000, 0.8339877775027843, 0.7270580752471872, 0.8013511652567021, 0.7489973886099706, 0.9647668631144197, 1.4406086304771719, 1.6376005221775904, 1.3686144679115566, 1.2051140074616571, 1.2232170397428770, 1.1785015757024468, 1.2720488631325702, 1.4845607775546621, 1.0038608184511295, 1.4011693227324362, 1.2782420466946043, 1.0808595015305793, 1.2923716723984215, 1.2876404222029678, 1.3024029638718018, 1.1243308902566644, 1.3825311808461356, 1.1826028495527394, 1.0753560400260920, 1.4587594729770430, 1.3281281084314180, 1.1987898717701806, 1.3212567274973721, 1.2131355949220173, 1.2202213287069972, 1.2345177139086974, 1.1415707241388824, 1.2618615652263814, 1.3978228798726429, 1.1676202853133009, 1.2821402577607839, 1.4378331263208257, 1.0764974304705950, 1.1968636840861584, 1.3079197545950789, 1.3246769344178762, 1.0956265919521080, 1.3056225547363036, 1.3094504040915045, 1.2916519124885637, 1.2995343661957905, 1.0839793112463321, 1.2515453598485311, 1.3907042923175941, 1.1137329234266407, 1.2293962485228747, 1.4537855131563087, 1.1564260868809058, 1.2616419368628695, 1.1777963280146100, 1.2782540498222059, 1.2617698479511545, 1.2911000941099631, 1.1719344274281953, 1.3904853415093545, 1.1612440756337188, 1.1800751870755894, 1.2653752924717137, 1.3987404424771417, 1.1573292016433725, 1.2132227320045601, 1.2835627159341194, 1.3950341330597937, 1.0774862045842490, 1.2361956384863142, 1.3415505497959577, 1.1881870996394799}, 614 controllerTargetedViewDuration: []float64{1.2521739130434784, 1.2325291342837938, 1.0924796023620962, 1.1315714628442570, 1.3109201861848649, 1.2904005140483341, 1.2408200617376139, 1.2143186827596988, 1.2001258197216824, 1.2059386524427240, 1.1687014183641575, 1.5938588248347272, 1.1735049856838198, 1.1322996720968055, 1.2010702989934061, 1.2193620268012733, 1.2847380812524840, 1.1111384877632171, 1.4676632072726421, 1.3127404884038874, 1.3036822199799039, 1.1627828776831781, 1.0686746584877680, 1.2585854668086294, 1.3196479113378341, 1.3040688380370420, 1.2092520716891777, 0.9174437864843878, 0.4700000000000000, 0.4700000000000000, 0.4700000000000000, 0.4700000000000000, 0.9677983536241768, 1.4594930877396231, 1.4883132720086421, 1.2213393879261234, 1.1167787676139602, 1.1527862655996910, 1.1844688515164143, 1.2712560882996764, 1.2769188516898307, 1.0483030535756364, 1.2667785513482170, 1.1360673946540731, 1.0930571503977162, 1.2553993593963664, 1.2412509734564154, 1.2173708810202102, 1.1668170515618597, 1.2919854192770974, 1.1785774891590928, 1.2397180299682444, 1.4349751903776191, 1.2686663464941463, 1.1793337443757632, 1.2094760506747269, 1.1283680467942478, 1.1456014869605273, 1.1695603482439110, 1.1883473989997737, 1.3102878097954334, 1.3326636354319201, 1.2033095908546276, 1.2765637682955560, 1.2533105511679674, 1.0561925258579383, 1.1944030230453759, 1.2584181515051163, 1.2181701773236133, 1.1427643645565180, 1.2912929540520488, 1.2606456249879283, 1.2079980980125691, 1.1582846527456185, 1.0914599072895725, 1.2436632334468321, 1.2659732625682767, 1.1373906460646186, 1.2636670215783354, 1.3065542716228340, 1.1145058661373550, 1.1821457478344533, 1.1686494999739092, 1.2421504164081945, 1.2292642544361261, 1.2247229593559099, 1.1857675147732030, 1.2627704665069508, 1.1302481979483210, 1.2027256964130453, 1.2826968566299934, 1.2903197193121982, 1.1497164007008540, 1.2248494620352162, 1.2695192555858241, 1.2492112043621006, 1.1006141873118667, 1.2513218024356318, 1.2846249908259910, 1.2077144025965167}, 615 } 616 617 // PART 3: run controller and ensure output matches pre-generated controller response from python ref implementation 618 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 619 // sanity checks: 620 require.Equal(bs.T(), 604800.0, bs.ctl.curEpochTargetEndTime.UTC().Sub(refT).Seconds(), "Epoch should end 1 week from now, i.e. 604800s") 621 require.InEpsilon(bs.T(), ref.targetViewTime, bs.ctl.targetViewTime(), 1e-15) // ideal view time 622 require.Equal(bs.T(), len(ref.observedMinViewTimes), len(ref.realWorldViewDuration)) 623 624 // Notes: 625 // - We specifically make the first observation at when the full time of the epoch is left. 626 // The python simulation we compare with proceed exactly the same way. 627 // - we first make an observation, before requesting the controller output. Thereby, we 628 // avoid artifacts of recalling a controller that was just initialized with fallback values. 629 // - we call `measureViewDuration(..)` (_not_ `processIncorporatedBlock(..)`) to 630 // interfering with the deduplication logic. Here we want to test correct numerics. 631 // Correctness of the deduplication logic is verified in the different test. 632 observationTime := refT 633 634 for v := 0; v < len(ref.observedMinViewTimes); v++ { 635 observedBlock := makeTimedBlock(uint64(v), unittest.IdentifierFixture(), observationTime) 636 err := bs.ctl.measureViewDuration(observedBlock) 637 require.NoError(bs.T(), err) 638 proposalTiming := bs.ctl.GetProposalTiming() 639 tpt := proposalTiming.TargetPublicationTime(uint64(v+1), time.Now(), observedBlock.Block.BlockID) // value for `timeViewEntered` should be irrelevant here 640 641 controllerTargetedViewDuration := tpt.Sub(observedBlock.TimeObserved).Seconds() 642 require.InEpsilon(bs.T(), ref.controllerTargetedViewDuration[v], controllerTargetedViewDuration, 1e-5, "implementations deviate for view %d", v) // ideal view time 643 644 observationTime = observationTime.Add(time.Duration(int64(ref.realWorldViewDuration[v] * float64(time.Second)))) 645 } 646 647 } 648 649 func makeTimedBlock(view uint64, parentID flow.Identifier, time time.Time) TimedBlock { 650 header := unittest.BlockHeaderFixture(unittest.HeaderWithView(view)) 651 header.ParentID = parentID 652 return TimedBlock{ 653 Block: model.BlockFromFlow(header), 654 TimeObserved: time, 655 } 656 } 657 658 type controllerStateDigest struct { 659 proportionalErr Ewma 660 integralErr LeakyIntegrator 661 662 // latestProposalTiming holds the ProposalTiming that the controller generated in response to processing the latest observation 663 latestProposalTiming ProposalTiming 664 } 665 666 func captureControllerStateDigest(ctl *BlockTimeController) *controllerStateDigest { 667 return &controllerStateDigest{ 668 proportionalErr: ctl.proportionalErr, 669 integralErr: ctl.integralErr, 670 latestProposalTiming: ctl.GetProposalTiming(), 671 } 672 } 673 674 func seconds2Duration(durationinDeconds float64) time.Duration { 675 return time.Duration(int64(durationinDeconds * float64(time.Second))) 676 }