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  }