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

     1  package cruisectl
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     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  	"go.uber.org/atomic"
    15  
    16  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    17  	"github.com/onflow/flow-go/model/flow"
    18  	"github.com/onflow/flow-go/module/irrecoverable"
    19  	mockmodule "github.com/onflow/flow-go/module/mock"
    20  	mockprotocol "github.com/onflow/flow-go/state/protocol/mock"
    21  	"github.com/onflow/flow-go/utils/unittest"
    22  	"github.com/onflow/flow-go/utils/unittest/mocks"
    23  )
    24  
    25  // BlockTimeControllerSuite encapsulates tests for the BlockTimeController.
    26  type BlockTimeControllerSuite struct {
    27  	suite.Suite
    28  
    29  	initialView            uint64
    30  	epochCounter           uint64
    31  	curEpochFirstView      uint64
    32  	curEpochFinalView      uint64
    33  	curEpochTargetDuration uint64
    34  	curEpochTargetEndTime  uint64
    35  	epochFallbackTriggered bool
    36  
    37  	metrics  mockmodule.CruiseCtlMetrics
    38  	state    mockprotocol.State
    39  	params   mockprotocol.Params
    40  	snapshot mockprotocol.Snapshot
    41  	epochs   mocks.EpochQuery
    42  	curEpoch mockprotocol.Epoch
    43  
    44  	config *Config
    45  	ctx    irrecoverable.SignalerContext
    46  	cancel context.CancelFunc
    47  	ctl    *BlockTimeController
    48  }
    49  
    50  func TestBlockTimeController(t *testing.T) {
    51  	suite.Run(t, new(BlockTimeControllerSuite))
    52  }
    53  
    54  // EpochDurationSeconds returns the number of seconds in the epoch (1hr).
    55  func (bs *BlockTimeControllerSuite) EpochDurationSeconds() uint64 {
    56  	return 60 * 60
    57  }
    58  
    59  // SetupTest initializes mocks and default values.
    60  func (bs *BlockTimeControllerSuite) SetupTest() {
    61  	bs.config = DefaultConfig()
    62  	bs.config.MaxViewDuration = atomic.NewDuration(2 * time.Second)
    63  	bs.config.Enabled.Store(true)
    64  	bs.initialView = 0
    65  	bs.epochCounter = uint64(0)
    66  	bs.curEpochFirstView = uint64(0)
    67  	bs.curEpochFinalView = bs.EpochDurationSeconds() - 1 // 1 view/sec for 1hr epoch; term `-1` is needed because view 0 also takes 1 second
    68  	bs.curEpochTargetDuration = bs.EpochDurationSeconds()
    69  	bs.curEpochTargetEndTime = uint64(time.Now().Unix()) + bs.EpochDurationSeconds()
    70  	bs.epochFallbackTriggered = false
    71  	setupMocks(bs)
    72  }
    73  
    74  func setupMocks(bs *BlockTimeControllerSuite) {
    75  	bs.metrics = *mockmodule.NewCruiseCtlMetrics(bs.T())
    76  	bs.metrics.On("PIDError", mock.Anything, mock.Anything, mock.Anything).Maybe()
    77  	bs.metrics.On("TargetProposalDuration", mock.Anything).Maybe()
    78  	bs.metrics.On("ControllerOutput", mock.Anything).Maybe()
    79  
    80  	bs.state = *mockprotocol.NewState(bs.T())
    81  	bs.params = *mockprotocol.NewParams(bs.T())
    82  	bs.snapshot = *mockprotocol.NewSnapshot(bs.T())
    83  	bs.epochs = *mocks.NewEpochQuery(bs.T(), bs.epochCounter)
    84  	bs.curEpoch = *mockprotocol.NewEpoch(bs.T())
    85  
    86  	bs.state.On("Final").Return(&bs.snapshot)
    87  	bs.state.On("AtHeight", mock.Anything).Return(&bs.snapshot).Maybe()
    88  	bs.state.On("Params").Return(&bs.params)
    89  	bs.params.On("EpochFallbackTriggered").Return(
    90  		func() bool { return bs.epochFallbackTriggered },
    91  		func() error { return nil })
    92  	bs.snapshot.On("Phase").Return(
    93  		func() flow.EpochPhase { return bs.epochs.Phase() },
    94  		func() error { return nil })
    95  	bs.snapshot.On("Head").Return(unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView+11)), nil).Maybe()
    96  	bs.snapshot.On("Epochs").Return(&bs.epochs)
    97  	bs.curEpoch.On("Counter").Return(bs.epochCounter, nil)
    98  	bs.curEpoch.On("FirstView").Return(bs.curEpochFirstView, nil)
    99  	bs.curEpoch.On("FinalView").Return(bs.curEpochFinalView, nil)
   100  	bs.curEpoch.On("TargetDuration").Return(bs.curEpochTargetDuration, nil)
   101  	bs.curEpoch.On("TargetEndTime").Return(bs.curEpochTargetEndTime, nil)
   102  	bs.epochs.Add(&bs.curEpoch)
   103  
   104  	bs.ctx, bs.cancel = irrecoverable.NewMockSignalerContextWithCancel(bs.T(), context.Background())
   105  }
   106  
   107  // CreateAndStartController creates and starts the BlockTimeController.
   108  // Should be called only once per test case.
   109  func (bs *BlockTimeControllerSuite) CreateAndStartController() {
   110  	ctl, err := NewBlockTimeController(unittest.Logger(), &bs.metrics, bs.config, &bs.state, bs.initialView)
   111  	require.NoError(bs.T(), err)
   112  	bs.ctl = ctl
   113  	bs.ctl.Start(bs.ctx)
   114  	unittest.RequireCloseBefore(bs.T(), bs.ctl.Ready(), time.Second, "component did not start")
   115  }
   116  
   117  // StopController stops the BlockTimeController.
   118  func (bs *BlockTimeControllerSuite) StopController() {
   119  	bs.cancel()
   120  	unittest.RequireCloseBefore(bs.T(), bs.ctl.Done(), time.Second, "component did not stop")
   121  }
   122  
   123  // AssertCorrectInitialization checks that the controller is configured as expected after construction.
   124  func (bs *BlockTimeControllerSuite) AssertCorrectInitialization() {
   125  	// at initialization, controller should be set up to release blocks without delay
   126  	controllerTiming := bs.ctl.getProposalTiming()
   127  	now := time.Now().UTC()
   128  
   129  	if bs.ctl.epochFallbackTriggered || !bs.ctl.config.Enabled.Load() {
   130  		// if epoch fallback is triggered or controller is disabled, use fallback timing
   131  		assert.Equal(bs.T(), now.Add(bs.ctl.config.FallbackProposalDelay.Load()), controllerTiming.TargetPublicationTime(7, now, unittest.IdentifierFixture()))
   132  	} else {
   133  		// otherwise should publish immediately
   134  		assert.Equal(bs.T(), now, controllerTiming.TargetPublicationTime(7, now, unittest.IdentifierFixture()))
   135  	}
   136  	if bs.ctl.epochFallbackTriggered {
   137  		return
   138  	}
   139  
   140  	// should initialize epoch info
   141  	epoch := bs.ctl.epochInfo
   142  	assert.Equal(bs.T(), bs.curEpochFirstView, epoch.curEpochFirstView)
   143  	assert.Equal(bs.T(), bs.curEpochFinalView, epoch.curEpochFinalView)
   144  	assert.Equal(bs.T(), bs.curEpochTargetDuration, epoch.curEpochTargetDuration)
   145  	assert.Equal(bs.T(), bs.curEpochTargetEndTime, epoch.curEpochTargetEndTime)
   146  
   147  	// if next epoch is set up, final view should be set
   148  	if phase := bs.epochs.Phase(); phase > flow.EpochPhaseStaking {
   149  		finalView, err := bs.epochs.Next().FinalView()
   150  		require.NoError(bs.T(), err)
   151  		assert.Equal(bs.T(), finalView, *epoch.nextEpochFinalView)
   152  	} else {
   153  		assert.Nil(bs.T(), epoch.nextEpochFinalView)
   154  	}
   155  
   156  	// should create an initial measurement
   157  	assert.Equal(bs.T(), bs.initialView, controllerTiming.ObservationView())
   158  	assert.WithinDuration(bs.T(), time.Now(), controllerTiming.ObservationTime(), time.Minute)
   159  	// errors should be initialized to zero
   160  	assert.Equal(bs.T(), float64(0), bs.ctl.proportionalErr.Value())
   161  	assert.Equal(bs.T(), float64(0), bs.ctl.integralErr.Value())
   162  }
   163  
   164  // SanityCheckSubsequentMeasurements checks that two consecutive states of the BlockTimeController are different or equal and
   165  // broadly reasonable. It does not assert exact values, because part of the measurements depend on timing in the worker.
   166  func (bs *BlockTimeControllerSuite) SanityCheckSubsequentMeasurements(d1, d2 *controllerStateDigest, expectedEqual bool) {
   167  	if expectedEqual {
   168  		// later input should have left state invariant, including the Observation
   169  		assert.Equal(bs.T(), d1.latestProposalTiming.ObservationTime(), d2.latestProposalTiming.ObservationTime())
   170  		assert.Equal(bs.T(), d1.latestProposalTiming.ObservationView(), d2.latestProposalTiming.ObservationView())
   171  		// new measurement should have same error
   172  		assert.Equal(bs.T(), d1.proportionalErr.Value(), d2.proportionalErr.Value())
   173  		assert.Equal(bs.T(), d1.integralErr.Value(), d2.integralErr.Value())
   174  	} else {
   175  		// later input should have caused a new Observation to be recorded
   176  		assert.True(bs.T(), d1.latestProposalTiming.ObservationTime().Before(d2.latestProposalTiming.ObservationTime()))
   177  		// new measurement should have different error
   178  		assert.NotEqual(bs.T(), d1.proportionalErr.Value(), d2.proportionalErr.Value())
   179  		assert.NotEqual(bs.T(), d1.integralErr.Value(), d2.integralErr.Value())
   180  	}
   181  }
   182  
   183  // PrintMeasurement prints the current state of the controller and the last measurement.
   184  func (bs *BlockTimeControllerSuite) PrintMeasurement(parentBlockId flow.Identifier) {
   185  	ctl := bs.ctl
   186  	m := ctl.getProposalTiming()
   187  	tpt := m.TargetPublicationTime(m.ObservationView()+1, m.ObservationTime(), parentBlockId)
   188  	fmt.Printf("v=%d\tt=%s\tPD=%s\te_N=%.3f\tI_M=%.3f\n",
   189  		m.ObservationView(), m.ObservationTime(), tpt.Sub(m.ObservationTime()),
   190  		ctl.proportionalErr.Value(), ctl.integralErr.Value())
   191  }
   192  
   193  // TestStartStop tests that the component can be started and stopped gracefully.
   194  func (bs *BlockTimeControllerSuite) TestStartStop() {
   195  	bs.CreateAndStartController()
   196  	bs.StopController()
   197  }
   198  
   199  // TestInit_EpochStakingPhase tests initializing the component in the EpochStaking phase.
   200  // Measurement and epoch info should be initialized, next epoch final view should be nil.
   201  func (bs *BlockTimeControllerSuite) TestInit_EpochStakingPhase() {
   202  	bs.CreateAndStartController()
   203  	defer bs.StopController()
   204  	bs.AssertCorrectInitialization()
   205  }
   206  
   207  // TestInit_EpochStakingPhase tests initializing the component in the EpochSetup phase.
   208  // Measurement and epoch info should be initialized, next epoch final view should be set.
   209  func (bs *BlockTimeControllerSuite) TestInit_EpochSetupPhase() {
   210  	nextEpoch := mockprotocol.NewEpoch(bs.T())
   211  	nextEpoch.On("Counter").Return(bs.epochCounter+1, nil)
   212  	nextEpoch.On("FinalView").Return(bs.curEpochFinalView*2, nil)
   213  	nextEpoch.On("TargetDuration").Return(bs.EpochDurationSeconds(), nil)
   214  	nextEpoch.On("TargetEndTime").Return(bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), nil)
   215  	bs.epochs.Add(nextEpoch)
   216  
   217  	bs.CreateAndStartController()
   218  	defer bs.StopController()
   219  	bs.AssertCorrectInitialization()
   220  }
   221  
   222  // TestInit_EpochFallbackTriggered tests initializing the component when epoch fallback is triggered.
   223  // Default GetProposalTiming should be set.
   224  func (bs *BlockTimeControllerSuite) TestInit_EpochFallbackTriggered() {
   225  	bs.epochFallbackTriggered = true
   226  	bs.CreateAndStartController()
   227  	defer bs.StopController()
   228  	bs.AssertCorrectInitialization()
   229  }
   230  
   231  // TestEpochFallbackTriggered tests epoch fallback:
   232  //   - the GetProposalTiming should revert to default
   233  //   - duplicate events should be no-ops
   234  func (bs *BlockTimeControllerSuite) TestEpochFallbackTriggered() {
   235  	bs.CreateAndStartController()
   236  	defer bs.StopController()
   237  
   238  	// update error so that GetProposalTiming is non-default
   239  	bs.ctl.proportionalErr.AddObservation(20.0)
   240  	bs.ctl.integralErr.AddObservation(20.0)
   241  	err := bs.ctl.measureViewDuration(makeTimedBlock(bs.initialView+1, unittest.IdentifierFixture(), time.Now()))
   242  	require.NoError(bs.T(), err)
   243  	assert.NotEqual(bs.T(), bs.config.FallbackProposalDelay, bs.ctl.getProposalTiming())
   244  
   245  	// send the event
   246  	bs.ctl.EpochEmergencyFallbackTriggered()
   247  	// async: should revert to default GetProposalTiming
   248  	require.Eventually(bs.T(), func() bool {
   249  		now := time.Now().UTC()
   250  		return now.Add(bs.config.FallbackProposalDelay.Load()) == bs.ctl.getProposalTiming().TargetPublicationTime(7, now, unittest.IdentifierFixture())
   251  	}, time.Second, time.Millisecond)
   252  
   253  	// additional EpochEmergencyFallbackTriggered events should be no-ops
   254  	// (send capacity+1 events to guarantee one is processed)
   255  	for i := 0; i <= cap(bs.ctl.epochFallbacks); i++ {
   256  		bs.ctl.EpochEmergencyFallbackTriggered()
   257  	}
   258  	// state should be unchanged
   259  	now := time.Now().UTC()
   260  	assert.Equal(bs.T(), now.Add(bs.config.FallbackProposalDelay.Load()), bs.ctl.getProposalTiming().TargetPublicationTime(12, now, unittest.IdentifierFixture()))
   261  
   262  	// additional OnBlockIncorporated events should be no-ops
   263  	for i := 0; i <= cap(bs.ctl.incorporatedBlocks); i++ {
   264  		header := unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView + 1 + uint64(i)))
   265  		header.ParentID = unittest.IdentifierFixture()
   266  		bs.ctl.OnBlockIncorporated(model.BlockFromFlow(header))
   267  	}
   268  	// wait for the channel to drain, since OnBlockIncorporated doesn't block on sending
   269  	require.Eventually(bs.T(), func() bool {
   270  		return len(bs.ctl.incorporatedBlocks) == 0
   271  	}, time.Second, time.Millisecond)
   272  	// state should be unchanged
   273  	now = time.Now().UTC()
   274  	assert.Equal(bs.T(), now.Add(bs.config.FallbackProposalDelay.Load()), bs.ctl.getProposalTiming().TargetPublicationTime(17, now, unittest.IdentifierFixture()))
   275  }
   276  
   277  // TestOnBlockIncorporated_UpdateProposalDelay tests that a new measurement is taken and
   278  // GetProposalTiming updated upon receiving an OnBlockIncorporated event.
   279  func (bs *BlockTimeControllerSuite) TestOnBlockIncorporated_UpdateProposalDelay() {
   280  	bs.CreateAndStartController()
   281  	defer bs.StopController()
   282  
   283  	initialControllerState := captureControllerStateDigest(bs.ctl) // copy initial controller state
   284  	initialProposalDelay := bs.ctl.getProposalTiming()
   285  	block := model.BlockFromFlow(unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView + 1)))
   286  	bs.ctl.OnBlockIncorporated(block)
   287  	require.Eventually(bs.T(), func() bool {
   288  		return bs.ctl.getProposalTiming().ObservationView() > bs.initialView
   289  	}, time.Second, time.Millisecond)
   290  	nextControllerState := captureControllerStateDigest(bs.ctl)
   291  	nextProposalDelay := bs.ctl.getProposalTiming()
   292  
   293  	bs.SanityCheckSubsequentMeasurements(initialControllerState, nextControllerState, false)
   294  	// new measurement should update GetProposalTiming
   295  	now := time.Now().UTC()
   296  	assert.NotEqual(bs.T(),
   297  		initialProposalDelay.TargetPublicationTime(bs.initialView+2, now, unittest.IdentifierFixture()),
   298  		nextProposalDelay.TargetPublicationTime(bs.initialView+2, now, block.BlockID))
   299  
   300  	// duplicate events should be no-ops
   301  	for i := 0; i <= cap(bs.ctl.incorporatedBlocks); i++ {
   302  		bs.ctl.OnBlockIncorporated(block)
   303  	}
   304  	// wait for the channel to drain, since OnBlockIncorporated doesn't block on sending
   305  	require.Eventually(bs.T(), func() bool {
   306  		return len(bs.ctl.incorporatedBlocks) == 0
   307  	}, time.Second, time.Millisecond)
   308  
   309  	// state should be unchanged
   310  	finalControllerState := captureControllerStateDigest(bs.ctl)
   311  	bs.SanityCheckSubsequentMeasurements(nextControllerState, finalControllerState, true)
   312  	assert.Equal(bs.T(), nextProposalDelay, bs.ctl.getProposalTiming())
   313  }
   314  
   315  // TestEnableDisable tests that the controller responds to enabling and disabling.
   316  func (bs *BlockTimeControllerSuite) TestEnableDisable() {
   317  	// start in a disabled state
   318  	err := bs.config.SetEnabled(false)
   319  	require.NoError(bs.T(), err)
   320  	bs.CreateAndStartController()
   321  	defer bs.StopController()
   322  
   323  	now := time.Now()
   324  
   325  	initialControllerState := captureControllerStateDigest(bs.ctl)
   326  	initialProposalDelay := bs.ctl.getProposalTiming()
   327  	// the initial proposal timing should use fallback timing
   328  	assert.Equal(bs.T(), now.Add(bs.ctl.config.FallbackProposalDelay.Load()), initialProposalDelay.TargetPublicationTime(bs.initialView+1, now, unittest.IdentifierFixture()))
   329  
   330  	block := model.BlockFromFlow(unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView + 1)))
   331  	bs.ctl.OnBlockIncorporated(block)
   332  	require.Eventually(bs.T(), func() bool {
   333  		return bs.ctl.getProposalTiming().ObservationView() > bs.initialView
   334  	}, time.Second, time.Millisecond)
   335  	secondProposalDelay := bs.ctl.getProposalTiming()
   336  
   337  	// new measurement should not change GetProposalTiming
   338  	assert.Equal(bs.T(),
   339  		initialProposalDelay.TargetPublicationTime(bs.initialView+2, now, unittest.IdentifierFixture()),
   340  		secondProposalDelay.TargetPublicationTime(bs.initialView+2, now, block.BlockID))
   341  
   342  	// now, enable the controller
   343  	err = bs.ctl.config.SetEnabled(true)
   344  	require.NoError(bs.T(), err)
   345  
   346  	// send another block
   347  	block = model.BlockFromFlow(unittest.BlockHeaderFixture(unittest.HeaderWithView(bs.initialView + 2)))
   348  	bs.ctl.OnBlockIncorporated(block)
   349  	require.Eventually(bs.T(), func() bool {
   350  		return bs.ctl.getProposalTiming().ObservationView() > bs.initialView
   351  	}, time.Second, time.Millisecond)
   352  
   353  	thirdControllerState := captureControllerStateDigest(bs.ctl)
   354  	thirdProposalDelay := bs.ctl.getProposalTiming()
   355  
   356  	// new measurement should change GetProposalTiming
   357  	bs.SanityCheckSubsequentMeasurements(initialControllerState, thirdControllerState, false)
   358  	assert.NotEqual(bs.T(),
   359  		initialProposalDelay.TargetPublicationTime(bs.initialView+3, now, unittest.IdentifierFixture()),
   360  		thirdProposalDelay.TargetPublicationTime(bs.initialView+3, now, block.BlockID))
   361  
   362  }
   363  
   364  // TestOnBlockIncorporated_EpochTransition_Enabled tests epoch transition with controller enabled.
   365  func (bs *BlockTimeControllerSuite) TestOnBlockIncorporated_EpochTransition_Enabled() {
   366  	err := bs.ctl.config.SetEnabled(true)
   367  	require.NoError(bs.T(), err)
   368  	bs.testOnBlockIncorporated_EpochTransition()
   369  }
   370  
   371  // TestOnBlockIncorporated_EpochTransition_Disabled tests epoch transition with controller disabled.
   372  func (bs *BlockTimeControllerSuite) TestOnBlockIncorporated_EpochTransition_Disabled() {
   373  	err := bs.ctl.config.SetEnabled(false)
   374  	require.NoError(bs.T(), err)
   375  	bs.testOnBlockIncorporated_EpochTransition()
   376  }
   377  
   378  // testOnBlockIncorporated_EpochTransition tests that a view change into the next epoch
   379  // updates the local state to reflect the new epoch.
   380  func (bs *BlockTimeControllerSuite) testOnBlockIncorporated_EpochTransition() {
   381  	nextEpoch := mockprotocol.NewEpoch(bs.T())
   382  	nextEpoch.On("Counter").Return(bs.epochCounter+1, nil)
   383  	nextEpoch.On("FinalView").Return(bs.curEpochFinalView*2, nil)
   384  	nextEpoch.On("TargetDuration").Return(bs.EpochDurationSeconds(), nil) // 1s/view
   385  	nextEpoch.On("TargetEndTime").Return(bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), nil)
   386  	bs.epochs.Add(nextEpoch)
   387  	bs.CreateAndStartController()
   388  	defer bs.StopController()
   389  
   390  	initialControllerState := captureControllerStateDigest(bs.ctl)
   391  	bs.epochs.Transition()
   392  	timedBlock := makeTimedBlock(bs.curEpochFinalView+1, unittest.IdentifierFixture(), time.Now().UTC())
   393  	err := bs.ctl.processIncorporatedBlock(timedBlock)
   394  	require.True(bs.T(), bs.ctl.getProposalTiming().ObservationView() > bs.initialView)
   395  	require.NoError(bs.T(), err)
   396  	nextControllerState := captureControllerStateDigest(bs.ctl)
   397  
   398  	bs.SanityCheckSubsequentMeasurements(initialControllerState, nextControllerState, false)
   399  	// epoch boundaries should be updated
   400  	assert.Equal(bs.T(), bs.curEpochFinalView+1, bs.ctl.epochInfo.curEpochFirstView)
   401  	assert.Equal(bs.T(), bs.ctl.epochInfo.curEpochFinalView, bs.curEpochFinalView*2)
   402  	assert.Equal(bs.T(), bs.ctl.epochInfo.curEpochTargetEndTime, bs.curEpochTargetEndTime+bs.EpochDurationSeconds())
   403  	assert.Nil(bs.T(), bs.ctl.nextEpochFinalView)
   404  }
   405  
   406  // TestOnEpochSetupPhaseStarted ensures that the epoch info is updated when the next epoch is set up.
   407  func (bs *BlockTimeControllerSuite) TestOnEpochSetupPhaseStarted() {
   408  	nextEpoch := mockprotocol.NewEpoch(bs.T())
   409  	nextEpoch.On("Counter").Return(bs.epochCounter+1, nil)
   410  	nextEpoch.On("FinalView").Return(bs.curEpochFinalView*2, nil)
   411  	nextEpoch.On("TargetDuration").Return(bs.EpochDurationSeconds(), nil)
   412  	nextEpoch.On("TargetEndTime").Return(bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), nil)
   413  	bs.epochs.Add(nextEpoch)
   414  	bs.CreateAndStartController()
   415  	defer bs.StopController()
   416  
   417  	header := unittest.BlockHeaderFixture()
   418  	bs.ctl.EpochSetupPhaseStarted(bs.epochCounter, header)
   419  	require.Eventually(bs.T(), func() bool {
   420  		return bs.ctl.nextEpochFinalView != nil
   421  	}, time.Second, time.Millisecond)
   422  
   423  	assert.Equal(bs.T(), bs.curEpochFinalView*2, *bs.ctl.nextEpochFinalView)
   424  	assert.Equal(bs.T(), bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), *bs.ctl.nextEpochTargetEndTime)
   425  
   426  	// duplicate events should be no-ops
   427  	for i := 0; i <= cap(bs.ctl.epochSetups); i++ {
   428  		bs.ctl.EpochSetupPhaseStarted(bs.epochCounter, header)
   429  	}
   430  	assert.Equal(bs.T(), bs.curEpochFinalView*2, *bs.ctl.nextEpochFinalView)
   431  	assert.Equal(bs.T(), bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), *bs.ctl.nextEpochTargetEndTime)
   432  }
   433  
   434  // TestProposalDelay_AfterTargetTransitionTime tests the behaviour of the controller
   435  // when we have passed the target end time for the current epoch.
   436  // We should approach the min GetProposalTiming (increase view rate as much as possible)
   437  func (bs *BlockTimeControllerSuite) TestProposalDelay_AfterTargetTransitionTime() {
   438  	// we are near the end of the epoch in view terms
   439  	bs.initialView = uint64(float64(bs.curEpochFinalView) * .95)
   440  	bs.CreateAndStartController()
   441  	defer bs.StopController()
   442  
   443  	lastProposalDelay := float64(bs.EpochDurationSeconds()) // start with large dummy value
   444  	for view := bs.initialView + 1; view < bs.ctl.curEpochFinalView; view++ {
   445  		// we have passed the target end time of the epoch
   446  		receivedParentBlockAt := unix2time(bs.ctl.curEpochTargetEndTime + view)
   447  		timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), receivedParentBlockAt)
   448  		err := bs.ctl.measureViewDuration(timedBlock)
   449  		require.NoError(bs.T(), err)
   450  
   451  		// compute proposal delay:
   452  		pubTime := bs.ctl.getProposalTiming().TargetPublicationTime(view+1, time.Now().UTC(), timedBlock.Block.BlockID) // simulate building a child of `timedBlock`
   453  		delay := pubTime.Sub(receivedParentBlockAt)
   454  
   455  		assert.LessOrEqual(bs.T(), delay.Seconds(), lastProposalDelay)
   456  		lastProposalDelay = delay.Seconds()
   457  
   458  		// transition views until the end of the epoch, or for 100 views
   459  		if view-bs.initialView >= 100 {
   460  			break
   461  		}
   462  	}
   463  }
   464  
   465  // TestProposalDelay_BehindSchedule tests the behaviour of the controller when the
   466  // projected epoch switchover is LATER than the target switchover time, i.e.
   467  // we are behind schedule.
   468  // We should respond by lowering the GetProposalTiming (increasing view rate)
   469  func (bs *BlockTimeControllerSuite) TestProposalDelay_BehindSchedule() {
   470  	// we are 50% of the way through the epoch in view terms
   471  	bs.initialView = uint64(float64(bs.curEpochFinalView) * .5)
   472  	bs.CreateAndStartController()
   473  	defer bs.StopController()
   474  
   475  	lastProposalDelay := float64(bs.EpochDurationSeconds()) // start with large dummy value
   476  	idealEnteredViewTime := unix2time(bs.ctl.curEpochTargetEndTime - (bs.EpochDurationSeconds() / 2))
   477  
   478  	// 1s behind of schedule
   479  	receivedParentBlockAt := idealEnteredViewTime.Add(time.Second)
   480  	for view := bs.initialView + 1; view < bs.ctl.curEpochFinalView; view++ {
   481  		// hold the instantaneous error constant for each view
   482  		receivedParentBlockAt = receivedParentBlockAt.Add(sec2dur(bs.ctl.targetViewTime()))
   483  		timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), receivedParentBlockAt)
   484  		err := bs.ctl.measureViewDuration(timedBlock)
   485  		require.NoError(bs.T(), err)
   486  
   487  		// compute proposal delay:
   488  		pubTime := bs.ctl.getProposalTiming().TargetPublicationTime(view+1, time.Now().UTC(), timedBlock.Block.BlockID) // simulate building a child of `timedBlock`
   489  		delay := pubTime.Sub(receivedParentBlockAt)
   490  		// expecting decreasing GetProposalTiming
   491  		assert.LessOrEqual(bs.T(), delay.Seconds(), lastProposalDelay, "got non-decreasing delay on view %d (initial view: %d)", view, bs.initialView)
   492  		lastProposalDelay = delay.Seconds()
   493  
   494  		// transition views until the end of the epoch, or for 100 views
   495  		if view-bs.initialView >= 100 {
   496  			break
   497  		}
   498  	}
   499  }
   500  
   501  // TestProposalDelay_AheadOfSchedule tests the behaviour of the controller when the
   502  // projected epoch switchover is EARLIER than the target switchover time, i.e.
   503  // we are ahead of schedule.
   504  // We should respond by increasing the GetProposalTiming (lowering view rate)
   505  func (bs *BlockTimeControllerSuite) TestProposalDelay_AheadOfSchedule() {
   506  	// we are 50% of the way through the epoch in view terms
   507  	bs.initialView = uint64(float64(bs.curEpochFinalView) * .5)
   508  	bs.CreateAndStartController()
   509  	defer bs.StopController()
   510  
   511  	lastProposalDelay := time.Duration(0) // start with large dummy value
   512  	idealEnteredViewTime := bs.ctl.curEpochTargetEndTime - (bs.EpochDurationSeconds() / 2)
   513  	// 1s ahead of schedule
   514  	receivedParentBlockAt := idealEnteredViewTime - 1
   515  	for view := bs.initialView + 1; view < bs.ctl.curEpochFinalView; view++ {
   516  		// hold the instantaneous error constant for each view
   517  		receivedParentBlockAt = receivedParentBlockAt + uint64(bs.ctl.targetViewTime())
   518  		timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), unix2time(receivedParentBlockAt))
   519  		err := bs.ctl.measureViewDuration(timedBlock)
   520  		require.NoError(bs.T(), err)
   521  
   522  		// compute proposal delay:
   523  		pubTime := bs.ctl.getProposalTiming().TargetPublicationTime(view+1, time.Now().UTC(), timedBlock.Block.BlockID) // simulate building a child of `timedBlock`
   524  		delay := pubTime.Sub(unix2time(receivedParentBlockAt))
   525  
   526  		// expecting increasing GetProposalTiming
   527  		assert.GreaterOrEqual(bs.T(), delay, lastProposalDelay)
   528  		lastProposalDelay = delay
   529  
   530  		// transition views until the end of the epoch, or for 100 views
   531  		if view-bs.initialView >= 100 {
   532  			break
   533  		}
   534  	}
   535  }
   536  
   537  // TestMetricsWhenObservingBlock tests that when observing a new block the correct metrics are tracked.
   538  func (bs *BlockTimeControllerSuite) TestMetricsWhenObservingBlock() {
   539  	bs.metrics = *mockmodule.NewCruiseCtlMetrics(bs.T())
   540  	// should set metrics upon initialization
   541  	bs.metrics.On("PIDError", float64(0), float64(0), float64(0)).Once()
   542  	bs.metrics.On("TargetProposalDuration", time.Duration(0)).Once()
   543  	bs.metrics.On("ControllerOutput", time.Duration(0)).Once()
   544  	bs.CreateAndStartController()
   545  	defer bs.StopController()
   546  	bs.metrics.AssertExpectations(bs.T())
   547  
   548  	// we are at view 1 of the epoch, but the time is suddenly the target end time
   549  	enteredViewAt := bs.ctl.curEpochTargetEndTime
   550  	view := bs.initialView + 1
   551  	// we should observe a large error
   552  	bs.metrics.On("PIDError", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   553  		p := args[0].(float64)
   554  		i := args[1].(float64)
   555  		d := args[2].(float64)
   556  		assert.Greater(bs.T(), p, float64(0))
   557  		assert.Greater(bs.T(), i, float64(0))
   558  		assert.Greater(bs.T(), d, float64(0))
   559  	}).Once()
   560  	// should immediately use min proposal duration
   561  	bs.metrics.On("TargetProposalDuration", bs.config.MinViewDuration.Load()).Once()
   562  	// should have a large negative controller output
   563  	bs.metrics.On("ControllerOutput", mock.Anything).Run(func(args mock.Arguments) {
   564  		output := args[0].(time.Duration)
   565  		assert.Greater(bs.T(), output, time.Duration(0))
   566  	}).Once()
   567  
   568  	timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), unix2time(enteredViewAt))
   569  	err := bs.ctl.measureViewDuration(timedBlock)
   570  	require.NoError(bs.T(), err)
   571  }
   572  
   573  // TestMetrics_TargetPublicationTime tests that when recalling the `TargetPublicationTime`,
   574  // the controller metrics capture the corresponding publication _delay_.
   575  func (bs *BlockTimeControllerSuite) TestMetrics_TargetPublicationTime() {
   576  	// epoch's first view is 0, it has 3600 views in total, with each view targeted to be 1s long
   577  	bs.initialView = 7                                    // controller is initialized for view 7 of Epoch
   578  	epochTargetStartTime := uint64(time.Now().Unix()) - 7 // this is in the range (-8s,-7s] before 'now', because we truncate fractions of a second
   579  	bs.curEpochTargetEndTime = epochTargetStartTime + bs.EpochDurationSeconds()
   580  	setupMocks(bs)
   581  
   582  	// In the tests, the controller starts at the _beginning_ of view 7, i.e. 7 views (0, 1, ...,6) have concluded
   583  	bs.CreateAndStartController()
   584  	defer bs.StopController()
   585  	// Sanity check that controller's notion of target view time is 1 second. Otherwise, our test is inconsistent.
   586  	assert.Equal(bs.T(), bs.ctl.epochInfo.targetViewTime(), 1.0)
   587  
   588  	// Without any context, the controller should default to "publish immediately". Hence, when
   589  	// we recall `TargetPublicationTime`, the delay relative to 'now' should be very close to zero.
   590  	bs.metrics.On("ProposalPublicationDelay", inProximityOf(0, 50*time.Millisecond)).Once()
   591  	bs.ctl.TargetPublicationTime(0, time.Now(), flow.ZeroID)
   592  
   593  	// We assume block 7 arrives with perfect timing. When observing the proposal for view 10, the controller should have
   594  	// computed its first proper ProposalTiming.
   595  	timedBlock := makeTimedBlock(bs.initialView, unittest.IdentifierFixture(), unix2time(epochTargetStartTime).Add(7*time.Second))
   596  	assert.NoError(bs.T(), bs.ctl.measureViewDuration(timedBlock))
   597  	// As all the timing is perfect so far from the controller's point of view (mathematically, all errors are zero)
   598  	// the target publication time for the subsequent view 8 (=bs.initialView + 1) should be exactly epochTargetStartTime + 8s,
   599  	// because in total 8 views (0,..,7) have already concluded before.
   600  	expectedPublicationTime2 := unix2time(epochTargetStartTime).Add(8 * time.Second)
   601  
   602  	// Assuming this node is primary in view 8 (=bs.initialView + 1). It has just finished constructing its proposal
   603  	// and is computing when to publish it.
   604  	now := time.Now()
   605  	expectedDelay := expectedPublicationTime2.Sub(now) // approximate value that the metrics-component should capture as delay
   606  	bs.metrics.On("ProposalPublicationDelay", inProximityOf(expectedDelay, 100*time.Millisecond)).Once()
   607  	bs.ctl.TargetPublicationTime(bs.initialView+1, now, timedBlock.Block.BlockID)
   608  }
   609  
   610  // Test_vs_PythonSimulation performs a regression test. We implemented the controller in python
   611  // together with a statistical model for the view duration. We used the python implementation to tune
   612  // the PID controller parameters which we are using here.
   613  // In this test, we feed values pre-generated with the python simulation into the Go implementation
   614  // and compare the outputs to the pre-generated outputs from the python controller implementation.
   615  func (bs *BlockTimeControllerSuite) Test_vs_PythonSimulation() {
   616  	// PART 1: setup system to mirror python simulation
   617  	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   618  	refT := time.Now().UTC()
   619  	refT = time.Date(refT.Year(), refT.Month(), refT.Day(), refT.Hour(), refT.Minute(), 0, 0, time.UTC) // truncate to past minute
   620  
   621  	totalEpochViews := 483000
   622  	bs.initialView = 0
   623  	bs.curEpochFirstView, bs.curEpochFinalView = uint64(0), uint64(totalEpochViews-1) // views [0, .., totalEpochViews-1]
   624  	bs.curEpochTargetDuration = 7 * 24 * 60 * 60                                      // 1 week in seconds
   625  	bs.curEpochTargetEndTime = time2unix(refT) + bs.curEpochTargetDuration            // now + 1 week
   626  	bs.epochFallbackTriggered = false
   627  
   628  	bs.config = &Config{
   629  		TimingConfig: TimingConfig{
   630  			FallbackProposalDelay: atomic.NewDuration(500 * time.Millisecond), // irrelevant for this test, as controller should never enter fallback mode
   631  			MinViewDuration:       atomic.NewDuration(470 * time.Millisecond),
   632  			MaxViewDuration:       atomic.NewDuration(2010 * time.Millisecond),
   633  			Enabled:               atomic.NewBool(true),
   634  		},
   635  		ControllerParams: ControllerParams{KP: 2.0, KI: 0.06, KD: 3.0, N_ewma: 5, N_itg: 50},
   636  	}
   637  
   638  	setupMocks(bs)
   639  	bs.CreateAndStartController()
   640  	defer bs.StopController()
   641  
   642  	// PART 2: timing generated from python simulation and corresponding controller response
   643  	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   644  	ref := struct {
   645  		// targetViewTime is the idealized view duration of a perfect system.
   646  		// In Python simulation, this is the array `EpochSimulation.ideal_view_time`
   647  		targetViewTime float64 // units: seconds
   648  
   649  		// observedMinViewTimes[i] is the minimal time required time to execute the protocol for view i
   650  		//  - Duration from the primary observing the parent block (indexed by i) to having its child proposal (block for view i+1) ready for publication.
   651  		//  - This is the minimal time required to execute the protocol. Nodes can only delay their proposal but not progress any faster.
   652  		//  - in Python simulation, this is the array `EpochSimulation.min_view_times + EpochSimulation.observation_noise`
   653  		//    with is returned by function `EpochSimulation.current_view_observation()`
   654  		// Note that this is generally different than the time it takes the committee as a whole to transition
   655  		// through views. This is because the primary changes from view to view, and nodes observe blocks at slightly
   656  		// different times (small noise term). The real world (as well as the simulation) depend on collective swarm
   657  		// behaviour of the consensus committee, which is not observable by nodes individually.
   658  		// In contrast, our `observedMinViewTimes` here contains an additional noise term, to emulate
   659  		// the observations of a node in the real world.
   660  		observedMinViewTimes []float64 // units: seconds
   661  
   662  		// controllerTargetedViewDuration[i] is the duration targeted by the Python controller :
   663  		// - measured from observing the parent until publishing the child block for view i+1
   664  		controllerTargetedViewDuration []float64 // units: seconds
   665  
   666  		// realWorldViewDuration[i] is the duration of the ith view for the entire committee.
   667  		// This value occurs in response to the controller output and is not observable by nodes individually.
   668  		//  - in Python simulation, this is the array `EpochSimulation.real_world_view_duration`
   669  		//    with is recorded by the environment upon the call of `EpochSimulation.delay()`
   670  		realWorldViewDuration []float64 // units: seconds
   671  	}{
   672  		targetViewTime:                 1.2521739130434784,
   673  		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},
   674  		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},
   675  		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},
   676  	}
   677  
   678  	// PART 3: run controller and ensure output matches pre-generated controller response from python ref implementation
   679  	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   680  	// sanity checks:
   681  	require.Equal(bs.T(), uint64(604800), bs.ctl.curEpochTargetEndTime-time2unix(refT), "Epoch should end 1 week from now, i.e. 604800s")
   682  	require.InEpsilon(bs.T(), ref.targetViewTime, bs.ctl.targetViewTime(), 1e-15) // ideal view time
   683  	require.Equal(bs.T(), len(ref.observedMinViewTimes), len(ref.realWorldViewDuration))
   684  
   685  	// Notes:
   686  	// - We specifically make the first observation at when the full time of the epoch is left.
   687  	//   The python simulation we compare with proceed exactly the same way.
   688  	// - we first make an observation, before requesting the controller output. Thereby, we
   689  	//   avoid artifacts of recalling a controller that was just initialized with fallback values.
   690  	// - we call `measureViewDuration(..)` (_not_ `processIncorporatedBlock(..)`) to
   691  	//   interfering with the deduplication logic. Here we want to test correct numerics.
   692  	//   Correctness of the deduplication logic is verified in the different test.
   693  	observationTime := refT
   694  
   695  	for v := 0; v < len(ref.observedMinViewTimes); v++ {
   696  		observedBlock := makeTimedBlock(uint64(v), unittest.IdentifierFixture(), observationTime)
   697  		err := bs.ctl.measureViewDuration(observedBlock)
   698  		require.NoError(bs.T(), err)
   699  		proposalTiming := bs.ctl.getProposalTiming()
   700  		tpt := proposalTiming.TargetPublicationTime(uint64(v+1), time.Now(), observedBlock.Block.BlockID) // value for `timeViewEntered` should be irrelevant here
   701  
   702  		controllerTargetedViewDuration := tpt.Sub(observedBlock.TimeObserved).Seconds()
   703  		bs.T().Logf("%d: ctl=%f\tref=%f\tdiff=%f", v, controllerTargetedViewDuration, ref.controllerTargetedViewDuration[v], controllerTargetedViewDuration-ref.controllerTargetedViewDuration[v])
   704  		require.InEpsilon(bs.T(), ref.controllerTargetedViewDuration[v], controllerTargetedViewDuration, 1e-5, "implementations deviate for view %d", v) // ideal view time
   705  
   706  		observationTime = observationTime.Add(sec2dur(ref.realWorldViewDuration[v]))
   707  	}
   708  }
   709  
   710  func makeTimedBlock(view uint64, parentID flow.Identifier, time time.Time) TimedBlock {
   711  	header := unittest.BlockHeaderFixture(unittest.HeaderWithView(view))
   712  	header.ParentID = parentID
   713  	return TimedBlock{
   714  		Block:        model.BlockFromFlow(header),
   715  		TimeObserved: time,
   716  	}
   717  }
   718  
   719  type controllerStateDigest struct {
   720  	proportionalErr Ewma
   721  	integralErr     LeakyIntegrator
   722  
   723  	// latestProposalTiming holds the ProposalTiming that the controller generated in response to processing the latest observation
   724  	latestProposalTiming ProposalTiming
   725  }
   726  
   727  func captureControllerStateDigest(ctl *BlockTimeController) *controllerStateDigest {
   728  	return &controllerStateDigest{
   729  		proportionalErr:      ctl.proportionalErr,
   730  		integralErr:          ctl.integralErr,
   731  		latestProposalTiming: ctl.getProposalTiming(),
   732  	}
   733  }
   734  
   735  // inProximityOf returns a testify `argumentMatcher` that only accepts durations d,
   736  // such that |d - t| ≤ ε, for specified constants targetValue t and acceptedDeviation ε.
   737  func inProximityOf(targetValue, acceptedDeviation time.Duration) interface{} {
   738  	return mock.MatchedBy(func(duration time.Duration) bool {
   739  		e := targetValue.Seconds() - duration.Seconds()
   740  		return math.Abs(e) <= acceptedDeviation.Abs().Seconds()
   741  	})
   742  }