diff --git a/cmd/consensus/main.go b/cmd/consensus/main.go index fc685afc247..63ddfaa9cdd 100644 --- a/cmd/consensus/main.go +++ b/cmd/consensus/main.go @@ -92,7 +92,6 @@ func main() { dkgControllerConfig dkgmodule.ControllerConfig dkgMessagingEngineConfig = dkgeng.DefaultMessagingEngineConfig() cruiseCtlConfig = cruisectl.DefaultConfig() - cruiseCtlTargetTransitionTimeFlag = cruiseCtlConfig.TargetTransition.String() cruiseCtlFallbackProposalDurationFlag time.Duration cruiseCtlMinViewDurationFlag time.Duration cruiseCtlMaxViewDurationFlag time.Duration @@ -150,7 +149,6 @@ func main() { flags.DurationVar(&hotstuffMinTimeout, "hotstuff-min-timeout", 2500*time.Millisecond, "the lower timeout bound for the hotstuff pacemaker, this is also used as initial timeout") flags.Float64Var(&hotstuffTimeoutAdjustmentFactor, "hotstuff-timeout-adjustment-factor", timeout.DefaultConfig.TimeoutAdjustmentFactor, "adjustment of timeout duration in case of time out event") flags.Uint64Var(&hotstuffHappyPathMaxRoundFailures, "hotstuff-happy-path-max-round-failures", timeout.DefaultConfig.HappyPathMaxRoundFailures, "number of failed rounds before first timeout increase") - flags.StringVar(&cruiseCtlTargetTransitionTimeFlag, "cruise-ctl-target-epoch-transition-time", cruiseCtlTargetTransitionTimeFlag, "the target epoch switchover schedule") flags.DurationVar(&cruiseCtlFallbackProposalDurationFlag, "cruise-ctl-fallback-proposal-duration", cruiseCtlConfig.FallbackProposalDelay.Load(), "the proposal duration value to use when the controller is disabled, or in epoch fallback mode. In those modes, this value has the same as the old `--block-rate-delay`") flags.DurationVar(&cruiseCtlMinViewDurationFlag, "cruise-ctl-min-view-duration", cruiseCtlConfig.MinViewDuration.Load(), "the lower bound of authority for the controller, when active. This is the smallest amount of time a view is allowed to take.") flags.DurationVar(&cruiseCtlMaxViewDurationFlag, "cruise-ctl-max-view-duration", cruiseCtlConfig.MaxViewDuration.Load(), "the upper bound of authority for the controller when active. This is the largest amount of time a view is allowed to take.") @@ -179,14 +177,6 @@ func main() { startupTime = t nodeBuilder.Logger.Info().Time("startup_time", startupTime).Msg("got startup_time") } - // parse target transition time string, if set - if cruiseCtlTargetTransitionTimeFlag != cruiseCtlConfig.TargetTransition.String() { - transitionTime, err := cruisectl.ParseTransition(cruiseCtlTargetTransitionTimeFlag) - if err != nil { - return fmt.Errorf("invalid epoch transition time string: %w", err) - } - cruiseCtlConfig.TargetTransition = *transitionTime - } // convert local flag variables to atomic config variables, for dynamically updatable fields if cruiseCtlEnabledFlag != cruiseCtlConfig.Enabled.Load() { cruiseCtlConfig.Enabled.Store(cruiseCtlEnabledFlag) diff --git a/consensus/hotstuff/cruisectl/block_time_controller.go b/consensus/hotstuff/cruisectl/block_time_controller.go index 0748e8ec760..2b356ffce6f 100644 --- a/consensus/hotstuff/cruisectl/block_time_controller.go +++ b/consensus/hotstuff/cruisectl/block_time_controller.go @@ -34,24 +34,20 @@ type TimedBlock struct { // epochInfo stores data about the current and next epoch. It is updated when we enter // the first view of a new epoch, or the EpochSetup phase of the current epoch. type epochInfo struct { - curEpochFirstView uint64 - curEpochFinalView uint64 // F[v] - the final view of the epoch - curEpochTargetEndTime time.Time // T[v] - the target end time of the current epoch - nextEpochFinalView *uint64 + curEpochFirstView uint64 + curEpochFinalView uint64 // F[v] - the final view of the current epoch + curEpochTargetDuration uint64 // desired total duration of current epoch in seconds + curEpochTargetEndTime uint64 // T[v] - the target end time of the current epoch, represented as Unix Time [seconds] + nextEpochFinalView *uint64 // the final view of the next epoch + nextEpochTargetDuration *uint64 // desired total duration of next epoch in seconds, or nil if epoch has not yet been set up + nextEpochTargetEndTime *uint64 // the target end time of the next epoch, represented as Unix Time [seconds] } // targetViewTime returns τ[v], the ideal, steady-state view time for the current epoch. // For numerical stability, we avoid repetitive conversions between seconds and time.Duration. // Instead, internally within the controller, we work with float64 in units of seconds. func (epoch *epochInfo) targetViewTime() float64 { - return epochLength.Seconds() / float64(epoch.curEpochFinalView-epoch.curEpochFirstView+1) -} - -// fractionComplete returns the percentage of views completed of the epoch for the given curView. -// curView must be within the range [curEpochFirstView, curEpochFinalView] -// Returns the completion percentage as a float between [0, 1] -func (epoch *epochInfo) fractionComplete(curView uint64) float64 { - return float64(curView-epoch.curEpochFirstView) / float64(epoch.curEpochFinalView-epoch.curEpochFirstView) + return float64(epoch.curEpochTargetDuration) / float64(epoch.curEpochFinalView-epoch.curEpochFirstView+1) } // BlockTimeController dynamically adjusts the ProposalTiming of this node, @@ -67,8 +63,8 @@ func (epoch *epochInfo) fractionComplete(curView uint64) float64 { // This low-level controller output `(B0, x0, d)` is wrapped into a `ProposalTiming` // interface, specifically `happyPathBlockTime` on the happy path. The purpose of the // `ProposalTiming` wrapper is to translate the raw controller output into a form -// that is useful for the event handler. Edge cases, such as initialization or -// EECC are implemented by other implementations of `ProposalTiming`. +// that is useful for the EventHandler. Edge cases, such as initialization or +// epoch fallback are implemented by other implementations of `ProposalTiming`. type BlockTimeController struct { component.Component protocol.Consumer // consumes protocol state events @@ -79,7 +75,9 @@ type BlockTimeController struct { log zerolog.Logger metrics module.CruiseCtlMetrics - epochInfo // scheduled transition view for current/next epoch + epochInfo // scheduled transition view for current/next epoch + // Currently, the only possible state transition for `epochFallbackTriggered` is false → true. + // TODO for 'leaving Epoch Fallback via special service event' this might need to change. epochFallbackTriggered bool incorporatedBlocks chan TimedBlock // OnBlockIncorporated events, we desire these blocks to be processed in a timely manner and therefore use a small channel capacity @@ -162,6 +160,18 @@ func (ctl *BlockTimeController) initEpochInfo(curView uint64) error { } ctl.curEpochFinalView = curEpochFinalView + curEpochTargetDuration, err := curEpoch.TargetDuration() + if err != nil { + return fmt.Errorf("could not initialize current epoch target duration: %w", err) + } + ctl.curEpochTargetDuration = curEpochTargetDuration + + curEpochTargetEndTime, err := curEpoch.TargetEndTime() + if err != nil { + return fmt.Errorf("could not initialize current epoch target end time: %w", err) + } + ctl.curEpochTargetEndTime = curEpochTargetEndTime + phase, err := finalSnapshot.Phase() if err != nil { return fmt.Errorf("could not check snapshot phase: %w", err) @@ -172,9 +182,19 @@ func (ctl *BlockTimeController) initEpochInfo(curView uint64) error { return fmt.Errorf("could not initialize next epoch final view: %w", err) } ctl.epochInfo.nextEpochFinalView = &nextEpochFinalView - } - ctl.curEpochTargetEndTime = ctl.config.TargetTransition.inferTargetEndTime(time.Now().UTC(), ctl.epochInfo.fractionComplete(curView)) + nextEpochTargetDuration, err := finalSnapshot.Epochs().Next().TargetDuration() + if err != nil { + return fmt.Errorf("could not initialize next epoch target duration: %w", err) + } + ctl.nextEpochTargetDuration = &nextEpochTargetDuration + + nextEpochTargetEndTime, err := finalSnapshot.Epochs().Next().TargetEndTime() + if err != nil { + return fmt.Errorf("could not initialize next epoch target end time: %w", err) + } + ctl.nextEpochTargetEndTime = &nextEpochTargetEndTime + } epochFallbackTriggered, err := ctl.state.Params().EpochFallbackTriggered() if err != nil { @@ -197,8 +217,7 @@ func (ctl *BlockTimeController) initProposalTiming(curView uint64) { ctl.storeProposalTiming(newPublishImmediately(curView, time.Now().UTC())) } -// storeProposalTiming stores the latest ProposalTiming -// Concurrency safe. +// storeProposalTiming stores the latest ProposalTiming. Concurrency safe. func (ctl *BlockTimeController) storeProposalTiming(proposalTiming ProposalTiming) { ctl.latestProposalTiming.Store(&proposalTiming) } @@ -242,7 +261,7 @@ func (ctl *BlockTimeController) processEventsWorkerLogic(ctx irrecoverable.Signa case <-ctl.epochFallbacks: err := ctl.processEpochFallbackTriggered() if err != nil { - ctl.log.Err(err).Msgf("fatal error processing epoch EECC event") + ctl.log.Err(err).Msgf("fatal error processing epoch fallback event") ctx.Throw(err) } default: @@ -270,7 +289,7 @@ func (ctl *BlockTimeController) processEventsWorkerLogic(ctx irrecoverable.Signa case <-ctl.epochFallbacks: err := ctl.processEpochFallbackTriggered() if err != nil { - ctl.log.Err(err).Msgf("fatal error processing epoch EECC event") + ctl.log.Err(err).Msgf("fatal error processing epoch fallback event") ctx.Throw(err) return } @@ -321,6 +340,12 @@ func (ctl *BlockTimeController) checkForEpochTransition(tb TimedBlock) error { if ctl.nextEpochFinalView == nil { // final view of epoch we are entering should be known return fmt.Errorf("cannot transition without nextEpochFinalView set") } + if ctl.nextEpochTargetEndTime == nil { + return fmt.Errorf("cannot transition without nextEpochTargetEndTime set") + } + if ctl.nextEpochTargetDuration == nil { + return fmt.Errorf("cannot transition without nextEpochTargetDuration set") + } if view > *ctl.nextEpochFinalView { // the block's view should be within the upcoming epoch return fmt.Errorf("sanity check failed: curView %d is beyond both current epoch (final view %d) and next epoch (final view %d)", view, ctl.curEpochFinalView, *ctl.nextEpochFinalView) @@ -328,8 +353,11 @@ func (ctl *BlockTimeController) checkForEpochTransition(tb TimedBlock) error { ctl.curEpochFirstView = ctl.curEpochFinalView + 1 ctl.curEpochFinalView = *ctl.nextEpochFinalView + ctl.curEpochTargetDuration = *ctl.nextEpochTargetDuration + ctl.curEpochTargetEndTime = *ctl.nextEpochTargetEndTime ctl.nextEpochFinalView = nil - ctl.curEpochTargetEndTime = ctl.config.TargetTransition.inferTargetEndTime(tb.Block.Timestamp, ctl.epochInfo.fractionComplete(view)) + ctl.nextEpochTargetDuration = nil + ctl.nextEpochTargetEndTime = nil return nil } @@ -362,9 +390,9 @@ func (ctl *BlockTimeController) measureViewDuration(tb TimedBlock) error { // In accordance with this convention, observing the proposal for the last view of an epoch, marks the start of the last view. // By observing the proposal, nodes enter the last view, verify the block, vote for it, the primary aggregates the votes, // constructs the child (for first view of new epoch). The last view of the epoch ends, when the child proposal is published. - tau := ctl.targetViewTime() // τ - idealized target view time in units of seconds - viewDurationsRemaining := ctl.curEpochFinalView + 1 - view // k[v] - views remaining in current epoch - durationRemaining := ctl.curEpochTargetEndTime.Sub(tb.TimeObserved) + tau := ctl.targetViewTime() // τ: idealized target view time in units of seconds + viewDurationsRemaining := ctl.curEpochFinalView + 1 - view // k[v]: views remaining in current epoch + durationRemaining := u2t(ctl.curEpochTargetEndTime).Sub(tb.TimeObserved) // Γ[v] = T[v] - t[v], with t[v] ≡ tb.TimeObserved the time when observing the block that trigged the view change // Compute instantaneous error term: e[v] = k[v]·τ - T[v] i.e. the projected difference from target switchover // and update PID controller's error terms. All UNITS in SECOND. @@ -377,7 +405,7 @@ func (ctl *BlockTimeController) measureViewDuration(tb TimedBlock) error { u := propErr*ctl.config.KP + itgErr*ctl.config.KI + drivErr*ctl.config.KD // compute the controller output for this observation - unconstrainedBlockTime := time.Duration((tau - u) * float64(time.Second)) // desired time between parent and child block, in units of seconds + unconstrainedBlockTime := f2d(tau - u) // desired time between parent and child block, in units of seconds proposalTiming := newHappyPathBlockTime(tb, unconstrainedBlockTime, ctl.config.TimingConfig) constrainedBlockTime := proposalTiming.ConstrainedBlockTime() @@ -390,13 +418,13 @@ func (ctl *BlockTimeController) measureViewDuration(tb TimedBlock) error { Float64("proportional_err", propErr). Float64("integral_err", itgErr). Float64("derivative_err", drivErr). - Dur("controller_output", time.Duration(u*float64(time.Second))). + Dur("controller_output", f2d(u)). Dur("unconstrained_block_time", unconstrainedBlockTime). Dur("constrained_block_time", constrainedBlockTime). Msg("measured error upon view change") ctl.metrics.PIDError(propErr, itgErr, drivErr) - ctl.metrics.ControllerOutput(time.Duration(u * float64(time.Second))) + ctl.metrics.ControllerOutput(f2d(u)) ctl.metrics.TargetProposalDuration(proposalTiming.ConstrainedBlockTime()) ctl.storeProposalTiming(proposalTiming) @@ -416,9 +444,20 @@ func (ctl *BlockTimeController) processEpochSetupPhaseStarted(snapshot protocol. nextEpoch := snapshot.Epochs().Next() finalView, err := nextEpoch.FinalView() if err != nil { - return fmt.Errorf("could not get next epochInfo final view: %w", err) + return fmt.Errorf("could not get next epoch final view: %w", err) + } + targetDuration, err := nextEpoch.TargetDuration() + if err != nil { + return fmt.Errorf("could not get next epoch target duration: %w", err) + } + targetEndTime, err := nextEpoch.TargetEndTime() + if err != nil { + return fmt.Errorf("could not get next epoch target end time: %w", err) } + ctl.epochInfo.nextEpochFinalView = &finalView + ctl.epochInfo.nextEpochTargetDuration = &targetDuration + ctl.epochInfo.nextEpochTargetEndTime = &targetEndTime return nil } @@ -460,3 +499,19 @@ func (ctl *BlockTimeController) EpochSetupPhaseStarted(_ uint64, first *flow.Hea func (ctl *BlockTimeController) EpochEmergencyFallbackTriggered() { ctl.epochFallbacks <- struct{}{} } + +// t2u converts a time.Time to UNIX time represented as a uint64. +// Returned timestamp is precise to within one second of input. +func t2u(t time.Time) uint64 { + return uint64(t.Unix()) +} + +// u2t converts a UNIX timestamp represented as a uint64 to a time.Time. +func u2t(unix uint64) time.Time { + return time.Unix(int64(unix), 0) +} + +// f2d converts a floating-point number of seconds to a time.Duration. +func f2d(sec float64) time.Duration { + return time.Duration(int64(sec * float64(time.Second))) +} diff --git a/consensus/hotstuff/cruisectl/block_time_controller_test.go b/consensus/hotstuff/cruisectl/block_time_controller_test.go index d6cc074ab6b..46c6cf20e65 100644 --- a/consensus/hotstuff/cruisectl/block_time_controller_test.go +++ b/consensus/hotstuff/cruisectl/block_time_controller_test.go @@ -29,6 +29,8 @@ type BlockTimeControllerSuite struct { epochCounter uint64 curEpochFirstView uint64 curEpochFinalView uint64 + curEpochTargetDuration uint64 + curEpochTargetEndTime uint64 epochFallbackTriggered bool metrics mockmodule.CruiseCtlMetrics @@ -48,6 +50,11 @@ func TestBlockTimeController(t *testing.T) { suite.Run(t, new(BlockTimeControllerSuite)) } +// EpochDurationSeconds returns the number of seconds in the epoch (1hr). +func (bs *BlockTimeControllerSuite) EpochDurationSeconds() uint64 { + return 60 * 60 +} + // SetupTest initializes mocks and default values. func (bs *BlockTimeControllerSuite) SetupTest() { bs.config = DefaultConfig() @@ -55,7 +62,9 @@ func (bs *BlockTimeControllerSuite) SetupTest() { bs.initialView = 0 bs.epochCounter = uint64(0) bs.curEpochFirstView = uint64(0) - bs.curEpochFinalView = uint64(604_800) // 1 view/sec + bs.curEpochFinalView = bs.EpochDurationSeconds() // 1 view/sec for 1hr epoch + bs.curEpochTargetDuration = bs.EpochDurationSeconds() + bs.curEpochTargetEndTime = uint64(time.Now().Unix()) + bs.EpochDurationSeconds() bs.epochFallbackTriggered = false setupMocks(bs) } @@ -86,6 +95,8 @@ func setupMocks(bs *BlockTimeControllerSuite) { bs.curEpoch.On("Counter").Return(bs.epochCounter, nil) bs.curEpoch.On("FirstView").Return(bs.curEpochFirstView, nil) bs.curEpoch.On("FinalView").Return(bs.curEpochFinalView, nil) + bs.curEpoch.On("TargetDuration").Return(bs.curEpochTargetDuration, nil) + bs.curEpoch.On("TargetEndTime").Return(bs.curEpochTargetEndTime, nil) bs.epochs.Add(&bs.curEpoch) bs.ctx, bs.cancel = irrecoverable.NewMockSignalerContextWithCancel(bs.T(), context.Background()) @@ -126,10 +137,10 @@ func (bs *BlockTimeControllerSuite) AssertCorrectInitialization() { // should initialize epoch info epoch := bs.ctl.epochInfo - expectedEndTime := bs.config.TargetTransition.inferTargetEndTime(time.Now(), epoch.fractionComplete(bs.initialView)) assert.Equal(bs.T(), bs.curEpochFirstView, epoch.curEpochFirstView) assert.Equal(bs.T(), bs.curEpochFinalView, epoch.curEpochFinalView) - assert.Equal(bs.T(), expectedEndTime, epoch.curEpochTargetEndTime) + assert.Equal(bs.T(), bs.curEpochTargetDuration, epoch.curEpochTargetDuration) + assert.Equal(bs.T(), bs.curEpochTargetEndTime, epoch.curEpochTargetEndTime) // if next epoch is set up, final view should be set if phase := bs.epochs.Phase(); phase > flow.EpochPhaseStaking { @@ -196,7 +207,9 @@ func (bs *BlockTimeControllerSuite) TestInit_EpochStakingPhase() { func (bs *BlockTimeControllerSuite) TestInit_EpochSetupPhase() { nextEpoch := mockprotocol.NewEpoch(bs.T()) nextEpoch.On("Counter").Return(bs.epochCounter+1, nil) - nextEpoch.On("FinalView").Return(bs.curEpochFinalView+100_000, nil) + nextEpoch.On("FinalView").Return(bs.curEpochFinalView*2, nil) + nextEpoch.On("TargetDuration").Return(bs.EpochDurationSeconds(), nil) + nextEpoch.On("TargetEndTime").Return(bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), nil) bs.epochs.Add(nextEpoch) bs.CreateAndStartController() @@ -365,7 +378,9 @@ func (bs *BlockTimeControllerSuite) TestOnBlockIncorporated_EpochTransition_Disa func (bs *BlockTimeControllerSuite) testOnBlockIncorporated_EpochTransition() { nextEpoch := mockprotocol.NewEpoch(bs.T()) nextEpoch.On("Counter").Return(bs.epochCounter+1, nil) - nextEpoch.On("FinalView").Return(bs.curEpochFinalView+100_000, nil) + nextEpoch.On("FinalView").Return(bs.curEpochFinalView*2, nil) + nextEpoch.On("TargetDuration").Return(bs.EpochDurationSeconds(), nil) // 1s/view + nextEpoch.On("TargetEndTime").Return(bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), nil) bs.epochs.Add(nextEpoch) bs.CreateAndStartController() defer bs.StopController() @@ -381,7 +396,8 @@ func (bs *BlockTimeControllerSuite) testOnBlockIncorporated_EpochTransition() { bs.SanityCheckSubsequentMeasurements(initialControllerState, nextControllerState, false) // epoch boundaries should be updated assert.Equal(bs.T(), bs.curEpochFinalView+1, bs.ctl.epochInfo.curEpochFirstView) - assert.Equal(bs.T(), bs.ctl.epochInfo.curEpochFinalView, bs.curEpochFinalView+100_000) + assert.Equal(bs.T(), bs.ctl.epochInfo.curEpochFinalView, bs.curEpochFinalView*2) + assert.Equal(bs.T(), bs.ctl.epochInfo.curEpochTargetEndTime, bs.curEpochTargetEndTime+bs.EpochDurationSeconds()) assert.Nil(bs.T(), bs.ctl.nextEpochFinalView) } @@ -389,7 +405,9 @@ func (bs *BlockTimeControllerSuite) testOnBlockIncorporated_EpochTransition() { func (bs *BlockTimeControllerSuite) TestOnEpochSetupPhaseStarted() { nextEpoch := mockprotocol.NewEpoch(bs.T()) nextEpoch.On("Counter").Return(bs.epochCounter+1, nil) - nextEpoch.On("FinalView").Return(bs.curEpochFinalView+100_000, nil) + nextEpoch.On("FinalView").Return(bs.curEpochFinalView*2, nil) + nextEpoch.On("TargetDuration").Return(bs.EpochDurationSeconds(), nil) + nextEpoch.On("TargetEndTime").Return(bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), nil) bs.epochs.Add(nextEpoch) bs.CreateAndStartController() defer bs.StopController() @@ -400,13 +418,15 @@ func (bs *BlockTimeControllerSuite) TestOnEpochSetupPhaseStarted() { return bs.ctl.nextEpochFinalView != nil }, time.Second, time.Millisecond) - assert.Equal(bs.T(), bs.curEpochFinalView+100_000, *bs.ctl.nextEpochFinalView) + assert.Equal(bs.T(), bs.curEpochFinalView*2, *bs.ctl.nextEpochFinalView) + assert.Equal(bs.T(), bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), *bs.ctl.nextEpochTargetEndTime) // duplicate events should be no-ops for i := 0; i <= cap(bs.ctl.epochSetups); i++ { bs.ctl.EpochSetupPhaseStarted(bs.epochCounter, header) } - assert.Equal(bs.T(), bs.curEpochFinalView+100_000, *bs.ctl.nextEpochFinalView) + assert.Equal(bs.T(), bs.curEpochFinalView*2, *bs.ctl.nextEpochFinalView) + assert.Equal(bs.T(), bs.curEpochTargetEndTime+bs.EpochDurationSeconds(), *bs.ctl.nextEpochTargetEndTime) } // TestProposalDelay_AfterTargetTransitionTime tests the behaviour of the controller @@ -418,10 +438,10 @@ func (bs *BlockTimeControllerSuite) TestProposalDelay_AfterTargetTransitionTime( bs.CreateAndStartController() defer bs.StopController() - lastProposalDelay := time.Hour // start with large dummy value + lastProposalDelay := float64(bs.EpochDurationSeconds()) // start with large dummy value for view := bs.initialView + 1; view < bs.ctl.curEpochFinalView; view++ { // we have passed the target end time of the epoch - receivedParentBlockAt := bs.ctl.curEpochTargetEndTime.Add(time.Duration(view) * time.Second) + receivedParentBlockAt := u2t(bs.ctl.curEpochTargetEndTime + view) timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), receivedParentBlockAt) err := bs.ctl.measureViewDuration(timedBlock) require.NoError(bs.T(), err) @@ -430,8 +450,8 @@ func (bs *BlockTimeControllerSuite) TestProposalDelay_AfterTargetTransitionTime( pubTime := bs.ctl.GetProposalTiming().TargetPublicationTime(view+1, time.Now().UTC(), timedBlock.Block.BlockID) // simulate building a child of `timedBlock` delay := pubTime.Sub(receivedParentBlockAt) - assert.LessOrEqual(bs.T(), delay, lastProposalDelay) - lastProposalDelay = delay + assert.LessOrEqual(bs.T(), delay.Seconds(), lastProposalDelay) + lastProposalDelay = delay.Seconds() // transition views until the end of the epoch, or for 100 views if view-bs.initialView >= 100 { @@ -450,14 +470,14 @@ func (bs *BlockTimeControllerSuite) TestProposalDelay_BehindSchedule() { bs.CreateAndStartController() defer bs.StopController() - lastProposalDelay := time.Hour // start with large dummy value - idealEnteredViewTime := bs.ctl.curEpochTargetEndTime.Add(-epochLength / 2) + lastProposalDelay := float64(bs.EpochDurationSeconds()) // start with large dummy value + idealEnteredViewTime := u2t(bs.ctl.curEpochTargetEndTime - (bs.EpochDurationSeconds() / 2)) // 1s behind of schedule receivedParentBlockAt := idealEnteredViewTime.Add(time.Second) for view := bs.initialView + 1; view < bs.ctl.curEpochFinalView; view++ { // hold the instantaneous error constant for each view - receivedParentBlockAt = receivedParentBlockAt.Add(seconds2Duration(bs.ctl.targetViewTime())) + receivedParentBlockAt = receivedParentBlockAt.Add(f2d(bs.ctl.targetViewTime())) timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), receivedParentBlockAt) err := bs.ctl.measureViewDuration(timedBlock) require.NoError(bs.T(), err) @@ -466,8 +486,8 @@ func (bs *BlockTimeControllerSuite) TestProposalDelay_BehindSchedule() { pubTime := bs.ctl.GetProposalTiming().TargetPublicationTime(view+1, time.Now().UTC(), timedBlock.Block.BlockID) // simulate building a child of `timedBlock` delay := pubTime.Sub(receivedParentBlockAt) // expecting decreasing GetProposalTiming - assert.LessOrEqual(bs.T(), delay, lastProposalDelay) - lastProposalDelay = delay + assert.LessOrEqual(bs.T(), delay.Seconds(), lastProposalDelay, "got non-decreasing delay on view %d (initial view: %d)", view, bs.initialView) + lastProposalDelay = delay.Seconds() // transition views until the end of the epoch, or for 100 views if view-bs.initialView >= 100 { @@ -487,19 +507,19 @@ func (bs *BlockTimeControllerSuite) TestProposalDelay_AheadOfSchedule() { defer bs.StopController() lastProposalDelay := time.Duration(0) // start with large dummy value - idealEnteredViewTime := bs.ctl.curEpochTargetEndTime.Add(-epochLength / 2) + idealEnteredViewTime := bs.ctl.curEpochTargetEndTime - (bs.EpochDurationSeconds() / 2) // 1s ahead of schedule - receivedParentBlockAt := idealEnteredViewTime.Add(-time.Second) + receivedParentBlockAt := idealEnteredViewTime - 1 for view := bs.initialView + 1; view < bs.ctl.curEpochFinalView; view++ { // hold the instantaneous error constant for each view - receivedParentBlockAt = receivedParentBlockAt.Add(seconds2Duration(bs.ctl.targetViewTime())) - timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), receivedParentBlockAt) + receivedParentBlockAt = receivedParentBlockAt + uint64(bs.ctl.targetViewTime()) + timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), u2t(receivedParentBlockAt)) err := bs.ctl.measureViewDuration(timedBlock) require.NoError(bs.T(), err) // compute proposal delay: pubTime := bs.ctl.GetProposalTiming().TargetPublicationTime(view+1, time.Now().UTC(), timedBlock.Block.BlockID) // simulate building a child of `timedBlock` - delay := pubTime.Sub(receivedParentBlockAt) + delay := pubTime.Sub(u2t(receivedParentBlockAt)) // expecting increasing GetProposalTiming assert.GreaterOrEqual(bs.T(), delay, lastProposalDelay) @@ -543,7 +563,7 @@ func (bs *BlockTimeControllerSuite) TestMetrics() { assert.Greater(bs.T(), output, time.Duration(0)) }).Once() - timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), enteredViewAt) + timedBlock := makeTimedBlock(view, unittest.IdentifierFixture(), u2t(enteredViewAt)) err := bs.ctl.measureViewDuration(timedBlock) require.NoError(bs.T(), err) } @@ -556,16 +576,18 @@ func (bs *BlockTimeControllerSuite) TestMetrics() { func (bs *BlockTimeControllerSuite) Test_vs_PythonSimulation() { // PART 1: setup system to mirror python simulation // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + refT := time.Now().UTC() + refT = time.Date(refT.Year(), refT.Month(), refT.Day(), refT.Hour(), refT.Minute(), 0, 0, time.UTC) // truncate to past minute + totalEpochViews := 483000 bs.initialView = 0 bs.curEpochFirstView, bs.curEpochFinalView = uint64(0), uint64(totalEpochViews-1) // views [0, .., totalEpochViews-1] + bs.curEpochTargetDuration = 7 * 24 * 60 * 60 // 1 week in seconds + bs.curEpochTargetEndTime = t2u(refT) + bs.curEpochTargetDuration // now + 1 week bs.epochFallbackTriggered = false - refT := time.Now().UTC() - refT = time.Date(refT.Year(), refT.Month(), refT.Day(), refT.Hour(), refT.Minute(), 0, 0, time.UTC) // truncate to past minute bs.config = &Config{ TimingConfig: TimingConfig{ - TargetTransition: EpochTransitionTime{day: refT.Weekday(), hour: uint8(refT.Hour()), minute: uint8(refT.Minute())}, FallbackProposalDelay: atomic.NewDuration(500 * time.Millisecond), // irrelevant for this test, as controller should never enter fallback mode MinViewDuration: atomic.NewDuration(470 * time.Millisecond), MaxViewDuration: atomic.NewDuration(2010 * time.Millisecond), @@ -617,7 +639,7 @@ func (bs *BlockTimeControllerSuite) Test_vs_PythonSimulation() { // PART 3: run controller and ensure output matches pre-generated controller response from python ref implementation // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // sanity checks: - require.Equal(bs.T(), 604800.0, bs.ctl.curEpochTargetEndTime.UTC().Sub(refT).Seconds(), "Epoch should end 1 week from now, i.e. 604800s") + require.Equal(bs.T(), uint64(604800), bs.ctl.curEpochTargetEndTime-t2u(refT), "Epoch should end 1 week from now, i.e. 604800s") require.InEpsilon(bs.T(), ref.targetViewTime, bs.ctl.targetViewTime(), 1e-15) // ideal view time require.Equal(bs.T(), len(ref.observedMinViewTimes), len(ref.realWorldViewDuration)) @@ -639,9 +661,10 @@ func (bs *BlockTimeControllerSuite) Test_vs_PythonSimulation() { tpt := proposalTiming.TargetPublicationTime(uint64(v+1), time.Now(), observedBlock.Block.BlockID) // value for `timeViewEntered` should be irrelevant here controllerTargetedViewDuration := tpt.Sub(observedBlock.TimeObserved).Seconds() + bs.T().Logf("%d: ctl=%f\tref=%f\tdiff=%f", v, controllerTargetedViewDuration, ref.controllerTargetedViewDuration[v], controllerTargetedViewDuration-ref.controllerTargetedViewDuration[v]) require.InEpsilon(bs.T(), ref.controllerTargetedViewDuration[v], controllerTargetedViewDuration, 1e-5, "implementations deviate for view %d", v) // ideal view time - observationTime = observationTime.Add(time.Duration(int64(ref.realWorldViewDuration[v] * float64(time.Second)))) + observationTime = observationTime.Add(f2d(ref.realWorldViewDuration[v])) } } @@ -670,7 +693,3 @@ func captureControllerStateDigest(ctl *BlockTimeController) *controllerStateDige latestProposalTiming: ctl.GetProposalTiming(), } } - -func seconds2Duration(durationinDeconds float64) time.Duration { - return time.Duration(int64(durationinDeconds * float64(time.Second))) -} diff --git a/consensus/hotstuff/cruisectl/config.go b/consensus/hotstuff/cruisectl/config.go index 48a6f2b1139..4c188e0d886 100644 --- a/consensus/hotstuff/cruisectl/config.go +++ b/consensus/hotstuff/cruisectl/config.go @@ -10,7 +10,6 @@ import ( func DefaultConfig() *Config { return &Config{ TimingConfig{ - TargetTransition: DefaultEpochTransitionTime(), FallbackProposalDelay: atomic.NewDuration(250 * time.Millisecond), MinViewDuration: atomic.NewDuration(600 * time.Millisecond), MaxViewDuration: atomic.NewDuration(1600 * time.Millisecond), @@ -34,9 +33,6 @@ type Config struct { // TimingConfig specifies the BlockTimeController's limits of authority. type TimingConfig struct { - // TargetTransition defines the target time to transition epochs each week. - TargetTransition EpochTransitionTime - // FallbackProposalDelay is the minimal block construction delay. When used, it behaves like the // old command line flag `block-rate-delay`. Specifically, the primary measures the duration from // starting to construct its proposal to the proposal being ready to be published. If this @@ -94,33 +90,46 @@ func (c *ControllerParams) beta() float64 { return 1.0 / float64(c.N_itg) } -func (ctl *TimingConfig) GetFallbackProposalDuration() time.Duration { +// GetFallbackProposalDuration returns the proposal duration used when Cruise Control is not active. +func (ctl TimingConfig) GetFallbackProposalDuration() time.Duration { return ctl.FallbackProposalDelay.Load() } -func (ctl *TimingConfig) GetMaxViewDuration() time.Duration { + +// GetMaxViewDuration returns the max view duration returned by the controller. +func (ctl TimingConfig) GetMaxViewDuration() time.Duration { return ctl.MaxViewDuration.Load() } -func (ctl *TimingConfig) GetMinViewDuration() time.Duration { + +// GetMinViewDuration returns the min view duration returned by the controller. +func (ctl TimingConfig) GetMinViewDuration() time.Duration { return ctl.MinViewDuration.Load() } -func (ctl *TimingConfig) GetEnabled() bool { + +// GetEnabled returns whether the controller is enabled. +func (ctl TimingConfig) GetEnabled() bool { return ctl.Enabled.Load() } -func (ctl *TimingConfig) SetFallbackProposalDuration(dur time.Duration) error { +// SetFallbackProposalDuration sets the proposal duration used when Cruise Control is not active. +func (ctl TimingConfig) SetFallbackProposalDuration(dur time.Duration) error { ctl.FallbackProposalDelay.Store(dur) return nil } -func (ctl *TimingConfig) SetMaxViewDuration(dur time.Duration) error { + +// SetMaxViewDuration sets the max view duration returned by the controller. +func (ctl TimingConfig) SetMaxViewDuration(dur time.Duration) error { ctl.MaxViewDuration.Store(dur) return nil } -func (ctl *TimingConfig) SetMinViewDuration(dur time.Duration) error { + +// SetMinViewDuration sets the min view duration returned by the controller. +func (ctl TimingConfig) SetMinViewDuration(dur time.Duration) error { ctl.MinViewDuration.Store(dur) return nil - } -func (ctl *TimingConfig) SetEnabled(enabled bool) error { + +// SetEnabled sets whether the controller is enabled. +func (ctl TimingConfig) SetEnabled(enabled bool) error { ctl.Enabled.Store(enabled) return nil } diff --git a/consensus/hotstuff/cruisectl/transition_time.go b/consensus/hotstuff/cruisectl/transition_time.go deleted file mode 100644 index 52bfad3486b..00000000000 --- a/consensus/hotstuff/cruisectl/transition_time.go +++ /dev/null @@ -1,172 +0,0 @@ -package cruisectl - -import ( - "fmt" - "strings" - "time" -) - -// weekdays is a lookup from canonical weekday strings to the time package constant. -var weekdays = map[string]time.Weekday{ - strings.ToLower(time.Sunday.String()): time.Sunday, - strings.ToLower(time.Monday.String()): time.Monday, - strings.ToLower(time.Tuesday.String()): time.Tuesday, - strings.ToLower(time.Wednesday.String()): time.Wednesday, - strings.ToLower(time.Thursday.String()): time.Thursday, - strings.ToLower(time.Friday.String()): time.Friday, - strings.ToLower(time.Saturday.String()): time.Saturday, -} - -// epochLength is the length of an epoch (7 days, or 1 week). -const epochLength = time.Hour * 24 * 7 - -var transitionFmt = "%s@%02d:%02d" // example: wednesday@08:00 - -// EpochTransitionTime represents the target epoch transition time. -// Epochs last one week, so the transition is defined in terms of a day-of-week and time-of-day. -// The target time is always in UTC to avoid confusion resulting from different -// representations of the same transition time and around daylight savings time. -type EpochTransitionTime struct { - day time.Weekday // day of every week to target epoch transition - hour uint8 // hour of the day to target epoch transition - minute uint8 // minute of the hour to target epoch transition -} - -// DefaultEpochTransitionTime is the default epoch transition target. -// The target switchover is Wednesday 12:00 PDT, which is 19:00 UTC. -// The string representation is `wednesday@19:00`. -func DefaultEpochTransitionTime() EpochTransitionTime { - return EpochTransitionTime{ - day: time.Wednesday, - hour: 19, - minute: 0, - } -} - -// String returns the canonical string representation of the transition time. -// This is the format expected as user input, when this value is configured manually. -// See ParseSwitchover for details of the format. -func (tt *EpochTransitionTime) String() string { - return fmt.Sprintf(transitionFmt, strings.ToLower(tt.day.String()), tt.hour, tt.minute) -} - -// newInvalidTransitionStrError returns an informational error about an invalid transition string. -func newInvalidTransitionStrError(s string, msg string, args ...any) error { - args = append([]any{s}, args...) - return fmt.Errorf("invalid transition string (%s): "+msg, args...) -} - -// ParseTransition parses a transition time string. -// A transition string must be specified according to the format: -// -// WD@HH:MM -// -// WD is the weekday string as defined by `strings.ToLower(time.Weekday.String)` -// HH is the 2-character hour of day, in the range [00-23] -// MM is the 2-character minute of hour, in the range [00-59] -// All times are in UTC. -// -// A generic error is returned if the input is an invalid transition string. -func ParseTransition(s string) (*EpochTransitionTime, error) { - strs := strings.Split(s, "@") - if len(strs) != 2 { - return nil, newInvalidTransitionStrError(s, "split on @ yielded %d substrings - expected %d", len(strs), 2) - } - dayStr := strs[0] - timeStr := strs[1] - if len(timeStr) != 5 || timeStr[2] != ':' { - return nil, newInvalidTransitionStrError(s, "time part must have form HH:MM") - } - - var hour uint8 - _, err := fmt.Sscanf(timeStr[0:2], "%02d", &hour) - if err != nil { - return nil, newInvalidTransitionStrError(s, "error scanning hour part: %w", err) - } - var minute uint8 - _, err = fmt.Sscanf(timeStr[3:5], "%02d", &minute) - if err != nil { - return nil, newInvalidTransitionStrError(s, "error scanning minute part: %w", err) - } - - day, ok := weekdays[strings.ToLower(dayStr)] - if !ok { - return nil, newInvalidTransitionStrError(s, "invalid weekday part %s", dayStr) - } - if hour > 23 { - return nil, newInvalidTransitionStrError(s, "invalid hour part: %d>23", hour) - } - if minute > 59 { - return nil, newInvalidTransitionStrError(s, "invalid minute part: %d>59", hour) - } - - return &EpochTransitionTime{ - day: day, - hour: hour, - minute: minute, - }, nil -} - -// inferTargetEndTime infers the target end time for the current epoch, based on -// the current progress through the epoch and the current time. -// We do this in 3 steps: -// 1. find the 3 candidate target end times nearest to the current time. -// 2. compute the estimated end time for the current epoch. -// 3. select the candidate target end time which is nearest to the estimated end time. -// -// NOTE 1: This method is effective only if the node's local notion of current view and -// time are accurate. If a node is, for example, catching up from a very old state, it -// will infer incorrect target end times. Since catching-up nodes don't produce usable -// proposals, this is OK. -// NOTE 2: In the long run, the target end time should be specified by the smart contract -// and stored along with the other protocol.Epoch information. This would remove the -// need for this imperfect inference logic. -func (tt *EpochTransitionTime) inferTargetEndTime(curTime time.Time, epochFractionComplete float64) time.Time { - now := curTime.UTC() - // find the nearest target end time, plus the targets one week before and after - nearestTargetDate := tt.findNearestTargetTime(now) - earlierTargetDate := nearestTargetDate.AddDate(0, 0, -7) - laterTargetDate := nearestTargetDate.AddDate(0, 0, 7) - - estimatedTimeRemainingInEpoch := time.Duration((1.0 - epochFractionComplete) * float64(epochLength)) - estimatedEpochEndTime := now.Add(estimatedTimeRemainingInEpoch) - - minDiff := estimatedEpochEndTime.Sub(nearestTargetDate).Abs() - inferredTargetEndTime := nearestTargetDate - for _, date := range []time.Time{earlierTargetDate, laterTargetDate} { - // compare estimate to actual based on the target - diff := estimatedEpochEndTime.Sub(date).Abs() - if diff < minDiff { - minDiff = diff - inferredTargetEndTime = date - } - } - - return inferredTargetEndTime -} - -// findNearestTargetTime interprets ref as a date (ignores time-of-day portion) -// and finds the nearest date, either before or after ref, which has the given weekday. -// We then return a time.Time with this date and the hour/minute specified by the EpochTransitionTime. -func (tt *EpochTransitionTime) findNearestTargetTime(ref time.Time) time.Time { - ref = ref.UTC() - hour := int(tt.hour) - minute := int(tt.minute) - date := time.Date(ref.Year(), ref.Month(), ref.Day(), hour, minute, 0, 0, time.UTC) - - // walk back and forth by date around the reference until we find the closest matching weekday - walk := 0 - for date.Weekday() != tt.day || date.Sub(ref).Abs().Hours() > float64(24*7/2) { - walk++ - if walk%2 == 0 { - date = date.AddDate(0, 0, walk) - } else { - date = date.AddDate(0, 0, -walk) - } - // sanity check to avoid an infinite loop: should be impossible - if walk > 14 { - panic(fmt.Sprintf("unexpected failure to find nearest target time with ref=%s, transition=%s", ref.String(), tt.String())) - } - } - return date -} diff --git a/consensus/hotstuff/cruisectl/transition_time_test.go b/consensus/hotstuff/cruisectl/transition_time_test.go deleted file mode 100644 index 15bff07ce1e..00000000000 --- a/consensus/hotstuff/cruisectl/transition_time_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package cruisectl - -import ( - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "pgregory.net/rapid" -) - -// TestParseTransition_Valid tests that valid transition configurations have -// consistent parsing and formatting behaviour. -func TestParseTransition_Valid(t *testing.T) { - cases := []struct { - transition EpochTransitionTime - str string - }{{ - transition: EpochTransitionTime{time.Sunday, 0, 0}, - str: "sunday@00:00", - }, { - transition: EpochTransitionTime{time.Wednesday, 8, 1}, - str: "wednesday@08:01", - }, { - transition: EpochTransitionTime{time.Monday, 23, 59}, - str: "monday@23:59", - }, { - transition: EpochTransitionTime{time.Friday, 12, 21}, - str: "FrIdAy@12:21", - }} - - for _, c := range cases { - t.Run(c.str, func(t *testing.T) { - // 1 - the computed string representation should match the string fixture - assert.Equal(t, strings.ToLower(c.str), c.transition.String()) - // 2 - the parsed transition should match the transition fixture - parsed, err := ParseTransition(c.str) - assert.NoError(t, err) - assert.Equal(t, c.transition, *parsed) - }) - } -} - -// TestParseTransition_Invalid tests that a selection of invalid transition strings -// fail validation and return an error. -func TestParseTransition_Invalid(t *testing.T) { - cases := []string{ - // invalid WD part - "sundy@12:00", - "tue@12:00", - "@12:00", - // invalid HH part - "wednesday@24:00", - "wednesday@1:00", - "wednesday@:00", - "wednesday@012:00", - // invalid MM part - "wednesday@12:60", - "wednesday@12:1", - "wednesday@12:", - "wednesday@12:030", - // otherwise invalid - "", - "@:", - "monday@@12:00", - "monday@09:00am", - "monday@09:00PM", - "monday12:00", - "monday12::00", - "wednesday@1200", - } - - for _, transitionStr := range cases { - t.Run(transitionStr, func(t *testing.T) { - _, err := ParseTransition(transitionStr) - assert.Error(t, err) - }) - } -} - -// drawTransitionTime draws a random EpochTransitionTime. -func drawTransitionTime(t *rapid.T) EpochTransitionTime { - day := time.Weekday(rapid.IntRange(0, 6).Draw(t, "wd").(int)) - hour := rapid.Uint8Range(0, 23).Draw(t, "h").(uint8) - minute := rapid.Uint8Range(0, 59).Draw(t, "m").(uint8) - return EpochTransitionTime{day, hour, minute} -} - -// TestInferTargetEndTime_Fixture is a single human-readable fixture test, -// in addition to the property-based rapid tests. -func TestInferTargetEndTime_Fixture(t *testing.T) { - // The target time is around midday Wednesday - // |S|M|T|W|T|F|S| - // | * | - ett := EpochTransitionTime{day: time.Wednesday, hour: 13, minute: 24} - // The current time is mid-morning on Friday. We are about 28% through the epoch in time terms - // |S|M|T|W|T|F|S| - // | * | - // Friday, November 20, 2020 11:44 - curTime := time.Date(2020, 11, 20, 11, 44, 0, 0, time.UTC) - // We are 18% through the epoch in view terms - we are quite behind schedule - epochFractionComplete := .18 - // We should still be able to infer the target switchover time: - // Wednesday, November 25, 2020 13:24 - expectedTarget := time.Date(2020, 11, 25, 13, 24, 0, 0, time.UTC) - target := ett.inferTargetEndTime(curTime, epochFractionComplete) - assert.Equal(t, expectedTarget, target) -} - -// TestInferTargetEndTime tests that we can infer "the most reasonable" target time. -func TestInferTargetEndTime_Rapid(t *testing.T) { - rapid.Check(t, func(t *rapid.T) { - ett := drawTransitionTime(t) - curTime := time.Unix(rapid.Int64().Draw(t, "ref_unix").(int64), 0).UTC() - epochFractionComplete := rapid.Float64Range(0, 1).Draw(t, "pct_complete").(float64) - epochFractionRemaining := 1.0 - epochFractionComplete - - target := ett.inferTargetEndTime(curTime, epochFractionComplete) - computedEndTime := curTime.Add(time.Duration(float64(epochLength) * epochFractionRemaining)) - // selected target must be the nearest to the computed end time - delta := computedEndTime.Sub(target).Abs() - assert.LessOrEqual(t, delta.Hours(), float64(24*7)/2) - // nearest date must be a target time - assert.Equal(t, ett.day, target.Weekday()) - assert.Equal(t, int(ett.hour), target.Hour()) - assert.Equal(t, int(ett.minute), target.Minute()) - }) -} - -// TestFindNearestTargetTime tests finding the nearest target time to a reference time. -func TestFindNearestTargetTime(t *testing.T) { - rapid.Check(t, func(t *rapid.T) { - ett := drawTransitionTime(t) - ref := time.Unix(rapid.Int64().Draw(t, "ref_unix").(int64), 0).UTC() - - nearest := ett.findNearestTargetTime(ref) - distance := nearest.Sub(ref).Abs() - // nearest date must be at most 1/2 a week away - assert.LessOrEqual(t, distance.Hours(), float64(24*7)/2) - // nearest date must be a target time - assert.Equal(t, ett.day, nearest.Weekday()) - assert.Equal(t, int(ett.hour), nearest.Hour()) - assert.Equal(t, int(ett.minute), nearest.Minute()) - }) -} diff --git a/state/protocol/inmem/encodable.go b/state/protocol/inmem/encodable.go index 1f000a413b9..192f8add4bf 100644 --- a/state/protocol/inmem/encodable.go +++ b/state/protocol/inmem/encodable.go @@ -36,8 +36,8 @@ type EncodableEpoch struct { DKGPhase3FinalView uint64 FinalView uint64 RandomSource []byte - TargetDuration uint64 // desired real-world end time for the epoch in seconds - TargetEndTime uint64 // desired real-world end time for the epoch in unix time [seconds] + TargetDuration uint64 // desired real-world duration for the epoch, in seconds + TargetEndTime uint64 // desired real-world end time for the epoch, in UNIX time [seconds] InitialIdentities flow.IdentityList Clustering flow.ClusterList Clusters []EncodableCluster