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
60 changes: 50 additions & 10 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 @@ -66,6 +67,7 @@
- [Modified `process_slot`](#modified-process_slot)
- [Epoch processing](#epoch-processing)
- [Modified `process_epoch`](#modified-process_epoch)
- [New `process_ptc_update`](#new-process_ptc_update)
- [New `process_builder_pending_payments`](#new-process_builder_pending_payments)
- [Block processing](#block-processing)
- [Withdrawals](#withdrawals)
Expand Down Expand Up @@ -384,6 +386,8 @@ 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_epoch_last_ptc: Vector[ValidatorIndex, PTC_SIZE]
```

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

#### New `compute_ptc`

```python
def compute_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:
"""
Compute the payload timeliness committee for the given ``slot``
using the state's current effective balances.
"""
epoch = compute_epoch_at_slot(slot)
assert epoch <= get_current_epoch(state)
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
)
```

### Beacon state accessors

#### Modified `get_next_sync_committee_indices`
Expand Down Expand Up @@ -705,22 +731,23 @@ def get_attestation_participation_flag_indices(

#### New `get_ptc`

*Note*: `get_ptc` uses the cached `previous_epoch_last_ptc` for the last slot of
the previous epoch. This is sufficient because `process_payload_attestation`
requires `data.slot + 1 == state.slot`, and the only case where effective
balance updates can change the computed PTC occurs at the epoch boundary.

```python
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
)
state_epoch = get_current_epoch(state)
assert epoch + 1 >= state_epoch and epoch <= state_epoch
if epoch + 1 == state_epoch:
assert slot % SLOTS_PER_EPOCH == SLOTS_PER_EPOCH - 1
return state.previous_epoch_last_ptc
return compute_ptc(state, slot)
```

#### New `get_indexed_payload_attestation`
Expand Down Expand Up @@ -817,6 +844,8 @@ def process_slot(state: BeaconState) -> None:

```python
def process_epoch(state: BeaconState) -> None:
# [New in Gloas:EIP7732]
process_ptc_update(state)
process_justification_and_finalization(state)
process_inactivity_updates(state)
process_rewards_and_penalties(state)
Expand All @@ -836,6 +865,17 @@ def process_epoch(state: BeaconState) -> None:
process_proposer_lookahead(state)
```

#### New `process_ptc_update`

```python
def process_ptc_update(state: BeaconState) -> None:
"""
Cache the PTC for the current slot (last slot of the ending epoch)
before effective balance updates alter the weighted selection.
"""
state.previous_epoch_last_ptc = compute_ptc(state, Slot(state.slot))
```

#### New `process_builder_pending_payments`

```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
2 changes: 2 additions & 0 deletions specs/gloas/fork.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ 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_epoch_last_ptc=[ValidatorIndex(0)] * PTC_SIZE,
)

# [New in Gloas:EIP7732]
Expand Down
20 changes: 10 additions & 10 deletions specs/gloas/validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ validator" to implement Gloas.

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.
`get_ptc_assignment(state, epoch, validator_index)` where
`epoch == current_epoch`, as PTC committee selection is only stable within the
context of the current epoch.

```python
def get_ptc_assignment(
Expand All @@ -62,21 +62,20 @@ def get_ptc_assignment(
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
assert epoch == get_current_epoch(state)
Comment thread
nflaig marked this conversation as resolved.

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)):
if validator_index in compute_ptc(state, Slot(slot)):
return Slot(slot)
return None
```

### Lookahead

`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
future assignments by noting their assigned PTC slot.
assignment for the current epoch. A validator should note their assigned PTC
slot for that epoch.

## Beacon chain responsibilities

Expand Down Expand Up @@ -242,8 +241,9 @@ def prepare_execution_payload(
### Payload timeliness attestation

Some validators are selected to submit payload timeliness attestations.
Validators should call `get_ptc_assignment` at the beginning of an epoch to be
prepared to submit their PTC attestations during the next epoch.
Validators should call `get_ptc_assignment` at the beginning of the epoch to
determine the slot during the current epoch in which they should submit their
PTC attestation.

A validator should create and broadcast the `payload_attestation_message` to the
global execution attestation subnet within the first
Expand Down
1 change: 1 addition & 0 deletions specs/heze/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ 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_epoch_last_ptc: Vector[ValidatorIndex, PTC_SIZE]
```

## Helpers
Expand Down
1 change: 1 addition & 0 deletions specs/heze/fork.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ 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_epoch_last_ptc=pre.previous_epoch_last_ptc,
)

return post
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from eth_consensus_specs.test.context import (
expect_assertion_error,
single_phase,
spec_state_test,
with_phases,
)
from eth_consensus_specs.test.helpers.constants import GLOAS
from eth_consensus_specs.test.helpers.epoch_processing import run_epoch_processing_with


@with_phases([GLOAS])
@spec_state_test
@single_phase
def test_process_ptc_update_caches_last_slot_ptc(spec, state):
"""
Test that process_ptc_update caches the PTC for the current slot
(last slot of the epoch) into state.previous_epoch_last_ptc.
"""
# Advance to last slot of the epoch
spec.process_slots(state, state.slot + spec.SLOTS_PER_EPOCH - 1)

# Compute expected PTC for this slot (last slot of epoch, before balance updates)
expected_ptc = spec.compute_ptc(state, spec.Slot(state.slot))

yield from run_epoch_processing_with(spec, state, "process_ptc_update")

assert list(state.previous_epoch_last_ptc) == list(expected_ptc)


@with_phases([GLOAS])
@spec_state_test
@single_phase
def test_get_ptc_returns_cached_previous_for_epoch_boundary(spec, state):
"""
Test that after crossing an epoch boundary, get_ptc returns the cached
previous_epoch_last_ptc for the last slot of the previous epoch.
"""
# Advance to first slot of next epoch
target_slot = spec.SLOTS_PER_EPOCH
spec.process_slots(state, target_slot)

# Now state.slot = SLOTS_PER_EPOCH (first slot of epoch 1)
# Query PTC for slot SLOTS_PER_EPOCH - 1 (last slot of epoch 0)
last_slot_prev_epoch = spec.Slot(spec.SLOTS_PER_EPOCH - 1)
ptc = spec.get_ptc(state, last_slot_prev_epoch)

# Should match previous_epoch_last_ptc cached during epoch processing
assert list(ptc) == list(state.previous_epoch_last_ptc)
# Should not be all zeros (real PTC was cached)
assert any(v != 0 for v in ptc)


@with_phases([GLOAS])
@spec_state_test
@single_phase
def test_get_ptc_computes_current_epoch_on_demand(spec, state):
"""
Test that get_ptc computes current epoch PTCs on demand via compute_ptc.
"""
ptc_slot_0 = spec.get_ptc(state, spec.Slot(0))
computed_ptc_slot_0 = spec.compute_ptc(state, spec.Slot(0))

assert list(ptc_slot_0) == list(computed_ptc_slot_0)


@with_phases([GLOAS])
@spec_state_test
@single_phase
def test_compute_ptc_next_epoch_asserts(spec, state):
"""
Test that compute_ptc does not allow next-epoch computation.
"""
next_epoch_slot = spec.Slot(spec.SLOTS_PER_EPOCH)
expect_assertion_error(lambda: spec.compute_ptc(state, next_epoch_slot))


@with_phases([GLOAS])
@spec_state_test
@single_phase
def test_get_ptc_next_epoch_asserts(spec, state):
"""
Test that get_ptc does not allow next-epoch lookups.
"""
next_epoch_slot = spec.Slot(spec.SLOTS_PER_EPOCH)
expect_assertion_error(lambda: spec.get_ptc(state, next_epoch_slot))
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from eth_consensus_specs.test.context import (
expect_assertion_error,
single_phase,
spec_test,
with_phases,
with_state,
)
from eth_consensus_specs.test.helpers.constants import GLOAS
from eth_consensus_specs.test.helpers.state import next_epoch


def _compute_first_ptc_assignments(spec, state, epoch):
assignments = {}
start_slot = spec.compute_start_slot_at_epoch(epoch)
for slot in range(start_slot, start_slot + spec.SLOTS_PER_EPOCH):
for validator_index in spec.compute_ptc(state, spec.Slot(slot)):
assignments.setdefault(validator_index, spec.Slot(slot))
return assignments


def _assert_get_ptc_assignments(spec, state, epoch, assignments):
assert len(assignments) > 0

for validator_index, expected_slot in assignments.items():
assert spec.get_ptc_assignment(state, epoch, validator_index) == expected_slot

unassigned_validator = next(
(spec.ValidatorIndex(i) for i in range(len(state.validators)) if i not in assignments),
None,
)
if unassigned_validator is not None:
assert spec.get_ptc_assignment(state, epoch, unassigned_validator) is None


@with_phases([GLOAS])
@spec_test
@with_state
@single_phase
def test_get_ptc_assignment__previous_epoch(spec, state):
next_epoch(spec, state)

epoch = spec.Epoch(spec.get_current_epoch(state) - 1)
expect_assertion_error(lambda: spec.get_ptc_assignment(state, epoch, spec.ValidatorIndex(0)))


@with_phases([GLOAS])
@spec_test
@with_state
@single_phase
def test_get_ptc_assignment__current_epoch(spec, state):
epoch = spec.get_current_epoch(state)
assignments = _compute_first_ptc_assignments(spec, state, epoch)
_assert_get_ptc_assignments(spec, state, epoch, assignments)


@with_phases([GLOAS])
@spec_test
@with_state
@single_phase
def test_get_ptc_assignment__next_epoch(spec, state):
epoch = spec.Epoch(spec.get_current_epoch(state) + 1)
expect_assertion_error(lambda: spec.get_ptc_assignment(state, epoch, spec.ValidatorIndex(0)))
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,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.previous_epoch_last_ptc = [spec.ValidatorIndex(0)] * spec.PTC_SIZE

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