diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 6731919605..ca84b30eaa 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -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) @@ -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) @@ -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 @@ -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` @@ -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 + return state.current_ptc if slot == state.slot else state.previous_ptc ``` #### New `get_indexed_payload_attestation` @@ -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 diff --git a/specs/gloas/fork-choice.md b/specs/gloas/fork-choice.md index c1dfe470bb..e7e995501b 100644 --- a/specs/gloas/fork-choice.md +++ b/specs/gloas/fork-choice.md @@ -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 diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 88184ae982..06224f5197 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -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 ``` diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index d1d95f72a0..6ad162ecc2 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -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. - -```python -def get_ptc_assignment( - 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 diff --git a/specs/heze/beacon-chain.md b/specs/heze/beacon-chain.md index a82797401f..2a3bc598ce 100644 --- a/specs/heze/beacon-chain.md +++ b/specs/heze/beacon-chain.md @@ -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 diff --git a/specs/heze/fork.md b/specs/heze/fork.md index 01333da7a5..333652ea7e 100644 --- a/specs/heze/fork.md +++ b/specs/heze/fork.md @@ -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 diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_payload_attestation.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_payload_attestation.py index 15532e76dd..d09bed7fd6 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_payload_attestation.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_payload_attestation.py @@ -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) @@ -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 diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/sanity/test_slots.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/sanity/test_slots.py index 6bdd59ed1a..3764e40d52 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/sanity/test_slots.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/sanity/test_slots.py @@ -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)) diff --git a/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py index 45d25251b0..05d06ceb5e 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py @@ -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