Skip to content

ACP-181 P-Chain Epoched Views #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 12 commits into
base: main
Choose a base branch
from

Conversation

cam-schultz
Copy link
Contributor

@cam-schultz cam-schultz commented Feb 8, 2025

Proposes a standard P-Chain epoching scheme to provide more stable views of the P-Chain state VMs

Rendered

Comment on lines 32 to 38
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.
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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I'm way late to this thread, but just had the same thought today as Geoff did back in Feb.

I don't think there are any edge cases with the approach Geoff proposed that don't also exist in the specification as it currently is, right? In the example you provided:

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 always must have the same PChainEpochHeight as block 1 either way, because there always needs to be a block that seals an epoch. The only difference is that the time duration D for epoch 2 would only begin at time = 500, rather than whatever the last fixed interval time going back to the network upgrade activation time is.

For example, say we D=10, and have:

Epoch 0 Tstart = 0
Block 1 time = 0
Block 2 time = 4
Block 3 time = 9
Block 4 time = 15

Block 4 seals epoch 0, but in the design as currently written, if the Block 5 comes in at time = 21, it would seal epoch 1 despite it's PChainEpochHeight only being 6 seconds old, since it is the "current" P-Chain height of Block 4.

I think having the "end"/threshold time for epoch N being (timestamp of block that sealed epoch N-1) + D makes it more consistent such that the PChainEpochHeight is updated at most every D seconds since it last changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having the "end"/threshold time for epoch N being (timestamp of block that sealed epoch N-1) + D makes it more consistent such that the PChainEpochHeight is updated at most every D seconds since it last changed.

I updated the spec with this change. I removed the "low traffic edge cases" section altogether, since the 0-block edge case is no longer applicable, and the single-block edge case is not particularly interesting anymore. Epochs may still be of indeterminite length, and that is still called out as an open question.

}
```

- `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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `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.
- `GetEpoch(timestamp time.Time)` divides the time axis into intervals of length $D$, and returns the [epoch number](#epoch-number) of the interval that `timestamp` falls within.

@cam-schultz cam-schultz changed the title ACP-181 ProposerVM Epochs ACP-181 P-Chain Epoched Views May 6, 2025

## Abstract

Proposes a standard P-Chain epoching scheme such that any VM that implements it has consistent view of the P-Chain 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. This standard does *not* introduce epochs to the P-Chain's VM directly. Instead, it provides a standard that may be implemented by layers that inject P-Chain state into VMs, such as the ProposerVM.
Copy link
Contributor

@michaelkaplan13 michaelkaplan13 May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Proposes a standard P-Chain epoching scheme such that any VM that implements it has consistent view of the P-Chain 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. This standard does *not* introduce epochs to the P-Chain's VM directly. Instead, it provides a standard that may be implemented by layers that inject P-Chain state into VMs, such as the ProposerVM.
Proposes a standard P-Chain epoching scheme such that any VM that implements it uses a P-Chain block height known prior to the generation of its next block. This would enable VMs to optimize validator set retrievals, which currently must be done during block execution. This standard does *not* introduce epochs to the P-Chain's VM directly. Instead, it provides a standard that may be implemented by layers that inject P-Chain state into VMs, such as the ProposerVM.

I think this is more accurate wording, since epochs don't actually have a known duration.

Comment on lines 22 to 34
Let $T_{start}^0$ be the activation time of the network upgrade that activates this ACP. The time axis is divided into _epochs_ of duration $D$ such that an epoch $E_n$ is defined by its start and end times:

$$
E_n \coloneqq [ T_{start}^n,T_{end}^n )
$$

and the difference between $T_{end}^n$ and $T_{start}^n$ is $D$ for any $n$:

$$
T_{end}^n - T_{start}^n = D
$$

### Mapping Blocks to Epochs
Copy link
Contributor

@michaelkaplan13 michaelkaplan13 May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to define epochs here by the blocks within them, rather than starting off with a definition based on time? I think we should avoid using terms like "start time" and "end time" here because the the actual start/end time of a given epoch is defined by the last block in the epoch before it and the final block included in it.

Thinking out loud, but we could do something like "An epoch $E_{N}$ is defined by the one or more blocks that have $N$ as their epoch number. A block's epoch number is defined according to the following:

  • Epoch boundary time definition
  • Block to epoch number mapping
    "

Copy link
Contributor

@michaelkaplan13 michaelkaplan13 May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking more about this...

If an epoch N is defined by the blocks contained within it, should the "start time" of the epoch be the global time threshold that was crossed prior to the production of the block that sealed epoch N-1, or should it be the block timestamp of that block?

I'm imagine a rough recursive-like specification of something like:

  • An epoch $E_N$ is defined by the one or more blocks that it contains.
  • The first block contained in $E_N$ is the block immediately following the block that sealed epoch $E_{N-1}$. Call this sealing block $B_{S_{N-1}}$.
  • The last block in $E_N$ is the block that seals $E_N$, called $B_{S_N}$, and is the defined as the first block with a timestamp of >= $T_{B_{S_{N-1}}} + D$
  • The PChainHeight of each block is set by the block builder, and validated according to the same validation rules the ProsposerVM currently uses for the existing PChainHeight field.
  • If a blocks parent sealed an epoch, then its PChainEpochHeight must be set to its parent's PChainHeight. Otherwise, a blocks PChainEpochHeight must be the same as its parent's PChainEpochHeight.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this a bit here. I think I'm still generally in favour of the recursive definition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One nice property using the sealing block's timestamp as the epoch start time is that it guarantees the epoch duration will be at least $D$. That would improve the reliability of use cases such as ICM verification that require multiple network roundtrips to construct and submit the signed message.

The downside is that epoch transitions would not be coordinated across L1s, which in the current specification they would be. I don't know if this affects any of the targeted use cases, but wanted to call it out as a difference.

Comment on lines +106 to +107
- If the parent sealed its epoch, the current block [advances the epoch](#advancing-an-epoch), refreshing the epoch height, incrementing the epoch number, and setting the epoch starting time.
- Otherwise, the current block uses the current epoch height, number, and starting time, regardless of whether it seals the epoch.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be easier to reason about and implement this if we define the epoch starting time as the timestamp of the block that seals the previous epoch. Two reasons:

  1. This makes the minimum number of blocks per epoch to be 1. With the current approach, the minimum number of blocks per epoch is 2 because the first block sets the starting timestamp, which by definition cannot seal the epoch.
  2. The parameters of the epoch would be completely defined by the block that seals the previous epoch - The P-Chain height, starting timestamp, and epoch number can all be fetched by looking at one block header. Otherwise we have to check two separate block headers to get all the epoch information.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only needing to reference the parent block is a nice property. There was some awkwardness around ordering in the reference implementation using the child's timestamp as the epoch start time. This would alleviate that.

The motivation to use the child's timestamp stemmed from having a predictable epoch length from a starting point denoted by a block acceptance, such that an epoch's duration could be reliably predicted by monitoring on-chain activity. However, the sealing block of the previous epoch gives the same property, with the only difference being that the epoch's duration depends on a block not in the epoch. I am not aware of any use cases for which this distinction is important.

In short, I agree with your comments and will make the change. Thanks for the suggestion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants