-
Notifications
You must be signed in to change notification settings - Fork 32
ACP-181 ProposerVM Epochs #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cam-schultz
wants to merge
6
commits into
avalanche-foundation:main
Choose a base branch
from
cam-schultz:acp-181
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
3b790e4
acp-181 proposervm epochs
cam-schultz ea7c453
address typos
cam-schultz d436e21
update epoch definition; illustrate edge cases
cam-schultz 44bdb20
clarify
cam-schultz 522d45a
pseudocode implementation
cam-schultz dd36261
clarify definitions
cam-schultz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
| ACP | 181 | | ||
| :--- | :--- | | ||
| **Title** | ProposerVM Epochs | | ||
| **Author(s)** | Cam Schultz [@cam-schultz](https://github.com/cam-schultz) | | ||
| **Status** | Proposed, Implementable, Activated, Stale ([Discussion](POPULATED BY MAINTAINER, DO NOT SET)) | | ||
| **Track** | Standards | | ||
|
||
## Abstract | ||
|
||
Proposes the introduction of epochs to the ProposerVM, such that a consistent view of the P-Chain may be provided to inner VMs for a known duration of time. This would enable VMs to optimize validator set retrievals, which currently must be done as often as every P-Chain block. | ||
|
||
## Motivation | ||
|
||
The P-Chain maintains a registry of L1 and Subnet validators (including Primary Network validators). Validators are added, removed, or their weights changed by issuing P-Chain transactions that are included in P-Chain blocks. When describing an L1 or Subnet's validator set, what is really being described are the weights, BLS keys, and Node IDs of the active validators at a particular P-Chain height. Use cases that require on-demand views of L1 or Subnet validator sets need to fetch validator sets at arbitrary P-Chain heights, while use cases that require up-to-date views need to fetch them as often as every P-Chain block. | ||
|
||
ProposerVM epochs during which the P-Chain height is fixed would widen this window to a predictable epoch duration, allowing these use cases to implement optimizations such as pre-fetching validator sets once per epoch, or allowing more efficient backwards traversal of the P-Chain to fetch historical validator sets. | ||
|
||
## Specification | ||
|
||
### Epoch Definition | ||
|
||
An epoch $E_n$ is defined by its start time $T_{start}^n$, inclusive and its end time $T_{end}^n$, exclusive. | ||
|
||
$$ | ||
E_n \coloneqq [ T_{start}^n,T_{end}^n ) | ||
$$ | ||
|
||
### Epoch Duration | ||
|
||
Epochs have a constant duration $D$, such that for any given epoch $E_n$, the difference between its start and end times equals this duration: | ||
|
||
$$ | ||
T_{end}^n - T_{start}^n = D | ||
$$ | ||
|
||
$D$ is hardcoded into the ProposerVM source code, and may only be changed by a required network upgrade. | ||
|
||
#### Changing the Epoch Duration | ||
|
||
Future network upgrades may change the value of $D$ to some new duration $D'$. $D'$ should not take effect until the end of the current epoch, rather than the activation time of the network upgrade that defines $D'$. This ensures an in progress epoch at the upgrade activation time cannot have a realized duration less than both $D$ and $D'$. | ||
|
||
### Epoch Number | ||
|
||
Each epoch is associated with a monotonically increasing number, with $E_0$ being the epoch beginning at the activation time of the network upgrade that activates this ACP, and subsequent epochs incrementing the epoch number. Note that validators do not need to agree on epoch numbers, only the transition points between epochs, so no numbering scheme is provided in this ACP. | ||
|
||
### Sealing | ||
|
||
An epoch $E_n$ with end time $T_{end}^n$ is *sealed* by the first block that reaches the epoch boundary. For example, a block $b_m$ with timestamp $t_m$ seals $E_n$ if it is the first block for which the following is true: | ||
|
||
$$ | ||
t_{m-1} < T_{end}^n <= t_m | ||
$$ | ||
|
||
The sealing block is defined to be a member of the epoch it seals. In this example, $b_m \in E_n$. | ||
|
||
### P-Chain Height | ||
|
||
As discussed above, the main [motivation](#motivation) for introducing epochs to the ProposerVM is to provide a fixed view of the P-Chain for the duration of the epoch. To achieve this, ProposerVM blocks are extended to include the current epoch's fixed P-Chain height, `PChainEpochHeight`, in addition to the latest P-Chain height, `PChainHeight`. The validator set at `PChainEpochHeight` should be used by the ProposerVM and the inner VM for the duration of the epoch. The first block of a new epoch will set the epoch's `PChainEpochHeight` to its parent's `PChainHeight`. | ||
|
||
This approach ensures that at any given height, the validator set to be used for the next block is known (this is a basic requirement for light clients). Put another way, within an epoch, the next block will use the current block's `PChainEpochHeight`, and at the boundary of the next epoch, the next block will use the current block's `PChainHeight`. | ||
|
||
### Low Traffic Chain Edge Cases | ||
|
||
The epoch sealing [definition](#epoch-definition) produces a couple of interesting edge cases worth considering. In each of the below diagrams, **bold** block numbers indicate sealing blocks, and the rectangles denote epoch membership. | ||
|
||
1. What happens if there is only a single block with a timestamp in an epoch's range? | ||
|
||
Let $b_m$ be the only block with a timestamp in $E_{n+1}$'s range, meaning that $b_m$ seals $E_n$. $b_{m+1}$ must therefore seal $E_{n+1}$, meaning that it would be the only block in $E_{n+1}$, even though its timestamp does not fall with $E_{n+1}$'s range. | ||
|
||
<p align="center"> | ||
<img src=./edge_case_1.png /> | ||
</p> | ||
|
||
2. What happens if there are no blocks with a timestamp in an epoch's range? | ||
|
||
The next block that is produced will seal the epoch that its parent belonged to, even if there are entire epoch(s) that have since elapsed. This can result in a scenario in which the the sealing block's `PChainEpochHeight` is behind its `PChainHeight` by an arbitrary amount. | ||
|
||
<p align="center"> | ||
<img src=./edge_case_2.png /> | ||
</p> | ||
|
||
## Backwards Compatibility | ||
|
||
This change requires a network upgrade and is therefore not backwards compatible. | ||
|
||
## Reference Implementation | ||
|
||
The following pseudocode illustrates how the specified epoch definition may be used to select a block's `PChainEpochHeight`: | ||
|
||
```go | ||
type Block interface { | ||
Timestamp() time.Time | ||
PChainHeight() uint64 | ||
PChainEpochHeight() uint64 | ||
} | ||
|
||
// Returns the epoch number that the provided timestamp maps into. | ||
func GetEpoch(timestamp time.Time) uint64 | ||
|
||
// [grandParent] is [parent]'s parent | ||
func GetPChainEpochHeight(parent, grandParent Block) uint64 { | ||
if GetEpoch(parent.Timestamp()) != GetEpoch(grandParent.Timestamp()) { | ||
// If the parent crossed the epoch boundary, then it sealed the previous epoch. The child | ||
// is the first block of the new epoch, so should use the parent's P-Chain height. | ||
return parent.PChainHeight() | ||
} | ||
// Otherwise, the parent did not seal the previous epoch, so the child should use the same | ||
// epoch height. This is true even if the child crosses the epoch boundary, since sealing | ||
// blocks are considered to be part of the epoch they seal. | ||
return parent.PChainEpochHeight() | ||
} | ||
``` | ||
|
||
- `GetEpoch(timestamp time.Tim)` divides the time axis into intervals of length $D$, and returns the [epoch number](#epoch-number) of the interval that `timestamp` falls within. | ||
- The comparison | ||
|
||
```go | ||
if GetEpoch(parent.Timestamp()) != GetEpoch(grandParent.Timestamp()) | ||
``` | ||
|
||
checks if the block's parent [sealed](#epoch-definition) its epoch. | ||
- If the parent sealed its epoch, the current block advances the epoch, [refreshing the epoch height](#p-chain-height). | ||
- Otherwise, the current block uses the current epoch height, regardless of whether it seals the epoch. | ||
|
||
A full reference implementation is available in [AvalancheGo](https://github.com/ava-labs/avalanchego/pull/3746), and must be merged before this ACP may be considered `Implementable`. | ||
|
||
## Security Considerations | ||
|
||
### Excessive Validator Churn | ||
|
||
The introduction of epochs concentrates validator set changes over the epoch's duration into a single block at the epoch's boundary. Excessive validator churn can cause consensus failures and other dangerous behavior, so it is imperative that the amount of validator weight change at the epoch boundary is limited. One strategy to accomplish this is to queue validator set changes and spread them out over multiple epochs. Another strategy is to batch updates to the same validator together such that increases and decreases to that validator's weight cancel each other out. Mechanisms to mitigate against this are outside the scope of this ACP and left to validator managers to implement. | ||
|
||
## Open Questions | ||
|
||
- What should the epoch duration $D$ be set to? | ||
|
||
- Should the epoch numbering scheme be defined in this ACP? | ||
|
||
- Should validator churn limits be implemented for the primary network to mitigate against [excessive validator churn](#excessive-validator-churn)? | ||
|
||
- Is it safe for `PChainEpochHeight` and `PChainHeight` to differ significantly within a block, as described [above](#low-traffic-chain-edge-cases)? | ||
|
||
## Acknowledgements | ||
|
||
Thanks to [@iansuvak](https://github.com/iansuvak), [@geoff-vball](https://github.com/geoff-vball), [@yacovm](https://github.com/yacovm), [@michaelkaplan13](https://github.com/michaelkaplan13), [@StephenButtolph](https://github.com/StephenButtolph), and [@aaronbuchwald](https://github.com/aaronbuchwald) for discussion and feedback on this ACP. | ||
|
||
## Copyright | ||
|
||
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if, instead of having a set epoch duration, where$T_{end}^n = T_{start}^{n+1}$ , we used the timestamp of the block that sealed the previous epoch as the start time of the next epoch? This would eliminate
edge case 2
.You would lose being able to keep track of the numbering of the epochs, but I don't think that's important anyway. In this case we should always be able to check the timestamp of the start of the epoch, because we already have to know
PChainEpochHeight
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's a long gap between the epoch's end time and the block that seals it, you'd still have the same situation of
PChainEpochHeight
andPChainHeight
skew.Predictable epochs make implementing the current implementation very straightforward. I initially decided against including pseudocode of this in the ACP itself, but think it would go a long way to explaining why the definition is what it is. I'll add that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure it would eliminate the skew and actually simplify the current implementation. I'll write out an example...
D = 100
Epoch 0
Tstart = 0
Tend = 100
PChainEpochHeight = 0
Block 1 time = 0
Block 2 time = 57
Block 3 time = 99
Block 4 time = 103 SEALS EPOCH 0
Epoch 1
Tstart = 103
Tend = 203
PChainEpochHeight = 4
Block 5 time = 107
Block 6 time = 202
Block 7 time = 215 SEALS EPOCH 1
Epoch 2
Tstart = 215
Tend = 315
PChainEpochHeight = 7
Block 8 time = 400 SEALS EPOCH 2
Epoch 3
Tstart = 400
Tend = 500
PChainEpochHeight = 8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All we need to check is if the timestamp of the parent is greater than
D + timestamp of the PChainEpochHeight of the parent
. If so, we advance the epoch.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The edge case occurs if there's a long gap between the last block within the epoch bounds and the block that seals the epoch. Modifying your example:
Epoch 0 Tstart = 0 Tend = 100 PChainEpochHeight = 0
Block 1 time = 0
Block 2 time = 57
Block 3 time = 99
Block 4 time = 500 SEALS EPOCH 0
Block 4 will use the same
PChainEpochHeight
as block 1, even though many epoch durations have elapsed.