Skip to content

Add cached PTCs to the state#4992

Closed
potuz wants to merge 8 commits intoethereum:masterfrom
potuz:gloas/ptc_lookbehind
Closed

Add cached PTCs to the state#4992
potuz wants to merge 8 commits intoethereum:masterfrom
potuz:gloas/ptc_lookbehind

Conversation

@potuz
Copy link
Copy Markdown
Contributor

@potuz potuz commented Mar 9, 2026

Alternative to #4979 that only keeps track of 2 committees (8KB per state)

# [New in Gloas:EIP7732]
payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]
# [New in Gloas:EIP7732]
ptc_lookbehind: Vector[Vector[ValidatorIndex, PTC_SIZE], 2]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Could be previous_ptc, current_ptc which is ugly as well, but more like previous forks structures.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've just made this change. I prefer this.

process_epoch(state)
state.slot = Slot(state.slot + 1)
# [New in Gloas:EIP7732]
state.ptc_lookbehind = [state.ptc_lookbehind[1], compute_ptc(state)]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Could be moved to process_slot but it's uglier.

@github-actions github-actions bot added the gloas label Mar 9, 2026
@ensi321
Copy link
Copy Markdown
Contributor

ensi321 commented Mar 9, 2026

Personally prefer this over #4979 . It's a balance between state size and spec simplicity.

# [New in Gloas:EIP7732]
onboard_builders_from_pending_deposits(post)
# [New in Gloas:EIP7732]
ptc_lookbehind[1] = compute_ptc(post)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added outside because compute_ptc should only make sense on Gloas states.

given slot. To check for PTC assignments, use
`get_ptc_assignment(state, epoch, validator_index)` where `epoch <= next_epoch`,
as PTC committee selection is only stable within the context of the current and
next epoch.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This sentence was factually wrong which is what started this issue. Removed the helper entirely since there is no need to specify it.

@potuz
Copy link
Copy Markdown
Contributor Author

potuz commented Mar 10, 2026

Personally prefer this over #4979 . It's a balance between state size and spec simplicity.

I wouldn't be so certain, the PR that adds the full lookahead has a single point where this computation is carried and no synchronization with process slots that happens at time on wall clock. The total data added per state is 0.08% (that's 0.0008) of a current beacon state in mainnet.

@github-actions github-actions bot added testing CI, actions, tests, testing infra heze labels Mar 11, 2026
@jtraglia jtraglia changed the title Add PTC Lookbehind Add cached PTCs to the state Mar 12, 2026
next epoch.

```python
def get_ptc_assignment(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Leaving a note that get_ptc_assignment is still being used in three sections below.

current slot by checking if their validator index is in `get_ptc(state)`. PTC
committee selection is only stable within the context of the current epoch.

### Lookahead
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't seem to hold anymore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

why not?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Right above it says:

PTC committee selection is only stable within the context of the current epoch.

which means there is no 1 epoch lookahead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The committee selection is stable within the context of the current epoch, it doesn't mean that the epoch needs to be cached in the state. It means that any validator that wants to check if it has PTC duties in the current epoch, can do so at the beginning of the epoch. Clients will most likely implement this and cache it no matter which lookahead/lookbehind system we implement in-state.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Lookahead section states that PTC is stable for next epoch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

which section? I'm replying only to your comment that is based on the sentence:

current slot by checking if their validator index is in `get_ptc(state)`. PTC
committee selection is only stable within the context of the current epoch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Lookahead section states that PTC is stable for next epoch.

Lookahead section. I left this comment on Lookahead section.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, yeah what Github shows is confusing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

L77-L78:

get_ptc_assignment should be called at the start of each epoch to get the
assignment for the next epoch (current_epoch + 1). A validator should plan for

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

oh yeah that's definitely wrong.

return compute_balance_weighted_selection(
state, indices, seed, size=PTC_SIZE, shuffle_indices=False
)
assert slot == state.slot or slot + 1 == state.slot
Copy link
Copy Markdown
Contributor

@ensi321 ensi321 Mar 16, 2026

Choose a reason for hiding this comment

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

I think this change makes get_ptc to be too restrictive on the range of slot it accepts. Validator's PTC assignment should still be determined ahead on a per-epoch basis instead of per-slot basis.

I think we should accept slot [previous slot, end slot of state's current epoch]. Maybe something like

def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:                                                     
    epoch = get_current_epoch(state)                                                                                                 
    epoch_start_slot = compute_start_slot_at_epoch(epoch)
    epoch_end_slot = epoch_state_slot + SLOTS_PER_EPOCH                                                      
    assert slot >= state.slot - 1 and slot < epoch_end_slot
    if slot == state.slot:                                                                                                           
        return state.current_ptc                                                                                                     
    if slot + 1 == state.slot:                                                                                                       
        return state.previous_ptc                                                                                                                                                                  
    return compute_ptc(state, slot)                              

So get_ptc_assignment can still take advantage of this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

clients are free to simply pass the slot to conpute_ptc for caching purposes. Why should we have these off-protocol elements leaking in the spec?

OTOH one think that should be considered IMO is that if clients will anyway cache them, then most likely having the full cache in the spec is more efficient than keeping it in an ad-hoc in-memory cache that needs to be in-sync with the head state.

@sauliusgrigaitis
Copy link
Copy Markdown
Contributor

Grandine has a similar cache implemented already, so we would need to remove our cache implementation and switch to this. But my question is why do we need such a cache in the state (and specification)? Clients have a lot of other caches that are part of the client (not specification). So why is this time different?

@potuz
Copy link
Copy Markdown
Contributor Author

potuz commented Mar 17, 2026

Grandine has a similar cache implemented already, so we would need to remove our cache implementation and switch to this. But my question is why do we need such a cache in the state (and specification)? Clients have a lot of other caches that are part of the client (not specification). So why is this time different?

Because the state is flawed currently: PTC attestations are cast in slot 31, validation of gossip uses get_ptc for that state at that slot. Then those attestations are included in the block at slot 32, and validation of the block uses get_ptc for the state at slot 32 which can return a different ptc than the previous slot's

@nflaig
Copy link
Copy Markdown
Member

nflaig commented Mar 17, 2026

Grandine has a similar cache implemented already, so we would need to remove our cache implementation and switch to this. But my question is why do we need such a cache in the state (and specification)? Clients have a lot of other caches that are part of the client (not specification). So why is this time different?

you can handle that with separate caches in the client but one problem is that if you load a state you can't compute the previous PTC as you'd need the effective balances of the previous epoch, having the previous_ptc as part of the state makes this a lot simpler

lodekeeper added a commit to lodekeeper/consensus-specs that referenced this pull request Mar 18, 2026
Alternative to ethereum#4992 that addresses ethereum#4979's epoch-boundary PTC bug
with minimal state changes:

- Add single `previous_ptc` field to BeaconState (~4KB vs 8KB/ethereum#4992, 256KB/ethereum#4979)
- New `compute_ptc(state, slot)` extracted from `get_ptc` for on-demand computation
- New `process_ptc_update` at START of `process_epoch` (before balance updates)
- Modified `get_ptc` returns cached `previous_ptc` for epoch boundary case
- No per-slot rotation in `process_slots` (unlike ethereum#4992)
- Preserves `get_ptc_assignment` validator duties API (unlike ethereum#4992)
- Tightened `get_ptc_assignment` assert to `current_epoch <= epoch <= next_epoch`

Key insight: only the PTC for the last slot of the previous epoch needs
caching because `process_payload_attestation` enforces
`data.slot + 1 == state.slot` and effective balances only change at
epoch boundaries.

Co-authored-by: Nico Flaig <nflaig@users.noreply.github.com>
lodekeeper added a commit to lodekeeper/consensus-specs that referenced this pull request Mar 18, 2026
Alternative to ethereum#4992 that addresses ethereum#4979's epoch-boundary PTC bug
with minimal state changes:

- Add single `previous_ptc` field to BeaconState (~4KB vs 8KB/ethereum#4992, 256KB/ethereum#4979)
- New `compute_ptc(state, slot)` extracted from `get_ptc` for on-demand computation
- New `process_ptc_update` at START of `process_epoch` (before balance updates)
- Modified `get_ptc` returns cached `previous_ptc` for epoch boundary case
- No per-slot rotation in `process_slots` (unlike ethereum#4992)
- Preserves `get_ptc_assignment` validator duties API (unlike ethereum#4992)
- Tightened `get_ptc_assignment` assert to `current_epoch <= epoch <= next_epoch`

Key insight: only the PTC for the last slot of the previous epoch needs
caching because `process_payload_attestation` enforces
`data.slot + 1 == state.slot` and effective balances only change at
epoch boundaries.

Co-authored-by: Nico Flaig <nflaig@users.noreply.github.com>
@jihoonsong
Copy link
Copy Markdown
Member

Closing in lieu of #4979.

@jihoonsong jihoonsong closed this Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gloas heze testing CI, actions, tests, testing infra

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants