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

     1  // Package cruisectl implements a "cruise control" system for Flow by adjusting
     2  // nodes' latest ProposalTiming in response to changes in the measured view rate and
     3  // target epoch switchover time.
     4  //
     5  // It uses a PID controller with the projected epoch switchover time as the process
     6  // variable and the set-point computed using epoch length config. The error is
     7  // the difference between the projected epoch switchover time, assuming an
     8  // ideal view time τ, and the target epoch switchover time (based on a schedule).
     9  package cruisectl
    10  
    11  import (
    12  	"fmt"
    13  	"time"
    14  
    15  	"github.com/rs/zerolog"
    16  	"go.uber.org/atomic"
    17  
    18  	"github.com/onflow/flow-go/consensus/hotstuff"
    19  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    20  	"github.com/onflow/flow-go/model/flow"
    21  	"github.com/onflow/flow-go/module"
    22  	"github.com/onflow/flow-go/module/component"
    23  	"github.com/onflow/flow-go/module/irrecoverable"
    24  	"github.com/onflow/flow-go/state/protocol"
    25  	"github.com/onflow/flow-go/state/protocol/events"
    26  )
    27  
    28  // TimedBlock represents a block, with a timestamp recording when the BlockTimeController received the block
    29  type TimedBlock struct {
    30  	Block        *model.Block
    31  	TimeObserved time.Time // timestamp when BlockTimeController received the block, per convention in UTC
    32  }
    33  
    34  // epochInfo stores data about the current and next epoch. It is updated when we enter
    35  // the first view of a new epoch, or the EpochSetup phase of the current epoch.
    36  type epochInfo struct {
    37  	curEpochFirstView       uint64
    38  	curEpochFinalView       uint64  // F[v] - the final view of the current epoch
    39  	curEpochTargetDuration  uint64  // desired total duration of current epoch in seconds
    40  	curEpochTargetEndTime   uint64  // T[v] - the target end time of the current epoch, represented as Unix Time [seconds]
    41  	nextEpochFinalView      *uint64 // the final view of the next epoch
    42  	nextEpochTargetDuration *uint64 // desired total duration of next epoch in seconds, or nil if epoch has not yet been set up
    43  	nextEpochTargetEndTime  *uint64 // the target end time of the next epoch, represented as Unix Time [seconds]
    44  }
    45  
    46  // targetViewTime returns τ[v], the ideal, steady-state view time for the current epoch.
    47  // For numerical stability, we avoid repetitive conversions between seconds and time.Duration.
    48  // Instead, internally within the controller, we work with float64 in units of seconds.
    49  func (epoch *epochInfo) targetViewTime() float64 {
    50  	return float64(epoch.curEpochTargetDuration) / float64(epoch.curEpochFinalView-epoch.curEpochFirstView+1)
    51  }
    52  
    53  // BlockTimeController dynamically adjusts the ProposalTiming of this node,
    54  // based on the measured view rate of the consensus committee as a whole, in
    55  // order to achieve a desired switchover time for each epoch.
    56  // In a nutshell, the controller outputs the block time on the happy path, i.e.
    57  //   - Suppose the node is observing the parent block B0 at some time `x0`.
    58  //   - The controller determines the duration `d` of how much later the child block B1
    59  //     should be observed by the committee.
    60  //   - The controller internally memorizes the latest B0 it has seen and outputs
    61  //     the tuple `(B0, x0, d)`
    62  //
    63  // This low-level controller output `(B0, x0, d)` is wrapped into a `ProposalTiming`
    64  // interface, specifically `happyPathBlockTime` on the happy path. The purpose of the
    65  // `ProposalTiming` wrapper is to translate the raw controller output into a form
    66  // that is useful for the EventHandler. Edge cases, such as initialization or
    67  // epoch fallback are implemented by other implementations of `ProposalTiming`.
    68  type BlockTimeController struct {
    69  	component.Component
    70  	protocol.Consumer // consumes protocol state events
    71  
    72  	config *Config
    73  
    74  	state   protocol.State
    75  	log     zerolog.Logger
    76  	metrics module.CruiseCtlMetrics
    77  
    78  	epochInfo // scheduled transition view for current/next epoch
    79  	// Currently, the only possible state transition for `epochFallbackTriggered` is false → true.
    80  	// TODO for 'leaving Epoch Fallback via special service event' this might need to change.
    81  	epochFallbackTriggered bool
    82  
    83  	incorporatedBlocks chan TimedBlock   // OnBlockIncorporated events, we desire these blocks to be processed in a timely manner and therefore use a small channel capacity
    84  	epochSetups        chan *flow.Header // EpochSetupPhaseStarted events (block header within setup phase)
    85  	epochFallbacks     chan struct{}     // EpochFallbackTriggered events
    86  
    87  	proportionalErr Ewma
    88  	integralErr     LeakyIntegrator
    89  
    90  	// latestProposalTiming holds the ProposalTiming that the controller generated in response to processing the latest observation
    91  	latestProposalTiming *atomic.Pointer[ProposalTiming]
    92  }
    93  
    94  var _ hotstuff.ProposalDurationProvider = (*BlockTimeController)(nil)
    95  var _ protocol.Consumer = (*BlockTimeController)(nil)
    96  var _ component.Component = (*BlockTimeController)(nil)
    97  
    98  // NewBlockTimeController returns a new BlockTimeController.
    99  func NewBlockTimeController(log zerolog.Logger, metrics module.CruiseCtlMetrics, config *Config, state protocol.State, curView uint64) (*BlockTimeController, error) {
   100  	// Initial error must be 0 unless we are making assumptions of the prior history of the proportional error `e[v]`
   101  	initProptlErr, initItgErr, initDrivErr := .0, .0, .0
   102  	proportionalErr, err := NewEwma(config.alpha(), initProptlErr)
   103  	if err != nil {
   104  		return nil, fmt.Errorf("failed to initialize EWMA for computing the proportional error: %w", err)
   105  	}
   106  	integralErr, err := NewLeakyIntegrator(config.beta(), initItgErr)
   107  	if err != nil {
   108  		return nil, fmt.Errorf("failed to initialize LeakyIntegrator for computing the integral error: %w", err)
   109  	}
   110  
   111  	ctl := &BlockTimeController{
   112  		Consumer:             events.NewNoop(),
   113  		config:               config,
   114  		log:                  log.With().Str("hotstuff", "cruise_ctl").Logger(),
   115  		metrics:              metrics,
   116  		state:                state,
   117  		incorporatedBlocks:   make(chan TimedBlock, 3),
   118  		epochSetups:          make(chan *flow.Header, 5),
   119  		epochFallbacks:       make(chan struct{}, 5),
   120  		proportionalErr:      proportionalErr,
   121  		integralErr:          integralErr,
   122  		latestProposalTiming: atomic.NewPointer[ProposalTiming](nil), // set in initProposalTiming
   123  	}
   124  	ctl.Component = component.NewComponentManagerBuilder().
   125  		AddWorker(ctl.processEventsWorkerLogic).
   126  		Build()
   127  
   128  	// initialize state
   129  	err = ctl.initEpochInfo()
   130  	if err != nil {
   131  		return nil, fmt.Errorf("could not initialize epoch info: %w", err)
   132  	}
   133  	ctl.initProposalTiming(curView)
   134  
   135  	ctl.log.Debug().
   136  		Uint64("view", curView).
   137  		Msg("initialized BlockTimeController")
   138  	ctl.metrics.PIDError(initProptlErr, initItgErr, initDrivErr)
   139  	ctl.metrics.ControllerOutput(0)
   140  	ctl.metrics.TargetProposalDuration(0)
   141  
   142  	return ctl, nil
   143  }
   144  
   145  // initEpochInfo initializes the epochInfo state upon component startup.
   146  // No errors are expected during normal operation.
   147  func (ctl *BlockTimeController) initEpochInfo() error {
   148  	finalSnapshot := ctl.state.Final()
   149  	curEpoch := finalSnapshot.Epochs().Current()
   150  
   151  	curEpochFirstView, err := curEpoch.FirstView()
   152  	if err != nil {
   153  		return fmt.Errorf("could not initialize current epoch first view: %w", err)
   154  	}
   155  	ctl.curEpochFirstView = curEpochFirstView
   156  
   157  	curEpochFinalView, err := curEpoch.FinalView()
   158  	if err != nil {
   159  		return fmt.Errorf("could not initialize current epoch final view: %w", err)
   160  	}
   161  	ctl.curEpochFinalView = curEpochFinalView
   162  
   163  	curEpochTargetDuration, err := curEpoch.TargetDuration()
   164  	if err != nil {
   165  		return fmt.Errorf("could not initialize current epoch target duration: %w", err)
   166  	}
   167  	ctl.curEpochTargetDuration = curEpochTargetDuration
   168  
   169  	curEpochTargetEndTime, err := curEpoch.TargetEndTime()
   170  	if err != nil {
   171  		return fmt.Errorf("could not initialize current epoch target end time: %w", err)
   172  	}
   173  	ctl.curEpochTargetEndTime = curEpochTargetEndTime
   174  
   175  	phase, err := finalSnapshot.Phase()
   176  	if err != nil {
   177  		return fmt.Errorf("could not check snapshot phase: %w", err)
   178  	}
   179  	if phase > flow.EpochPhaseStaking {
   180  		nextEpochFinalView, err := finalSnapshot.Epochs().Next().FinalView()
   181  		if err != nil {
   182  			return fmt.Errorf("could not initialize next epoch final view: %w", err)
   183  		}
   184  		ctl.epochInfo.nextEpochFinalView = &nextEpochFinalView
   185  
   186  		nextEpochTargetDuration, err := finalSnapshot.Epochs().Next().TargetDuration()
   187  		if err != nil {
   188  			return fmt.Errorf("could not initialize next epoch target duration: %w", err)
   189  		}
   190  		ctl.nextEpochTargetDuration = &nextEpochTargetDuration
   191  
   192  		nextEpochTargetEndTime, err := finalSnapshot.Epochs().Next().TargetEndTime()
   193  		if err != nil {
   194  			return fmt.Errorf("could not initialize next epoch target end time: %w", err)
   195  		}
   196  		ctl.nextEpochTargetEndTime = &nextEpochTargetEndTime
   197  	}
   198  
   199  	epochFallbackTriggered, err := ctl.state.Params().EpochFallbackTriggered()
   200  	if err != nil {
   201  		return fmt.Errorf("could not check epoch fallback: %w", err)
   202  	}
   203  	ctl.epochFallbackTriggered = epochFallbackTriggered
   204  
   205  	return nil
   206  }
   207  
   208  // initProposalTiming initializes the ProposalTiming value upon startup.
   209  // CAUTION: Must be called after initEpochInfo.
   210  func (ctl *BlockTimeController) initProposalTiming(curView uint64) {
   211  	// When disabled, or in epoch fallback, use fallback timing (constant ProposalDuration)
   212  	if ctl.epochFallbackTriggered || !ctl.config.Enabled.Load() {
   213  		ctl.storeProposalTiming(newFallbackTiming(curView, time.Now().UTC(), ctl.config.FallbackProposalDelay.Load()))
   214  		return
   215  	}
   216  	// Otherwise, before we observe any view changes, publish blocks immediately
   217  	ctl.storeProposalTiming(newPublishImmediately(curView, time.Now().UTC()))
   218  }
   219  
   220  // storeProposalTiming stores the latest ProposalTiming. Concurrency safe.
   221  func (ctl *BlockTimeController) storeProposalTiming(proposalTiming ProposalTiming) {
   222  	ctl.latestProposalTiming.Store(&proposalTiming)
   223  }
   224  
   225  // getProposalTiming returns the controller's latest ProposalTiming. Concurrency safe.
   226  func (ctl *BlockTimeController) getProposalTiming() ProposalTiming {
   227  	pt := ctl.latestProposalTiming.Load()
   228  	if pt == nil { // should never happen, as we always store non-nil instances of ProposalTiming. Though, this extra check makes `GetProposalTiming` universal.
   229  		return nil
   230  	}
   231  	return *pt
   232  }
   233  
   234  // TargetPublicationTime is intended to be called by the EventHandler, whenever it
   235  // wants to publish a new proposal. The event handler inputs
   236  //   - proposalView: the view it is proposing for,
   237  //   - timeViewEntered: the time when the EventHandler entered this view
   238  //   - parentBlockId: the ID of the parent block, which the EventHandler is building on
   239  //
   240  // TargetPublicationTime returns the time stamp when the new proposal should be broadcasted.
   241  // For a given view where we are the primary, suppose the actual time we are done building our proposal is P:
   242  //   - if P < TargetPublicationTime(..), then the EventHandler should wait until
   243  //     `TargetPublicationTime` to broadcast the proposal
   244  //   - if P >= TargetPublicationTime(..), then the EventHandler should immediately broadcast the proposal
   245  //
   246  // Note: Technically, our metrics capture the publication delay relative to this function's _latest_ call.
   247  // Currently, the EventHandler is the only caller of this function, and only calls it once per proposal.
   248  //
   249  // Concurrency safe.
   250  func (ctl *BlockTimeController) TargetPublicationTime(proposalView uint64, timeViewEntered time.Time, parentBlockId flow.Identifier) time.Time {
   251  	targetPublicationTime := ctl.getProposalTiming().TargetPublicationTime(proposalView, timeViewEntered, parentBlockId)
   252  
   253  	publicationDelay := time.Until(targetPublicationTime)
   254  	// targetPublicationTime should already account for the controller's upper limit of authority (longest view time
   255  	// the controller is allowed to select). However, targetPublicationTime is allowed to be in the past, if the
   256  	// controller want to signal that the proposal should be published asap. We could hypothetically update a past
   257  	// targetPublicationTime to 'now' at every level in the code. However, this time stamp would move into the past
   258  	// immediately, and we would have to update the targetPublicationTime over and over. Instead, we just allow values
   259  	// in the past, thereby making repeated corrections unnecessary. In this model, the code _interpreting_ the value
   260  	// needs to apply the convention a negative publicationDelay essentially means "no delay".
   261  	if publicationDelay < 0 {
   262  		publicationDelay = 0 // Controller can only delay publication of proposal. Hence, the delay is lower-bounded by zero.
   263  	}
   264  	ctl.metrics.ProposalPublicationDelay(publicationDelay)
   265  
   266  	return targetPublicationTime
   267  }
   268  
   269  // processEventsWorkerLogic is the logic for processing events received from other components.
   270  // This method should be executed by a dedicated worker routine (not concurrency safe).
   271  func (ctl *BlockTimeController) processEventsWorkerLogic(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) {
   272  	ready()
   273  
   274  	done := ctx.Done()
   275  	for {
   276  
   277  		// Priority 1: EpochSetup
   278  		select {
   279  		case block := <-ctl.epochSetups:
   280  			snapshot := ctl.state.AtHeight(block.Height)
   281  			err := ctl.processEpochSetupPhaseStarted(snapshot)
   282  			if err != nil {
   283  				ctl.log.Err(err).Msgf("fatal error handling EpochSetupPhaseStarted event")
   284  				ctx.Throw(err)
   285  				return
   286  			}
   287  		default:
   288  		}
   289  
   290  		// Priority 2: EpochFallbackTriggered
   291  		select {
   292  		case <-ctl.epochFallbacks:
   293  			err := ctl.processEpochFallbackTriggered()
   294  			if err != nil {
   295  				ctl.log.Err(err).Msgf("fatal error processing epoch fallback event")
   296  				ctx.Throw(err)
   297  			}
   298  		default:
   299  		}
   300  
   301  		// Priority 3: OnBlockIncorporated
   302  		select {
   303  		case <-done:
   304  			return
   305  		case block := <-ctl.incorporatedBlocks:
   306  			err := ctl.processIncorporatedBlock(block)
   307  			if err != nil {
   308  				ctl.log.Err(err).Msgf("fatal error handling OnBlockIncorporated event")
   309  				ctx.Throw(err)
   310  				return
   311  			}
   312  		case block := <-ctl.epochSetups:
   313  			snapshot := ctl.state.AtHeight(block.Height)
   314  			err := ctl.processEpochSetupPhaseStarted(snapshot)
   315  			if err != nil {
   316  				ctl.log.Err(err).Msgf("fatal error handling EpochSetupPhaseStarted event")
   317  				ctx.Throw(err)
   318  				return
   319  			}
   320  		case <-ctl.epochFallbacks:
   321  			err := ctl.processEpochFallbackTriggered()
   322  			if err != nil {
   323  				ctl.log.Err(err).Msgf("fatal error processing epoch fallback event")
   324  				ctx.Throw(err)
   325  				return
   326  			}
   327  		}
   328  	}
   329  }
   330  
   331  // processIncorporatedBlock processes `OnBlockIncorporated` events from HotStuff.
   332  // Whenever the view changes, we:
   333  //   - updates epoch info, if this is the first observed view of a new epoch
   334  //   - compute error terms, compensation function output, and new ProposalTiming
   335  //   - compute a new projected epoch end time, assuming an ideal view rate
   336  //
   337  // No errors are expected during normal operation.
   338  func (ctl *BlockTimeController) processIncorporatedBlock(tb TimedBlock) error {
   339  	// if epoch fallback is triggered, we always use fallbackProposalTiming
   340  	if ctl.epochFallbackTriggered {
   341  		return nil
   342  	}
   343  
   344  	latest := ctl.getProposalTiming()
   345  	if tb.Block.View <= latest.ObservationView() { // we don't care about older blocks that are incorporated into the protocol state
   346  		return nil
   347  	}
   348  
   349  	err := ctl.checkForEpochTransition(tb)
   350  	if err != nil {
   351  		return fmt.Errorf("could not check for epoch transition: %w", err)
   352  	}
   353  
   354  	err = ctl.measureViewDuration(tb)
   355  	if err != nil {
   356  		return fmt.Errorf("could not measure view rate: %w", err)
   357  	}
   358  	return nil
   359  }
   360  
   361  // checkForEpochTransition updates the epochInfo to reflect an epoch transition if curView
   362  // being entered causes a transition to the next epoch. Otherwise, this is a no-op.
   363  // No errors are expected during normal operation.
   364  func (ctl *BlockTimeController) checkForEpochTransition(tb TimedBlock) error {
   365  	view := tb.Block.View
   366  	if view <= ctl.curEpochFinalView { // prevalent case: we are still within the current epoch
   367  		return nil
   368  	}
   369  
   370  	// sanity checks, since we are beyond the final view of the most recently processed epoch:
   371  	if ctl.nextEpochFinalView == nil { // final view of epoch we are entering should be known
   372  		return fmt.Errorf("cannot transition without nextEpochFinalView set")
   373  	}
   374  	if ctl.nextEpochTargetEndTime == nil {
   375  		return fmt.Errorf("cannot transition without nextEpochTargetEndTime set")
   376  	}
   377  	if ctl.nextEpochTargetDuration == nil {
   378  		return fmt.Errorf("cannot transition without nextEpochTargetDuration set")
   379  	}
   380  	if view > *ctl.nextEpochFinalView { // the block's view should be within the upcoming epoch
   381  		return fmt.Errorf("sanity check failed: curView %d is beyond both current epoch (final view %d) and next epoch (final view %d)",
   382  			view, ctl.curEpochFinalView, *ctl.nextEpochFinalView)
   383  	}
   384  
   385  	ctl.curEpochFirstView = ctl.curEpochFinalView + 1
   386  	ctl.curEpochFinalView = *ctl.nextEpochFinalView
   387  	ctl.curEpochTargetDuration = *ctl.nextEpochTargetDuration
   388  	ctl.curEpochTargetEndTime = *ctl.nextEpochTargetEndTime
   389  	ctl.nextEpochFinalView = nil
   390  	ctl.nextEpochTargetDuration = nil
   391  	ctl.nextEpochTargetEndTime = nil
   392  	return nil
   393  }
   394  
   395  // measureViewDuration computes a new measurement of projected epoch switchover time and error for the newly entered view.
   396  // It updates the latest ProposalTiming based on the new error.
   397  // No errors are expected during normal operation.
   398  func (ctl *BlockTimeController) measureViewDuration(tb TimedBlock) error {
   399  	view := tb.Block.View
   400  	// if the controller is disabled, we don't update measurements and instead use a fallback timing
   401  	if !ctl.config.Enabled.Load() {
   402  		fallbackDelay := ctl.config.FallbackProposalDelay.Load()
   403  		ctl.storeProposalTiming(newFallbackTiming(view, tb.TimeObserved, fallbackDelay))
   404  		ctl.log.Debug().
   405  			Uint64("cur_view", view).
   406  			Dur("fallback_proposal_delay", fallbackDelay).
   407  			Msg("controller is disabled - using fallback timing")
   408  		return nil
   409  	}
   410  
   411  	previousProposalTiming := ctl.getProposalTiming()
   412  	previousPropErr := ctl.proportionalErr.Value()
   413  
   414  	// Compute the projected time still needed for the remaining views, assuming that we progress through the remaining views with
   415  	// the idealized target view time.
   416  	// Note the '+1' term in the computation of `viewDurationsRemaining`. This is related to our convention that the epoch begins
   417  	// (happy path) when observing the first block of the epoch. Only by observing this block, the nodes transition to the first
   418  	// view of the epoch. Up to that point, the consensus replicas remain in the last view of the previous epoch, in the state of
   419  	// "having processed the last block of the old epoch and voted for it" (happy path). Replicas remain in this state until they
   420  	// see a confirmation of the view (either QC or TC for the last view of the previous epoch).
   421  	// In accordance with this convention, observing the proposal for the last view of an epoch, marks the start of the last view.
   422  	// By observing the proposal, nodes enter the last view, verify the block, vote for it, the primary aggregates the votes,
   423  	// constructs the child (for first view of new epoch). The last view of the epoch ends, when the child proposal is published.
   424  	tau := ctl.targetViewTime()                                                    // τ: idealized target view time in units of seconds
   425  	viewDurationsRemaining := ctl.curEpochFinalView + 1 - view                     // k[v]: views remaining in current epoch
   426  	durationRemaining := unix2time(ctl.curEpochTargetEndTime).Sub(tb.TimeObserved) // Γ[v] = T[v] - t[v], with t[v] ≡ tb.TimeObserved the time when observing the block that triggered the view change
   427  
   428  	// Compute instantaneous error term: e[v] = k[v]·τ - T[v] i.e. the projected difference from target switchover
   429  	// and update PID controller's error terms. All UNITS in SECOND.
   430  	instErr := float64(viewDurationsRemaining)*tau - durationRemaining.Seconds()
   431  	propErr := ctl.proportionalErr.AddObservation(instErr)
   432  	itgErr := ctl.integralErr.AddObservation(instErr)
   433  	drivErr := propErr - previousPropErr
   434  
   435  	// controller output u[v] in units of second
   436  	u := propErr*ctl.config.KP + itgErr*ctl.config.KI + drivErr*ctl.config.KD
   437  
   438  	// compute the controller output for this observation
   439  	unconstrainedBlockTime := sec2dur(tau - u) // desired time between parent and child block, in units of seconds
   440  	proposalTiming := newHappyPathBlockTime(tb, unconstrainedBlockTime, ctl.config.TimingConfig)
   441  	constrainedBlockTime := proposalTiming.ConstrainedBlockTime()
   442  
   443  	ctl.log.Debug().
   444  		Uint64("last_observation", previousProposalTiming.ObservationView()).
   445  		Dur("duration_since_last_observation", tb.TimeObserved.Sub(previousProposalTiming.ObservationTime())).
   446  		Dur("projected_time_remaining", durationRemaining).
   447  		Uint64("view_durations_remaining", viewDurationsRemaining).
   448  		Float64("inst_err", instErr).
   449  		Float64("proportional_err", propErr).
   450  		Float64("integral_err", itgErr).
   451  		Float64("derivative_err", drivErr).
   452  		Dur("controller_output", sec2dur(u)).
   453  		Dur("unconstrained_block_time", unconstrainedBlockTime).
   454  		Dur("constrained_block_time", constrainedBlockTime).
   455  		Msg("measured error upon view change")
   456  
   457  	ctl.metrics.PIDError(propErr, itgErr, drivErr)
   458  	ctl.metrics.ControllerOutput(sec2dur(u))
   459  	ctl.metrics.TargetProposalDuration(proposalTiming.ConstrainedBlockTime())
   460  
   461  	ctl.storeProposalTiming(proposalTiming)
   462  	return nil
   463  }
   464  
   465  // processEpochSetupPhaseStarted processes EpochSetupPhaseStarted events from the protocol state.
   466  // Whenever we enter the EpochSetup phase, we:
   467  //   - store the next epoch's final view
   468  //
   469  // No errors are expected during normal operation.
   470  func (ctl *BlockTimeController) processEpochSetupPhaseStarted(snapshot protocol.Snapshot) error {
   471  	if ctl.epochFallbackTriggered {
   472  		return nil
   473  	}
   474  
   475  	nextEpoch := snapshot.Epochs().Next()
   476  	finalView, err := nextEpoch.FinalView()
   477  	if err != nil {
   478  		return fmt.Errorf("could not get next epoch final view: %w", err)
   479  	}
   480  	targetDuration, err := nextEpoch.TargetDuration()
   481  	if err != nil {
   482  		return fmt.Errorf("could not get next epoch target duration: %w", err)
   483  	}
   484  	targetEndTime, err := nextEpoch.TargetEndTime()
   485  	if err != nil {
   486  		return fmt.Errorf("could not get next epoch target end time: %w", err)
   487  	}
   488  
   489  	ctl.epochInfo.nextEpochFinalView = &finalView
   490  	ctl.epochInfo.nextEpochTargetDuration = &targetDuration
   491  	ctl.epochInfo.nextEpochTargetEndTime = &targetEndTime
   492  	return nil
   493  }
   494  
   495  // processEpochFallbackTriggered processes EpochFallbackTriggered events from the protocol state.
   496  // When epoch fallback mode is triggered, we:
   497  //   - set ProposalTiming to the default value
   498  //   - set epoch fallback triggered, to disable the controller
   499  //
   500  // No errors are expected during normal operation.
   501  func (ctl *BlockTimeController) processEpochFallbackTriggered() error {
   502  	ctl.epochFallbackTriggered = true
   503  	latestFinalized, err := ctl.state.Final().Head()
   504  	if err != nil {
   505  		return fmt.Errorf("failed to retrieve latest finalized block from protocol state %w", err)
   506  	}
   507  
   508  	ctl.storeProposalTiming(newFallbackTiming(latestFinalized.View, time.Now().UTC(), ctl.config.FallbackProposalDelay.Load()))
   509  	return nil
   510  }
   511  
   512  // OnBlockIncorporated listens to notification from HotStuff about incorporating new blocks.
   513  // The event is queued for async processing by the worker. If the channel is full,
   514  // the event is discarded - since we are taking an average it doesn't matter if we
   515  // occasionally miss a sample.
   516  func (ctl *BlockTimeController) OnBlockIncorporated(block *model.Block) {
   517  	select {
   518  	case ctl.incorporatedBlocks <- TimedBlock{Block: block, TimeObserved: time.Now().UTC()}:
   519  	default:
   520  	}
   521  }
   522  
   523  // EpochSetupPhaseStarted responds to the EpochSetup phase starting for the current epoch.
   524  // The event is queued for async processing by the worker.
   525  func (ctl *BlockTimeController) EpochSetupPhaseStarted(_ uint64, first *flow.Header) {
   526  	ctl.epochSetups <- first
   527  }
   528  
   529  // EpochEmergencyFallbackTriggered responds to epoch fallback mode being triggered.
   530  func (ctl *BlockTimeController) EpochEmergencyFallbackTriggered() {
   531  	ctl.epochFallbacks <- struct{}{}
   532  }
   533  
   534  // time2unix converts a time.Time to UNIX time represented as a uint64.
   535  // Returned timestamp is precise to within one second of input.
   536  func time2unix(t time.Time) uint64 {
   537  	return uint64(t.Unix())
   538  }
   539  
   540  // unix2time converts a UNIX timestamp represented as a uint64 to a time.Time.
   541  func unix2time(unix uint64) time.Time {
   542  	return time.Unix(int64(unix), 0)
   543  }
   544  
   545  // sec2dur converts a floating-point number of seconds to a time.Duration.
   546  func sec2dur(sec float64) time.Duration {
   547  	return time.Duration(int64(sec * float64(time.Second)))
   548  }