Skip to content

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
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions ACPs/181-proposervm-epochs/README.md
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.
Comment on lines +30 to +36
Copy link
Contributor

@geoff-vball geoff-vball Feb 28, 2025

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.

Copy link
Contributor Author

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 and PChainHeight 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.

Copy link
Contributor

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

Copy link
Contributor

@geoff-vball geoff-vball Feb 28, 2025

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.

Copy link
Contributor Author

@cam-schultz cam-schultz Feb 28, 2025

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.


#### 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/).
Binary file added ACPs/181-proposervm-epochs/edge_case_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ACPs/181-proposervm-epochs/edge_case_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.