From d48d0275b22eeb5545786cbbc4d96eb866d432c1 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 3 Oct 2023 15:06:49 -0400 Subject: [PATCH 1/7] add flip --- ...e-account-specified-epoch-switchover.md.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 protocol/20231003-service-account-specified-epoch-switchover.md.md diff --git a/protocol/20231003-service-account-specified-epoch-switchover.md.md b/protocol/20231003-service-account-specified-epoch-switchover.md.md new file mode 100644 index 00000000..9a32b5b6 --- /dev/null +++ b/protocol/20231003-service-account-specified-epoch-switchover.md.md @@ -0,0 +1,235 @@ +--- +status: draft +flip: NNN (set to the issue number) +authors: Jordan Schalm (jordan@dapperlabs.com) +sponsor: Jordan Schalm (jordan@dapperlabs.com) +updated: 2023-10-03 +--- + +# FLIP NNN: Smart-Contract-Specified Epoch Switchover Timing + +## Objective + +- Increase robustness of Cruise Control System +- Create an explicit target time for epoch switchover, defined by the service account + +## Motivation + +At the time of writing, the target epoch switchover time is inferred based on a baked-in assumption of week-long epochs, and a configurable weekly switchover time. Therefore, each node’s **Process Variable** (switchover time) is determined by a heuristic, which has several downsides: + +- In extreme edge cases (timing off by several days), different nodes may disagree about the target Process Variable value. +- Networks with different-length epochs (Canary, Testnet) can not use Cruise Control at all. + +## User Benefit + +- Chain-queriable target epoch switchover time +- Increased robustness of Cruise Control System (reliable and consistent epoch and block timing) + +## Design Proposal + + +- The `FlowEpoch` smart contract determines and broadcasts a `TargetEndTime` for each epoch, within the `EpochSetup` event. +- The `cruisectl.BlockTimeController` component reads this `TargetEndTime` and uses it as the Process Variable value for its PID controller, rather than the current heuristic method. + +### `TargetEndTime` Definition + +Below are two options for how to configure and compute the `TargetEndTime`. Overall the author is in favour of **Option 2.** + + +#### Option 1: Duration-Only + +The configuration consists only of the epoch duration. Each epoch’s `TargetEndTime` is computed based on a reference time/view pair obtained via the `getBlock` API. + +```swift +pub struct EpochTimingConfig { + duration: UInt64 // in seconds +} +``` + +```swift +// Compute the target switchover time based on the current time/view. +// Invoked when transitioning into the EpochSetup phase. +pub fun getTargetEndTimeForEpoch( + curBlock: Block, + epoch: EpochMetadata, + config EpochTimingConfig, +): UInt64 { + let now = curBlock.timestamp + let viewsToEpochEnd = nextEpoch.finalView - curBlock.view + let estSecondsToNextEpochEnd = UFix64(viewsToNextEpochEnd) / UFix64(nextEpoch.lengthInViews) * config.duration + return UInt64(estSecondsToNextEpochEnd) +} +``` + +```swift +// Memorize the end time of each epoch. +// Invoked when transitioning into a new epoch. +pub fun memorizeEpochEndTime(curBlock: Block, epoch: EpochMetadata) { + epoch.endedAt = curBlock.timestamp +} + +// Compute the switchover time based on the last memorized reference timestamp. +pub fun getTargetEndTimeForEpoch( + refEpoch: EpochMetadata, + targetEpochCounter: UInt64, + config EpochTimingConfig, +): UInt64 { + return refEpoch.endedAt + config.duration * (targetEpochCounter-refEpoch.counter) +} +``` + +##### Pros + +- Simpler configuration +- Does not require manual config changes to account for a durable switchover timing change. + +##### Cons + +- Drift can accumulate over time +- **Approach 1.2** Does not work well with `resetEpoch` process, as that involves an epoch transition at a non-target time +- Depends on block time API +- Approach 2 requires additional storage/logic in smart contract changes + +#### Option 2: Duration & Reference Switchover + +The configuration consists of the epoch duration and a reference counter/end-time pair. Each epoch’s `TargetEndTime` is computed solely based on the target epoch’s counter, the reference counter/end-time pair, and the duration. + +```swift +pub struct EpochTimingConfig { + duration: UInt64 // in seconds + refCounter: UInt64 // the counter of a reference epoch + refTimestamp: UInt64 // the UNIX timestamp (UTC) at which refCounter ended +} +``` + +```swift +// Compute target switchover time based on offset from reference counter/switchover. +pub fun getTargetEndTimeForEpoch( + targetEpochCounter: UInt64, + config EpochTimingConfig, +): UInt64 { + return config.refTimestamp + config.duration * (targetEpochCounter-refCounter) +} +``` + +##### Pros + +- Simple computation +- Drift cannot accumulate over time +- Does not use block time API +- Compatible with `resetEpoch` process + +##### Cons + +- More complex configuration specification +- Requires manual config changes for durable switchover time changes + + +### Implementation Plan + +#### Smart Contract + +- Add `targetEndTime` field to `EpochSetup` event, `EpochMetadata` +- Add config for determining `targetEndTime` to smart contract `ConfigMetadata` +- Add logic to compute `targetEndTime` to `startEpochSetup` +- Add function for service account to adjust new config +- Testing + - Validate field is set as expected + - Validate field is computed correctly + - Validate setter/getter for new config values + +#### Core Protocol + +- Add `TargetEndTime` field to `EpochSetup` event, `Epoch` API +- Update `EpochSetup` service event conversion function + - Read `TargetEndTime` field + - Ensure conversion is backward-compatible +- Update `cruisectl.BlockRateController` + - Remove `EpochTransitionTime` inference heuristic + - Replace `EpochTransitionTime` with `time.Duration`, retrieved from `EpochSetup` event +- Add mechanism to set `TargetEndTime` in bootstrapping/sporking process + - *Comment: currently Cruise Control is disabled by default.* + - Option 1: Add an optional flag to explicitly the desired epoch duration (seconds). We can compute a reference counter/timestamp. + - Option 2: Compute an initial `duration` config value, based on the committee size and epoch length and expected view rate. +- Update network instantiation + - Default value for deploying `FlowEpoch` + +### Deployment Plan + +As usual, deploy to Canary → Testnet → Mainnet + +1. Upgrade `FlowEpoch` + + + +2. Upgrade Consensus Nodes + + *Comment: Since we are modifying the EpochSetup model, this will likely require a spork.* + + + +### Drawbacks + +This proposal assumes that the service account is more reliable than the heuristic currently in use for determining target epoch switchover times. +Many more important system functions already depend on correct operation of the service account. +However, Cruise Control will become susceptible to faults in the service account's defined switchover time, rather than faults in the current heuristic. + +### Alternatives Considered + +See 2 options above. + +### Performance Implications + +None anticipated. + +### Dependencies + +There are internal dependencies that will need to be updated in lockstep as part of this FLIP. No external dependencies. + +### Engineering Impact + +* Do you expect changes to binary size / build time / test times? +* Who will maintain this code? Is this code in its own buildable unit? +Can this code be tested in its own? +Is visibility suitably restricted to only a small API surface for others to use? + +### Best Practices + +N/A. + +### Tutorials and Examples + +N/A. + +### Compatibility + +The change will not break compatibility. + +### User Impact + +Little direct user impact. + +## Related Issues + +N/A. + +## Prior Art + +N/A. + +## Questions and Discussion Topics + +- Do you prefer Option 1 or Option 2? +- Do you foresee any significant hurdles beyond those outlined in the Implementation Plan? + +### Q&A +#### What happens during a `resetEpoch`? + +In ****************Option 1.1**************** and **2**, the timing of a particular epoch transition does not affect the target timing for other epochs. Therefore, the `TargetEndTime` computation of an epoch during a spork will not behave differently from any other epoch. + +#### Why do we set `TargetEndTime` rather than `TargetStartTime`? + +The time information is specified in the `EpochSetup` event, which occurs partway through the current epoch. If we specified a `TargetStartTime`, then the PID Controller’s Process Variable would have an undefined value for part of the epoch, and the Cruise Control system would be unable to function. On Mainnet, this corresponds to about 90% of the duration of an epoch. \ No newline at end of file From 253d147848f8bb1cbb2f707574ab0a980f4b37a1 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 3 Oct 2023 15:07:11 -0400 Subject: [PATCH 2/7] minor change --- .../20231003-service-account-specified-epoch-switchover.md.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/20231003-service-account-specified-epoch-switchover.md.md b/protocol/20231003-service-account-specified-epoch-switchover.md.md index 9a32b5b6..3148589e 100644 --- a/protocol/20231003-service-account-specified-epoch-switchover.md.md +++ b/protocol/20231003-service-account-specified-epoch-switchover.md.md @@ -210,7 +210,7 @@ The change will not break compatibility. ### User Impact -Little direct user impact. +N/A. ## Related Issues From 5647f130bb94a0d75152af377a99a0179919b270 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 3 Oct 2023 15:11:19 -0400 Subject: [PATCH 3/7] update flip # --- .../20231003-service-account-specified-epoch-switchover.md.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol/20231003-service-account-specified-epoch-switchover.md.md b/protocol/20231003-service-account-specified-epoch-switchover.md.md index 3148589e..531d2a90 100644 --- a/protocol/20231003-service-account-specified-epoch-switchover.md.md +++ b/protocol/20231003-service-account-specified-epoch-switchover.md.md @@ -1,12 +1,12 @@ --- status: draft -flip: NNN (set to the issue number) +flip: 204 (set to the issue number) authors: Jordan Schalm (jordan@dapperlabs.com) sponsor: Jordan Schalm (jordan@dapperlabs.com) updated: 2023-10-03 --- -# FLIP NNN: Smart-Contract-Specified Epoch Switchover Timing +# FLIP 204: Smart-Contract-Specified Epoch Switchover Timing ## Objective From 94f2c5099c4c8d1c48b667fb580b35d222aa54b8 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 3 Oct 2023 15:12:49 -0400 Subject: [PATCH 4/7] add context --- .../20231003-service-account-specified-epoch-switchover.md.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/protocol/20231003-service-account-specified-epoch-switchover.md.md b/protocol/20231003-service-account-specified-epoch-switchover.md.md index 531d2a90..45be2bc8 100644 --- a/protocol/20231003-service-account-specified-epoch-switchover.md.md +++ b/protocol/20231003-service-account-specified-epoch-switchover.md.md @@ -15,6 +15,8 @@ updated: 2023-10-03 ## Motivation +[Cruise Control: Automated Block Rate & Epoch Timing (Design)](https://www.notion.so/Cruise-Control-Automated-Block-Rate-Epoch-Timing-Design-4dbcb0dab1394fc7b91966d7d84ad48d?pvs=21) defines an existing system for controlling system block production to achieve a target *block rate*, in turn to achieve a target *epoch switchover time*. This system has been deployed on Mainnet since May. + At the time of writing, the target epoch switchover time is inferred based on a baked-in assumption of week-long epochs, and a configurable weekly switchover time. Therefore, each node’s **Process Variable** (switchover time) is determined by a heuristic, which has several downsides: - In extreme edge cases (timing off by several days), different nodes may disagree about the target Process Variable value. From f10b53859d56125f0a4b6820180b26359aafa29c Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 4 Oct 2023 12:11:46 -0400 Subject: [PATCH 5/7] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bastian Müller --- ...e-account-specified-epoch-switchover.md.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/protocol/20231003-service-account-specified-epoch-switchover.md.md b/protocol/20231003-service-account-specified-epoch-switchover.md.md index 45be2bc8..9386e853 100644 --- a/protocol/20231003-service-account-specified-epoch-switchover.md.md +++ b/protocol/20231003-service-account-specified-epoch-switchover.md.md @@ -42,7 +42,7 @@ Below are two options for how to configure and compute the `TargetEndTime`. Over The configuration consists only of the epoch duration. Each epoch’s `TargetEndTime` is computed based on a reference time/view pair obtained via the `getBlock` API. -```swift +```cadence pub struct EpochTimingConfig { duration: UInt64 // in seconds } @@ -52,12 +52,12 @@ pub struct EpochTimingConfig { // Compute the target switchover time based on the current time/view. // Invoked when transitioning into the EpochSetup phase. pub fun getTargetEndTimeForEpoch( - curBlock: Block, + currentBlock: Block, epoch: EpochMetadata, - config EpochTimingConfig, + config: EpochTimingConfig ): UInt64 { - let now = curBlock.timestamp - let viewsToEpochEnd = nextEpoch.finalView - curBlock.view + let now = currentBlock.timestamp + let viewsToEpochEnd = nextEpoch.finalView - currentBlock.view let estSecondsToNextEpochEnd = UFix64(viewsToNextEpochEnd) / UFix64(nextEpoch.lengthInViews) * config.duration return UInt64(estSecondsToNextEpochEnd) } @@ -66,17 +66,17 @@ pub fun getTargetEndTimeForEpoch( ```swift // Memorize the end time of each epoch. // Invoked when transitioning into a new epoch. -pub fun memorizeEpochEndTime(curBlock: Block, epoch: EpochMetadata) { - epoch.endedAt = curBlock.timestamp +pub fun memorizeEpochEndTime(currentBlock: Block, epoch: EpochMetadata) { + epoch.endedAt = currentBlock.timestamp } // Compute the switchover time based on the last memorized reference timestamp. -pub fun getTargetEndTimeForEpoch( - refEpoch: EpochMetadata, +pub fun getTargetEndTime( + forEpoch refEpoch: EpochMetadata, targetEpochCounter: UInt64, - config EpochTimingConfig, + config: EpochTimingConfig ): UInt64 { - return refEpoch.endedAt + config.duration * (targetEpochCounter-refEpoch.counter) + return refEpoch.endedAt + config.duration * (targetEpochCounter - refEpoch.counter) } ``` @@ -108,7 +108,7 @@ pub struct EpochTimingConfig { // Compute target switchover time based on offset from reference counter/switchover. pub fun getTargetEndTimeForEpoch( targetEpochCounter: UInt64, - config EpochTimingConfig, + config: EpochTimingConfig ): UInt64 { return config.refTimestamp + config.duration * (targetEpochCounter-refCounter) } @@ -230,7 +230,7 @@ N/A. ### Q&A #### What happens during a `resetEpoch`? -In ****************Option 1.1**************** and **2**, the timing of a particular epoch transition does not affect the target timing for other epochs. Therefore, the `TargetEndTime` computation of an epoch during a spork will not behave differently from any other epoch. +In **Option 1.1** and **2**, the timing of a particular epoch transition does not affect the target timing for other epochs. Therefore, the `TargetEndTime` computation of an epoch during a spork will not behave differently from any other epoch. #### Why do we set `TargetEndTime` rather than `TargetStartTime`? From e265ce9dcbf6b4a8d4f7d172f76e7931ad61d365 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 4 Oct 2023 12:12:37 -0400 Subject: [PATCH 6/7] Apply suggestions from code review --- ...31003-service-account-specified-epoch-switchover.md.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/protocol/20231003-service-account-specified-epoch-switchover.md.md b/protocol/20231003-service-account-specified-epoch-switchover.md.md index 9386e853..7647a839 100644 --- a/protocol/20231003-service-account-specified-epoch-switchover.md.md +++ b/protocol/20231003-service-account-specified-epoch-switchover.md.md @@ -48,7 +48,7 @@ pub struct EpochTimingConfig { } ``` -```swift +```cadence // Compute the target switchover time based on the current time/view. // Invoked when transitioning into the EpochSetup phase. pub fun getTargetEndTimeForEpoch( @@ -63,7 +63,7 @@ pub fun getTargetEndTimeForEpoch( } ``` -```swift +```cadence // Memorize the end time of each epoch. // Invoked when transitioning into a new epoch. pub fun memorizeEpochEndTime(currentBlock: Block, epoch: EpochMetadata) { @@ -96,7 +96,7 @@ pub fun getTargetEndTime( The configuration consists of the epoch duration and a reference counter/end-time pair. Each epoch’s `TargetEndTime` is computed solely based on the target epoch’s counter, the reference counter/end-time pair, and the duration. -```swift +```cadence pub struct EpochTimingConfig { duration: UInt64 // in seconds refCounter: UInt64 // the counter of a reference epoch @@ -104,7 +104,7 @@ pub struct EpochTimingConfig { } ``` -```swift +```cadence // Compute target switchover time based on offset from reference counter/switchover. pub fun getTargetEndTimeForEpoch( targetEpochCounter: UInt64, From 0616678b5ce4e8aa7d98b10c191ad8500c5ff3f6 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Thu, 5 Oct 2023 10:56:07 -0400 Subject: [PATCH 7/7] update flip based on review feedback - clean up code samples - label options 1.1, 1.2 - add suggestions --- ...rvice-account-specified-epoch-switchover.md.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/protocol/20231003-service-account-specified-epoch-switchover.md.md b/protocol/20231003-service-account-specified-epoch-switchover.md.md index 7647a839..531ebee5 100644 --- a/protocol/20231003-service-account-specified-epoch-switchover.md.md +++ b/protocol/20231003-service-account-specified-epoch-switchover.md.md @@ -31,6 +31,7 @@ At the time of writing, the target epoch switchover time is inferred based on a - The `FlowEpoch` smart contract determines and broadcasts a `TargetEndTime` for each epoch, within the `EpochSetup` event. + - (For informational purposes, we will also include this time in the `EpochStart` event) - The `cruisectl.BlockTimeController` component reads this `TargetEndTime` and uses it as the Process Variable value for its PID controller, rather than the current heuristic method. ### `TargetEndTime` Definition @@ -48,6 +49,7 @@ pub struct EpochTimingConfig { } ``` +##### Option 1.1 ```cadence // Compute the target switchover time based on the current time/view. // Invoked when transitioning into the EpochSetup phase. @@ -57,12 +59,13 @@ pub fun getTargetEndTimeForEpoch( config: EpochTimingConfig ): UInt64 { let now = currentBlock.timestamp - let viewsToEpochEnd = nextEpoch.finalView - currentBlock.view - let estSecondsToNextEpochEnd = UFix64(viewsToNextEpochEnd) / UFix64(nextEpoch.lengthInViews) * config.duration + let viewsToEpochEnd = epoch.finalView - currentBlock.view + let estSecondsToNextEpochEnd = UFix64(viewsToEpochEnd) / UFix64(epoch.lengthInViews) * config.duration return UInt64(estSecondsToNextEpochEnd) } ``` +##### Option 1.2 ```cadence // Memorize the end time of each epoch. // Invoked when transitioning into a new epoch. @@ -71,8 +74,8 @@ pub fun memorizeEpochEndTime(currentBlock: Block, epoch: EpochMetadata) { } // Compute the switchover time based on the last memorized reference timestamp. -pub fun getTargetEndTime( - forEpoch refEpoch: EpochMetadata, +pub fun getTargetEndTimeForEpoch( + refEpoch: EpochMetadata, targetEpochCounter: UInt64, config: EpochTimingConfig ): UInt64 { @@ -131,7 +134,9 @@ pub fun getTargetEndTimeForEpoch( #### Smart Contract -- Add `targetEndTime` field to `EpochSetup` event, `EpochMetadata` +- Add `targetEndTime` field to `EpochSetup` event, `EpochStart` event + - CAUTION: This must be added as the last field to maintain backward-compatibility + - NOTE: Unfortunately, since we cannot modify existing structs, we cannot add this field to `EpochMetadata` - Add config for determining `targetEndTime` to smart contract `ConfigMetadata` - Add logic to compute `targetEndTime` to `startEpochSetup` - Add function for service account to adjust new config