Skip to content
Closed
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
56 changes: 45 additions & 11 deletions specs/gloas/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
- [New `compute_balance_weighted_selection`](#new-compute_balance_weighted_selection)
- [New `compute_balance_weighted_acceptance`](#new-compute_balance_weighted_acceptance)
- [Modified `compute_proposer_indices`](#modified-compute_proposer_indices)
- [New `compute_ptc`](#new-compute_ptc)
- [Beacon state accessors](#beacon-state-accessors)
- [Modified `get_next_sync_committee_indices`](#modified-get_next_sync_committee_indices)
- [Modified `get_attestation_participation_flag_indices`](#modified-get_attestation_participation_flag_indices)
Expand All @@ -63,6 +64,7 @@
- [Beacon state mutators](#beacon-state-mutators)
- [New `initiate_builder_exit`](#new-initiate_builder_exit)
- [Beacon chain state transition function](#beacon-chain-state-transition-function)
- [Modified `process_slots`](#modified-process_slots)
- [Modified `process_slot`](#modified-process_slot)
- [Epoch processing](#epoch-processing)
- [Modified `process_epoch`](#modified-process_epoch)
Expand Down Expand Up @@ -384,6 +386,10 @@ class BeaconState(Container):
latest_block_hash: Hash32
# [New in Gloas:EIP7732]
payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]
# [New in Gloas:EIP7732]
previous_ptc: Vector[ValidatorIndex, PTC_SIZE]
# [New in Gloas:EIP7732]
current_ptc: Vector[ValidatorIndex, PTC_SIZE]
```

## Dataclasses
Expand Down Expand Up @@ -630,6 +636,26 @@ def compute_proposer_indices(
]
```

#### New `compute_ptc`

```python
def compute_ptc(state: BeaconState) -> Vector[ValidatorIndex, PTC_SIZE]:
"""
Get the payload timeliness committee for the current slot.
"""
epoch = get_current_epoch(state)
seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(state.slot))
indices: List[ValidatorIndex] = []
# Concatenate all committees for this slot in order
committees_per_slot = get_committee_count_per_slot(state, epoch)
for i in range(committees_per_slot):
committee = get_beacon_committee(state, state.slot, CommitteeIndex(i))
indices.extend(committee)
return compute_balance_weighted_selection(
state, indices, seed, size=PTC_SIZE, shuffle_indices=False
)
```

### Beacon state accessors

#### Modified `get_next_sync_committee_indices`
Expand Down Expand Up @@ -710,17 +736,8 @@ def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:
"""
Get the payload timeliness committee for the given ``slot``.
"""
epoch = compute_epoch_at_slot(slot)
seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(slot))
indices: List[ValidatorIndex] = []
# Concatenate all committees for this slot in order
committees_per_slot = get_committee_count_per_slot(state, epoch)
for i in range(committees_per_slot):
committee = get_beacon_committee(state, slot, CommitteeIndex(i))
indices.extend(committee)
return compute_balance_weighted_selection(
state, indices, seed, size=PTC_SIZE, shuffle_indices=False
)
assert slot == state.slot or slot + 1 == state.slot
Comment thread
jtraglia marked this conversation as resolved.

@ensi321 ensi321 Mar 16, 2026

Copy link
Copy Markdown
Contributor

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.

return state.current_ptc if slot == state.slot else state.previous_ptc
```

#### New `get_indexed_payload_attestation`
Expand Down Expand Up @@ -793,6 +810,23 @@ transitions that trigger an unhandled exception (e.g. a failed `assert` or an
out-of-range list access) are considered invalid. State transitions that cause
an `uint64` overflow or underflow are also considered invalid.

### Modified `process_slots`

```python
def process_slots(state: BeaconState, slot: Slot) -> None:
assert state.slot < slot
while state.slot < slot:
process_slot(state)
# Process epoch on the start slot of the next epoch
if (state.slot + 1) % SLOTS_PER_EPOCH == 0:
process_epoch(state)
state.slot = Slot(state.slot + 1)
# [New in Gloas:EIP7732]
state.previous_ptc = state.current_ptc
# [New in Gloas:EIP7732]
state.current_ptc = compute_ptc(state)
```

### Modified `process_slot`

```python
Expand Down
2 changes: 1 addition & 1 deletion specs/gloas/fork-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,11 +854,11 @@ def on_payload_attestation_message(
data = ptc_message.data
# PTC attestation must be for a known block. If block is unknown, delay consideration until the block is found
state = store.block_states[data.beacon_block_root]
ptc = get_ptc(state, data.slot)
# PTC votes can only change the vote for their assigned beacon block, return early otherwise
if data.slot != state.slot:
return
# Check that the attester is from the PTC
ptc = get_ptc(state, data.slot)
assert ptc_message.validator_index in ptc

# Verify the signature and check that its for the current slot if it is coming from the wire
Expand Down
6 changes: 6 additions & 0 deletions specs/gloas/fork.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,16 @@ def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState:
latest_block_hash=pre.latest_execution_payload_header.block_hash,
# [New in Gloas:EIP7732]
payload_expected_withdrawals=[],
# [New in Gloas:EIP7732]
previous_ptc=[ValidatorIndex(0)] * PTC_SIZE,
# [New in Gloas:EIP7732]
current_ptc=[ValidatorIndex(0)] * PTC_SIZE,
)

# [New in Gloas:EIP7732]
onboard_builders_from_pending_deposits(post)
# [New in Gloas:EIP7732]
post.current_ptc = compute_ptc(post)

return post
```
26 changes: 3 additions & 23 deletions specs/gloas/validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,29 +48,9 @@ validator" to implement Gloas.
### Payload timeliness committee

A validator may be a member of the new Payload Timeliness Committee (PTC) for a
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.


```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.

state: BeaconState, epoch: Epoch, validator_index: ValidatorIndex
) -> Optional[Slot]:
"""
Returns the slot during the requested epoch in which the validator with
index ``validator_index`` is a member of the PTC. Returns None if no
assignment is found.
"""
next_epoch = Epoch(get_current_epoch(state) + 1)
assert epoch <= next_epoch

start_slot = compute_start_slot_at_epoch(epoch)
for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH):
if validator_index in get_ptc(state, Slot(slot)):
return Slot(slot)
return None
```
given slot. Validators can check if their validator index is in the PTC for the
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.


Expand Down
2 changes: 2 additions & 0 deletions specs/heze/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ class BeaconState(Container):
builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT]
latest_block_hash: Hash32
payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]
previous_ptc: Vector[ValidatorIndex, PTC_SIZE]
current_ptc: Vector[ValidatorIndex, PTC_SIZE]
```

## Helpers
Expand Down
2 changes: 2 additions & 0 deletions specs/heze/fork.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ def upgrade_to_heze(pre: gloas.BeaconState) -> BeaconState:
builder_pending_withdrawals=pre.builder_pending_withdrawals,
latest_block_hash=pre.latest_block_hash,
payload_expected_withdrawals=pre.payload_expected_withdrawals,
previous_ptc=pre.previous_ptc,
current_ptc=pre.current_ptc,
)

return post
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,12 @@ def test_process_payload_attestation_too_old_slot(spec, state):
"""
Test payload attestation for slot too far in the past fails
"""
# Advance state to slot 3
spec.process_slots(state, state.slot + 3)
# Advance to slot 1 and create a valid attestation for slot 0
spec.process_slots(state, state.slot + 1)
payload_attestation = prepare_signed_payload_attestation(spec, state)

# Try to attest to slot 0 (2 slots ago, should be 1 slot ago)
payload_attestation = prepare_signed_payload_attestation(spec, state, slot=state.slot - 2)
# Advance again so the attestation becomes too old (slot 0 is now 2 slots behind)
spec.process_slots(state, state.slot + 1)

yield from run_payload_attestation_processing(spec, state, payload_attestation, valid=False)

Expand Down Expand Up @@ -375,6 +376,8 @@ def test_process_payload_attestation_sampling_not_capped(spec, state):
low_balance = spec.EFFECTIVE_BALANCE_INCREMENT
for validator in state.validators:
validator.effective_balance = low_balance
# Direct balance mutations bypass slot processing, so refresh the cached current-slot PTC.
state.current_ptc = spec.compute_ptc(state)

chosen_slot = None
chosen_index = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,58 @@ def test_execution_payload_availability_reset_from_unset(spec, state):
yield "post", state

assert state.execution_payload_availability[next_slot_index] == 0b0


@with_gloas_and_later
@spec_state_test
def test_ptc_rotates_on_slot_advance(spec, state):
"""
Test that process_slots correctly rotates previous_ptc/current_ptc:
old current becomes previous, new current is freshly computed.
"""
old_current_ptc = list(state.current_ptc)

yield "pre", state
yield "slots", 1

spec.process_slots(state, state.slot + 1)

yield "post", state

new_current_ptc = list(state.current_ptc)
# Sanity: the two PTCs should differ, so the rotation test is meaningful
assert old_current_ptc != new_current_ptc
# After advancing, old current should become previous
assert list(state.previous_ptc) == old_current_ptc
# And new current should be freshly computed for the new slot
assert new_current_ptc == list(spec.compute_ptc(state))


@with_gloas_and_later
@spec_state_test
def test_ptc_rotates_across_epoch_boundary(spec, state):
"""
Test that previous_ptc/current_ptc correctly rotate when crossing an epoch boundary.
"""
# Advance to the last slot of the epoch
target_slot = spec.SLOTS_PER_EPOCH - 1
if state.slot < target_slot:
spec.process_slots(state, target_slot)

old_current_ptc = list(state.current_ptc)

yield "pre", state
yield "slots", 1

# Cross the epoch boundary
spec.process_slots(state, state.slot + 1)

yield "post", state

new_current_ptc = list(state.current_ptc)
# Sanity: the two PTCs should differ, so the rotation test is meaningful
assert old_current_ptc != new_current_ptc
# Old current should become previous
assert list(state.previous_ptc) == old_current_ptc
# New current should be computed for the first slot of the new epoch
assert new_current_ptc == list(spec.compute_ptc(state))
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def create_genesis_state(spec, validator_balances, activation_threshold):
spec.BuilderPendingPayment() for _ in range(2 * spec.SLOTS_PER_EPOCH)
]
state.builder_pending_withdrawals = []
state.current_ptc = spec.compute_ptc(state)

if is_post_fulu(spec):
# Initialize proposer lookahead list
Expand Down