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 }