From 6c986259672262f86c933243adf9f19fb6be3aa9 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 6 Mar 2026 11:02:54 -0300 Subject: [PATCH 01/15] Add a PTC Lookbehind slice in the state --- specs/gloas/beacon-chain.md | 60 ++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 6731919605..9ff1d7b4bd 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) @@ -67,6 +68,7 @@ - [Epoch processing](#epoch-processing) - [Modified `process_epoch`](#modified-process_epoch) - [New `process_builder_pending_payments`](#new-process_builder_pending_payments) + - [New `process_ptc_lookbehind`](#new-process_ptc_lookbehind) - [Block processing](#block-processing) - [Withdrawals](#withdrawals) - [New `get_builder_withdrawals`](#new-get_builder_withdrawals) @@ -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] + ptc_lookbehind: Vector[Vector[ValidatorIndex, PTC_SIZE], 2 * SLOTS_PER_EPOCH] ``` ## Dataclasses @@ -630,6 +634,26 @@ def compute_proposer_indices( ] ``` +#### New `compute_ptc` + +```python +def compute_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 + ) +``` + ### Beacon state accessors #### Modified `get_next_sync_committee_indices` @@ -711,16 +735,10 @@ 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) + if epoch < state_epoch: + return state.ptc_lookbehind[slot % SLOTS_PER_EPOCH] + return state.ptc_lookbehind[SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH] ``` #### New `get_indexed_payload_attestation` @@ -815,6 +833,9 @@ def process_slot(state: BeaconState) -> None: #### Modified `process_epoch` +*Note*: The function `process_epoch` is modified inGloas to call the new helpers +`process_builder_pending_payments` and `process_ptc_lookbehind`. + ```python def process_epoch(state: BeaconState) -> None: process_justification_and_finalization(state) @@ -834,6 +855,8 @@ def process_epoch(state: BeaconState) -> None: process_participation_flag_updates(state) process_sync_committee_updates(state) process_proposer_lookahead(state) + # [New in Gloas:EIP7732] + process_ptc_lookbehind(state) ``` #### New `process_builder_pending_payments` @@ -853,6 +876,23 @@ def process_builder_pending_payments(state: BeaconState) -> None: state.builder_pending_payments = old_payments + new_payments ``` +#### New `process_ptc_lookbehind` + +```python +def process_ptc_lookbehind(state: BeaconState) -> None: + """ + Get the payload timeliness committee for the given ``slot``. + """ + # Shift out ptc members in the first epoch + state.ptc_lookbehind[:SLOTS_PER_EPOCH] = state.ptc_lookbehind[SLOTS_PER_EPOCH:] + # Fill in the last epoch withnew ptc indices + start_slot = state.slot + 1 - SLOTS_PER_EPOCH + last_epoch_committees = [ + compute_ptc(state, Slot(slot)) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH) + ] + state.ptc_lookbehind[SLOTS_PER_EPOCH:] = last_epoch_committees +``` + ### Block processing ```python From 37f1c0e09d51e15b0df6ab1047675ed0efecc312 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 6 Mar 2026 15:21:13 +0100 Subject: [PATCH 02/15] Update specs/gloas/beacon-chain.md Co-authored-by: Justin Traglia <95511699+jtraglia@users.noreply.github.com> --- specs/gloas/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 9ff1d7b4bd..030b07a395 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -833,7 +833,7 @@ def process_slot(state: BeaconState) -> None: #### Modified `process_epoch` -*Note*: The function `process_epoch` is modified inGloas to call the new helpers +*Note*: The function `process_epoch` is modified in Gloas to call the new helpers `process_builder_pending_payments` and `process_ptc_lookbehind`. ```python From 2bdbf018b624fb83fa8a43eeff7c6328b169c136 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 6 Mar 2026 15:25:47 +0100 Subject: [PATCH 03/15] Update specs/gloas/beacon-chain.md Co-authored-by: Justin Traglia <95511699+jtraglia@users.noreply.github.com> --- specs/gloas/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 030b07a395..1998744446 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -885,7 +885,7 @@ def process_ptc_lookbehind(state: BeaconState) -> None: """ # Shift out ptc members in the first epoch state.ptc_lookbehind[:SLOTS_PER_EPOCH] = state.ptc_lookbehind[SLOTS_PER_EPOCH:] - # Fill in the last epoch withnew ptc indices + # Fill in the last epoch with new ptc indices start_slot = state.slot + 1 - SLOTS_PER_EPOCH last_epoch_committees = [ compute_ptc(state, Slot(slot)) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH) From fd398a6251af5457130f5b728fdbcbcde2509c1f Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 6 Mar 2026 11:32:20 -0300 Subject: [PATCH 04/15] lint --- specs/gloas/beacon-chain.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 1998744446..789b972194 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -833,8 +833,8 @@ def process_slot(state: BeaconState) -> None: #### Modified `process_epoch` -*Note*: The function `process_epoch` is modified in Gloas to call the new helpers -`process_builder_pending_payments` and `process_ptc_lookbehind`. +*Note*: The function `process_epoch` is modified in Gloas to call the new +helpers `process_builder_pending_payments` and `process_ptc_lookbehind`. ```python def process_epoch(state: BeaconState) -> None: From 10e5813d4f259a30c22715ba241ac9a7754bf6cc Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 6 Mar 2026 08:57:45 -0600 Subject: [PATCH 05/15] Fix off-by-1 issue in process_ptc_lookahead --- specs/gloas/beacon-chain.md | 9 +++-- .../test_process_ptc_lookbehind.py | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_lookbehind.py diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 789b972194..64337e8365 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -881,12 +881,13 @@ def process_builder_pending_payments(state: BeaconState) -> None: ```python def process_ptc_lookbehind(state: BeaconState) -> None: """ - Get the payload timeliness committee for the given ``slot``. + Update the cached PTC window for the previous and current epochs. """ - # Shift out ptc members in the first epoch + # Shift out PTC members in the first epoch state.ptc_lookbehind[:SLOTS_PER_EPOCH] = state.ptc_lookbehind[SLOTS_PER_EPOCH:] - # Fill in the last epoch with new ptc indices - start_slot = state.slot + 1 - SLOTS_PER_EPOCH + # Fill in the last epoch with the PTC for the epoch that starts after this boundary + next_epoch = Epoch(get_current_epoch(state) + 1) + start_slot = compute_start_slot_at_epoch(next_epoch) last_epoch_committees = [ compute_ptc(state, Slot(slot)) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH) ] diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_lookbehind.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_lookbehind.py new file mode 100644 index 0000000000..267e686c89 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_lookbehind.py @@ -0,0 +1,40 @@ +from eth_consensus_specs.test.context import ( + 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 + + +def _compute_epoch_ptc(spec, state, epoch): + start_slot = spec.compute_start_slot_at_epoch(epoch) + return [ + spec.compute_ptc(state, spec.Slot(slot)) + for slot in range(start_slot, start_slot + spec.SLOTS_PER_EPOCH) + ] + + +@with_phases([GLOAS]) +@spec_state_test +@single_phase +def test_process_ptc_lookbehind_rotates_to_next_epoch(spec, state): + spec.process_slots(state, state.slot + spec.SLOTS_PER_EPOCH - 1) + + current_epoch = spec.get_current_epoch(state) + current_epoch_ptc = _compute_epoch_ptc(spec, state, current_epoch) + state.ptc_lookbehind = current_epoch_ptc + current_epoch_ptc + + next_epoch = spec.Epoch(current_epoch + 1) + next_epoch_ptc = _compute_epoch_ptc(spec, state, next_epoch) + + yield from run_epoch_processing_with(spec, state, "process_ptc_lookbehind") + + assert list(state.ptc_lookbehind[: spec.SLOTS_PER_EPOCH]) == current_epoch_ptc + assert list(state.ptc_lookbehind[spec.SLOTS_PER_EPOCH :]) == next_epoch_ptc + + # run_epoch_processing_with does not increment the slot + state.slot += 1 + + assert spec.get_ptc(state, spec.Slot(state.slot - 1)) == current_epoch_ptc[-1] + assert spec.get_ptc(state, state.slot) == next_epoch_ptc[0] From a7bd06c092094da81f84e9a89d1757a0cc7a05bd Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 6 Mar 2026 09:17:19 -0600 Subject: [PATCH 06/15] Make necessary changes to fork specs --- specs/gloas/fork.md | 23 +++++++++++++++++++ specs/heze/beacon-chain.md | 1 + specs/heze/fork.md | 1 + .../test_process_payload_attestation.py | 2 ++ .../test/helpers/genesis.py | 1 + 5 files changed, 28 insertions(+) diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 88184ae982..e3e5b3ee71 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -7,6 +7,7 @@ - [Introduction](#introduction) - [Configuration](#configuration) - [Helpers](#helpers) + - [New `initialize_ptc_lookbehind`](#new-initialize_ptc_lookbehind) - [New `onboard_builders_from_pending_deposits`](#new-onboard_builders_from_pending_deposits) - [Fork to Gloas](#fork-to-gloas) - [Fork trigger](#fork-trigger) @@ -29,6 +30,26 @@ Warning: this configuration is not definitive. ## Helpers +### New `initialize_ptc_lookbehind` + +```python +def initialize_ptc_lookbehind( + state: BeaconState, +) -> Vector[Vector[ValidatorIndex, PTC_SIZE], 2 * SLOTS_PER_EPOCH]: + """ + Return the cached PTC window starting from the current epoch. + Used to initialize the ``ptc_lookbehind`` field in the beacon state at genesis and after forks. + """ + current_epoch = get_current_epoch(state) + start_slot = compute_start_slot_at_epoch(current_epoch) + empty_previous_epoch = [ + Vector[ValidatorIndex, PTC_SIZE]([ValidatorIndex(0) for _ in range(PTC_SIZE)]) + for _ in range(SLOTS_PER_EPOCH) + ] + current_epoch_ptc = [compute_ptc(state, Slot(start_slot + i)) for i in range(SLOTS_PER_EPOCH)] + return empty_previous_epoch + current_epoch_ptc +``` + ### New `onboard_builders_from_pending_deposits` ```python @@ -159,6 +180,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] + ptc_lookbehind=initialize_ptc_lookbehind(pre), ) # [New in Gloas:EIP7732] diff --git a/specs/heze/beacon-chain.md b/specs/heze/beacon-chain.md index a82797401f..8ca64b5d12 100644 --- a/specs/heze/beacon-chain.md +++ b/specs/heze/beacon-chain.md @@ -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] + ptc_lookbehind: Vector[Vector[ValidatorIndex, PTC_SIZE], 2 * SLOTS_PER_EPOCH] ``` ## Helpers diff --git a/specs/heze/fork.md b/specs/heze/fork.md index 01333da7a5..a50a628de2 100644 --- a/specs/heze/fork.md +++ b/specs/heze/fork.md @@ -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, + ptc_lookbehind=pre.ptc_lookbehind, ) 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..4cd3741639 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 @@ -375,6 +375,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 epoch processing, so refresh the cached current-epoch PTC. + state.ptc_lookbehind = spec.initialize_ptc_lookbehind(state) chosen_slot = None chosen_index = None 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..cb158aa4d0 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.ptc_lookbehind = spec.initialize_ptc_lookbehind(state) if is_post_fulu(spec): # Initialize proposer lookahead list From dc0b3f41ff0492c248c5954f216193efc0d2c14e Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 6 Mar 2026 09:46:48 -0600 Subject: [PATCH 07/15] Make some improvements to get_ptc() and add unit tests --- specs/gloas/beacon-chain.md | 9 +- .../gloas/unittests/validator/__init__.py | 1 + .../unittests/validator/test_validator.py | 102 ++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/validator/__init__.py create mode 100644 tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/validator/test_validator.py diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 64337e8365..7aedec1a9f 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -729,6 +729,9 @@ def get_attestation_participation_flag_indices( #### New `get_ptc` +*Note*: `get_ptc` uses the cached `ptc_lookbehind` for previous and current +epoch lookups, and computes next-epoch PTC assignments on demand. + ```python def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: """ @@ -736,9 +739,13 @@ def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: """ epoch = compute_epoch_at_slot(slot) state_epoch = get_current_epoch(state) + assert epoch <= state_epoch + 1 if epoch < state_epoch: + assert epoch + 1 == state_epoch return state.ptc_lookbehind[slot % SLOTS_PER_EPOCH] - return state.ptc_lookbehind[SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH] + if epoch == state_epoch: + return state.ptc_lookbehind[SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH] + return compute_ptc(state, slot) ``` #### New `get_indexed_payload_attestation` diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/validator/__init__.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/validator/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/validator/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/validator/test_validator.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/validator/test_validator.py new file mode 100644 index 0000000000..e828f938b1 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/validator/test_validator.py @@ -0,0 +1,102 @@ +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 _run_get_ptc_assignments(spec, state, epoch, valid=True, assignments=None): + if not valid: + expect_assertion_error( + lambda: spec.get_ptc_assignment(state, epoch, spec.ValidatorIndex(0)) + ) + return + + if assignments is None: + assignments = _compute_first_ptc_assignments(spec, state, epoch) + _assert_get_ptc_assignments(spec, state, epoch, 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__current_epoch_minus_2(spec, state): + next_epoch(spec, state) + next_epoch(spec, state) + + epoch = spec.Epoch(spec.get_current_epoch(state) - 2) + _run_get_ptc_assignments(spec, state, epoch, valid=False) + + +@with_phases([GLOAS]) +@spec_test +@with_state +@single_phase +def test_get_ptc_assignment__current_epoch_minus_1(spec, state): + previous_epoch = spec.get_current_epoch(state) + previous_assignments = _compute_first_ptc_assignments(spec, state, previous_epoch) + + next_epoch(spec, state) + + _run_get_ptc_assignments( + spec, + state, + previous_epoch, + valid=True, + assignments=previous_assignments, + ) + + +@with_phases([GLOAS]) +@spec_test +@with_state +@single_phase +def test_get_ptc_assignment__current_epoch(spec, state): + epoch = spec.get_current_epoch(state) + _run_get_ptc_assignments(spec, state, epoch, valid=True) + + +@with_phases([GLOAS]) +@spec_test +@with_state +@single_phase +def test_get_ptc_assignment__current_epoch_plus_1(spec, state): + epoch = spec.Epoch(spec.get_current_epoch(state) + 1) + _run_get_ptc_assignments(spec, state, epoch, valid=True) + + +@with_phases([GLOAS]) +@spec_test +@with_state +@single_phase +def test_get_ptc_assignment__current_epoch_plus_2(spec, state): + epoch = spec.Epoch(spec.get_current_epoch(state) + 2) + _run_get_ptc_assignments(spec, state, epoch, valid=False) From 72d5acc495bde927e53571bf2019299584a2bbd7 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 23 Mar 2026 11:39:41 -0300 Subject: [PATCH 08/15] Add 3 epochs --- specs/gloas/beacon-chain.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 7aedec1a9f..87a20375d1 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -387,7 +387,7 @@ class BeaconState(Container): # [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 * SLOTS_PER_EPOCH] + ptc_lookbehind: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD)* SLOTS_PER_EPOCH] ``` ## Dataclasses @@ -729,8 +729,7 @@ def get_attestation_participation_flag_indices( #### New `get_ptc` -*Note*: `get_ptc` uses the cached `ptc_lookbehind` for previous and current -epoch lookups, and computes next-epoch PTC assignments on demand. +*Note*: `get_ptc` uses the cached `ptc_lookbehind` for lookups. ```python def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: @@ -739,13 +738,12 @@ def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: """ epoch = compute_epoch_at_slot(slot) state_epoch = get_current_epoch(state) - assert epoch <= state_epoch + 1 if epoch < state_epoch: assert epoch + 1 == state_epoch return state.ptc_lookbehind[slot % SLOTS_PER_EPOCH] - if epoch == state_epoch: - return state.ptc_lookbehind[SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH] - return compute_ptc(state, slot) + assert epoch <= state_epoch + MIN_SEED_LOOKAHEAD + offset = (epoch - state_epoch + 1) * SLOTS_PER_EPOCH + return state.ptc_lookbehind[offset + slot % SLOTS_PER_EPOCH] ``` #### New `get_indexed_payload_attestation` @@ -888,17 +886,18 @@ def process_builder_pending_payments(state: BeaconState) -> None: ```python def process_ptc_lookbehind(state: BeaconState) -> None: """ - Update the cached PTC window for the previous and current epochs. + Update the cached PTC window. """ # Shift out PTC members in the first epoch - state.ptc_lookbehind[:SLOTS_PER_EPOCH] = state.ptc_lookbehind[SLOTS_PER_EPOCH:] + last_epoch_start = len(state.ptc_lookbehind) - SLOTS_PER_EPOCH + state.ptc_lookbehind[:last_epoch_start] = state.ptc_lookbehind[SLOTS_PER_EPOCH:] # Fill in the last epoch with the PTC for the epoch that starts after this boundary - next_epoch = Epoch(get_current_epoch(state) + 1) + next_epoch = Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1) start_slot = compute_start_slot_at_epoch(next_epoch) last_epoch_committees = [ compute_ptc(state, Slot(slot)) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH) ] - state.ptc_lookbehind[SLOTS_PER_EPOCH:] = last_epoch_committees + state.ptc_lookbehind[last_epoch_start:] = last_epoch_committees ``` ### Block processing From bc6b2c57f56f388a93e26a02097e7a65439ec2b2 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Mon, 23 Mar 2026 12:29:02 -0500 Subject: [PATCH 09/15] Refactor process_ptc_lookbehind --- specs/gloas/beacon-chain.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 87a20375d1..e34dc176e8 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -387,7 +387,9 @@ class BeaconState(Container): # [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 + MIN_SEED_LOOKAHEAD)* SLOTS_PER_EPOCH] + ptc_lookbehind: Vector[ + Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH + ] ``` ## Dataclasses @@ -729,7 +731,7 @@ def get_attestation_participation_flag_indices( #### New `get_ptc` -*Note*: `get_ptc` uses the cached `ptc_lookbehind` for lookups. +*Note*: `get_ptc` uses the cached `ptc_lookbehind` for lookups. ```python def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: @@ -888,16 +890,20 @@ def process_ptc_lookbehind(state: BeaconState) -> None: """ Update the cached PTC window. """ - # Shift out PTC members in the first epoch - last_epoch_start = len(state.ptc_lookbehind) - SLOTS_PER_EPOCH - state.ptc_lookbehind[:last_epoch_start] = state.ptc_lookbehind[SLOTS_PER_EPOCH:] - # Fill in the last epoch with the PTC for the epoch that starts after this boundary + prev = 0 + curr = SLOTS_PER_EPOCH + next = 2 * SLOTS_PER_EPOCH + + # Previous epoch: shift in PTC from the current epoch + state.ptc_lookbehind[prev:curr] = state.ptc_lookbehind[curr:next] + # Current epoch: shift in PTC from the next epoch + state.ptc_lookbehind[curr:next] = state.ptc_lookbehind[next:] + # Next epoch: compute PTC for the epoch next_epoch = Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1) start_slot = compute_start_slot_at_epoch(next_epoch) - last_epoch_committees = [ + state.ptc_lookbehind[next:] = [ compute_ptc(state, Slot(slot)) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH) ] - state.ptc_lookbehind[last_epoch_start:] = last_epoch_committees ``` ### Block processing From 6579ca0862d9e215f1ef628b2df88c3d8295cfee Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Mon, 23 Mar 2026 12:35:26 -0500 Subject: [PATCH 10/15] Rename ptc_lookbehind to ptc_window --- specs/gloas/beacon-chain.md | 26 +++++++++---------- specs/gloas/fork.md | 10 +++---- specs/heze/beacon-chain.md | 2 +- specs/heze/fork.md | 2 +- .../test_process_payload_attestation.py | 2 +- ...okbehind.py => test_process_ptc_window.py} | 10 +++---- .../test/helpers/genesis.py | 2 +- 7 files changed, 26 insertions(+), 28 deletions(-) rename tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/{test_process_ptc_lookbehind.py => test_process_ptc_window.py} (78%) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index e34dc176e8..2f1c1d3c92 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -68,7 +68,7 @@ - [Epoch processing](#epoch-processing) - [Modified `process_epoch`](#modified-process_epoch) - [New `process_builder_pending_payments`](#new-process_builder_pending_payments) - - [New `process_ptc_lookbehind`](#new-process_ptc_lookbehind) + - [New `process_ptc_window`](#new-process_ptc_window) - [Block processing](#block-processing) - [Withdrawals](#withdrawals) - [New `get_builder_withdrawals`](#new-get_builder_withdrawals) @@ -387,9 +387,7 @@ class BeaconState(Container): # [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 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH - ] + ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] ``` ## Dataclasses @@ -731,7 +729,7 @@ def get_attestation_participation_flag_indices( #### New `get_ptc` -*Note*: `get_ptc` uses the cached `ptc_lookbehind` for lookups. +*Note*: `get_ptc` uses the cached `ptc_window` for lookups. ```python def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: @@ -742,10 +740,10 @@ def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: state_epoch = get_current_epoch(state) if epoch < state_epoch: assert epoch + 1 == state_epoch - return state.ptc_lookbehind[slot % SLOTS_PER_EPOCH] + return state.ptc_window[slot % SLOTS_PER_EPOCH] assert epoch <= state_epoch + MIN_SEED_LOOKAHEAD offset = (epoch - state_epoch + 1) * SLOTS_PER_EPOCH - return state.ptc_lookbehind[offset + slot % SLOTS_PER_EPOCH] + return state.ptc_window[offset + slot % SLOTS_PER_EPOCH] ``` #### New `get_indexed_payload_attestation` @@ -841,7 +839,7 @@ def process_slot(state: BeaconState) -> None: #### Modified `process_epoch` *Note*: The function `process_epoch` is modified in Gloas to call the new -helpers `process_builder_pending_payments` and `process_ptc_lookbehind`. +helpers `process_builder_pending_payments` and `process_ptc_window`. ```python def process_epoch(state: BeaconState) -> None: @@ -863,7 +861,7 @@ def process_epoch(state: BeaconState) -> None: process_sync_committee_updates(state) process_proposer_lookahead(state) # [New in Gloas:EIP7732] - process_ptc_lookbehind(state) + process_ptc_window(state) ``` #### New `process_builder_pending_payments` @@ -883,10 +881,10 @@ def process_builder_pending_payments(state: BeaconState) -> None: state.builder_pending_payments = old_payments + new_payments ``` -#### New `process_ptc_lookbehind` +#### New `process_ptc_window` ```python -def process_ptc_lookbehind(state: BeaconState) -> None: +def process_ptc_window(state: BeaconState) -> None: """ Update the cached PTC window. """ @@ -895,13 +893,13 @@ def process_ptc_lookbehind(state: BeaconState) -> None: next = 2 * SLOTS_PER_EPOCH # Previous epoch: shift in PTC from the current epoch - state.ptc_lookbehind[prev:curr] = state.ptc_lookbehind[curr:next] + state.ptc_window[prev:curr] = state.ptc_window[curr:next] # Current epoch: shift in PTC from the next epoch - state.ptc_lookbehind[curr:next] = state.ptc_lookbehind[next:] + state.ptc_window[curr:next] = state.ptc_window[next:] # Next epoch: compute PTC for the epoch next_epoch = Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1) start_slot = compute_start_slot_at_epoch(next_epoch) - state.ptc_lookbehind[next:] = [ + state.ptc_window[next:] = [ compute_ptc(state, Slot(slot)) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH) ] ``` diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index e3e5b3ee71..8245583351 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -7,7 +7,7 @@ - [Introduction](#introduction) - [Configuration](#configuration) - [Helpers](#helpers) - - [New `initialize_ptc_lookbehind`](#new-initialize_ptc_lookbehind) + - [New `initialize_ptc_window`](#new-initialize_ptc_window) - [New `onboard_builders_from_pending_deposits`](#new-onboard_builders_from_pending_deposits) - [Fork to Gloas](#fork-to-gloas) - [Fork trigger](#fork-trigger) @@ -30,15 +30,15 @@ Warning: this configuration is not definitive. ## Helpers -### New `initialize_ptc_lookbehind` +### New `initialize_ptc_window` ```python -def initialize_ptc_lookbehind( +def initialize_ptc_window( state: BeaconState, ) -> Vector[Vector[ValidatorIndex, PTC_SIZE], 2 * SLOTS_PER_EPOCH]: """ Return the cached PTC window starting from the current epoch. - Used to initialize the ``ptc_lookbehind`` field in the beacon state at genesis and after forks. + Used to initialize the ``ptc_window`` field in the beacon state at genesis and after forks. """ current_epoch = get_current_epoch(state) start_slot = compute_start_slot_at_epoch(current_epoch) @@ -181,7 +181,7 @@ def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: # [New in Gloas:EIP7732] payload_expected_withdrawals=[], # [New in Gloas:EIP7732] - ptc_lookbehind=initialize_ptc_lookbehind(pre), + ptc_window=initialize_ptc_window(pre), ) # [New in Gloas:EIP7732] diff --git a/specs/heze/beacon-chain.md b/specs/heze/beacon-chain.md index 8ca64b5d12..a3a2a63f38 100644 --- a/specs/heze/beacon-chain.md +++ b/specs/heze/beacon-chain.md @@ -150,7 +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] - ptc_lookbehind: Vector[Vector[ValidatorIndex, PTC_SIZE], 2 * SLOTS_PER_EPOCH] + ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] ``` ## Helpers diff --git a/specs/heze/fork.md b/specs/heze/fork.md index a50a628de2..7aef37d7df 100644 --- a/specs/heze/fork.md +++ b/specs/heze/fork.md @@ -103,7 +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, - ptc_lookbehind=pre.ptc_lookbehind, + ptc_window=pre.ptc_window, ) 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 4cd3741639..eef256e508 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 @@ -376,7 +376,7 @@ def test_process_payload_attestation_sampling_not_capped(spec, state): for validator in state.validators: validator.effective_balance = low_balance # Direct balance mutations bypass epoch processing, so refresh the cached current-epoch PTC. - state.ptc_lookbehind = spec.initialize_ptc_lookbehind(state) + state.ptc_window = spec.initialize_ptc_window(state) chosen_slot = None chosen_index = None diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_lookbehind.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py similarity index 78% rename from tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_lookbehind.py rename to tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py index 267e686c89..cefa39b642 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_lookbehind.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py @@ -18,20 +18,20 @@ def _compute_epoch_ptc(spec, state, epoch): @with_phases([GLOAS]) @spec_state_test @single_phase -def test_process_ptc_lookbehind_rotates_to_next_epoch(spec, state): +def test_process_ptc_window_rotates_to_next_epoch(spec, state): spec.process_slots(state, state.slot + spec.SLOTS_PER_EPOCH - 1) current_epoch = spec.get_current_epoch(state) current_epoch_ptc = _compute_epoch_ptc(spec, state, current_epoch) - state.ptc_lookbehind = current_epoch_ptc + current_epoch_ptc + state.ptc_window = current_epoch_ptc + current_epoch_ptc next_epoch = spec.Epoch(current_epoch + 1) next_epoch_ptc = _compute_epoch_ptc(spec, state, next_epoch) - yield from run_epoch_processing_with(spec, state, "process_ptc_lookbehind") + yield from run_epoch_processing_with(spec, state, "process_ptc_window") - assert list(state.ptc_lookbehind[: spec.SLOTS_PER_EPOCH]) == current_epoch_ptc - assert list(state.ptc_lookbehind[spec.SLOTS_PER_EPOCH :]) == next_epoch_ptc + assert list(state.ptc_window[: spec.SLOTS_PER_EPOCH]) == current_epoch_ptc + assert list(state.ptc_window[spec.SLOTS_PER_EPOCH :]) == next_epoch_ptc # run_epoch_processing_with does not increment the slot state.slot += 1 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 cb158aa4d0..2130c8eff0 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py @@ -252,7 +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.ptc_lookbehind = spec.initialize_ptc_lookbehind(state) + state.ptc_window = spec.initialize_ptc_window(state) if is_post_fulu(spec): # Initialize proposer lookahead list From b7c1794903d7deea5fed836b3f2061b5f87520aa Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Mon, 23 Mar 2026 12:45:32 -0500 Subject: [PATCH 11/15] Update initialize_ptc_window & fix a test --- specs/gloas/fork.md | 12 ++++--- .../test_process_ptc_window.py | 35 +++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 8245583351..85b9414da7 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -35,19 +35,23 @@ Warning: this configuration is not definitive. ```python def initialize_ptc_window( state: BeaconState, -) -> Vector[Vector[ValidatorIndex, PTC_SIZE], 2 * SLOTS_PER_EPOCH]: +) -> Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH]: """ Return the cached PTC window starting from the current epoch. Used to initialize the ``ptc_window`` field in the beacon state at genesis and after forks. """ current_epoch = get_current_epoch(state) - start_slot = compute_start_slot_at_epoch(current_epoch) empty_previous_epoch = [ Vector[ValidatorIndex, PTC_SIZE]([ValidatorIndex(0) for _ in range(PTC_SIZE)]) for _ in range(SLOTS_PER_EPOCH) ] - current_epoch_ptc = [compute_ptc(state, Slot(start_slot + i)) for i in range(SLOTS_PER_EPOCH)] - return empty_previous_epoch + current_epoch_ptc + current_start = compute_start_slot_at_epoch(current_epoch) + current_epoch_ptc = [ + compute_ptc(state, Slot(current_start + i)) for i in range(SLOTS_PER_EPOCH) + ] + next_start = compute_start_slot_at_epoch(Epoch(current_epoch + MIN_SEED_LOOKAHEAD)) + next_epoch_ptc = [compute_ptc(state, Slot(next_start + i)) for i in range(SLOTS_PER_EPOCH)] + return empty_previous_epoch + current_epoch_ptc + next_epoch_ptc ``` ### New `onboard_builders_from_pending_deposits` diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py index cefa39b642..dd5ac79b81 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py @@ -18,23 +18,38 @@ def _compute_epoch_ptc(spec, state, epoch): @with_phases([GLOAS]) @spec_state_test @single_phase -def test_process_ptc_window_rotates_to_next_epoch(spec, state): - spec.process_slots(state, state.slot + spec.SLOTS_PER_EPOCH - 1) +def test_process_ptc_window_shifts_all_three_epochs(spec, state): + """ + Verify that process_ptc_window shifts prev/curr/next correctly + and that get_ptc returns the right committees afterwards. + """ + spec.process_slots(state, state.slot + 2 * spec.SLOTS_PER_EPOCH - 1) current_epoch = spec.get_current_epoch(state) - current_epoch_ptc = _compute_epoch_ptc(spec, state, current_epoch) - state.ptc_window = current_epoch_ptc + current_epoch_ptc + prev_epoch_ptc = _compute_epoch_ptc(spec, state, spec.Epoch(current_epoch - 1)) + curr_epoch_ptc = _compute_epoch_ptc(spec, state, current_epoch) + next_epoch_ptc = _compute_epoch_ptc(spec, state, spec.Epoch(current_epoch + 1)) - next_epoch = spec.Epoch(current_epoch + 1) - next_epoch_ptc = _compute_epoch_ptc(spec, state, next_epoch) + # Set up the 3-epoch window: [prev, curr, next] + state.ptc_window = prev_epoch_ptc + curr_epoch_ptc + next_epoch_ptc + + # Compute what the new next epoch should be after the shift + new_next_epoch = spec.Epoch(current_epoch + spec.MIN_SEED_LOOKAHEAD + 1) + new_next_epoch_ptc = _compute_epoch_ptc(spec, state, new_next_epoch) yield from run_epoch_processing_with(spec, state, "process_ptc_window") - assert list(state.ptc_window[: spec.SLOTS_PER_EPOCH]) == current_epoch_ptc - assert list(state.ptc_window[spec.SLOTS_PER_EPOCH :]) == next_epoch_ptc + # After shift: [curr, next, new_next] + SPE = spec.SLOTS_PER_EPOCH + assert list(state.ptc_window[:SPE]) == curr_epoch_ptc + assert list(state.ptc_window[SPE : 2 * SPE]) == next_epoch_ptc + assert list(state.ptc_window[2 * SPE :]) == new_next_epoch_ptc - # run_epoch_processing_with does not increment the slot + # run_epoch_processing_with does not increment the slot, so do it manually state.slot += 1 - assert spec.get_ptc(state, spec.Slot(state.slot - 1)) == current_epoch_ptc[-1] + # Now state_epoch = current_epoch + 1 + # Previous epoch lookup (current_epoch) should hit the first section + assert spec.get_ptc(state, spec.Slot(state.slot - 1)) == curr_epoch_ptc[-1] + # Current epoch lookup should hit the second section assert spec.get_ptc(state, state.slot) == next_epoch_ptc[0] From 41d963c4304ccac3a86916d78cded61e02910196 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Mon, 23 Mar 2026 13:13:33 -0500 Subject: [PATCH 12/15] Hardcode ptc_window to be prev/curr/next epochs --- specs/gloas/beacon-chain.md | 8 +++---- specs/gloas/fork.md | 10 ++++---- specs/heze/beacon-chain.md | 2 +- .../test_process_ptc_window.py | 24 +++---------------- 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 2f1c1d3c92..5dc2a00d1e 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -387,7 +387,7 @@ class BeaconState(Container): # [New in Gloas:EIP7732] payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in Gloas:EIP7732] - ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] + ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], 3 * SLOTS_PER_EPOCH] ``` ## Dataclasses @@ -741,7 +741,7 @@ def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: if epoch < state_epoch: assert epoch + 1 == state_epoch return state.ptc_window[slot % SLOTS_PER_EPOCH] - assert epoch <= state_epoch + MIN_SEED_LOOKAHEAD + assert epoch <= state_epoch + 1 offset = (epoch - state_epoch + 1) * SLOTS_PER_EPOCH return state.ptc_window[offset + slot % SLOTS_PER_EPOCH] ``` @@ -889,7 +889,7 @@ def process_ptc_window(state: BeaconState) -> None: Update the cached PTC window. """ prev = 0 - curr = SLOTS_PER_EPOCH + curr = 1 * SLOTS_PER_EPOCH next = 2 * SLOTS_PER_EPOCH # Previous epoch: shift in PTC from the current epoch @@ -897,7 +897,7 @@ def process_ptc_window(state: BeaconState) -> None: # Current epoch: shift in PTC from the next epoch state.ptc_window[curr:next] = state.ptc_window[next:] # Next epoch: compute PTC for the epoch - next_epoch = Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1) + next_epoch = Epoch(get_current_epoch(state) + 2) start_slot = compute_start_slot_at_epoch(next_epoch) state.ptc_window[next:] = [ compute_ptc(state, Slot(slot)) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH) diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 85b9414da7..285aef49e7 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -35,23 +35,23 @@ Warning: this configuration is not definitive. ```python def initialize_ptc_window( state: BeaconState, -) -> Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH]: +) -> Vector[Vector[ValidatorIndex, PTC_SIZE], 3 * SLOTS_PER_EPOCH]: """ Return the cached PTC window starting from the current epoch. Used to initialize the ``ptc_window`` field in the beacon state at genesis and after forks. """ - current_epoch = get_current_epoch(state) - empty_previous_epoch = [ + previous_epoch_ptc = [ Vector[ValidatorIndex, PTC_SIZE]([ValidatorIndex(0) for _ in range(PTC_SIZE)]) for _ in range(SLOTS_PER_EPOCH) ] + current_epoch = get_current_epoch(state) current_start = compute_start_slot_at_epoch(current_epoch) current_epoch_ptc = [ compute_ptc(state, Slot(current_start + i)) for i in range(SLOTS_PER_EPOCH) ] - next_start = compute_start_slot_at_epoch(Epoch(current_epoch + MIN_SEED_LOOKAHEAD)) + next_start = compute_start_slot_at_epoch(Epoch(current_epoch + 1)) next_epoch_ptc = [compute_ptc(state, Slot(next_start + i)) for i in range(SLOTS_PER_EPOCH)] - return empty_previous_epoch + current_epoch_ptc + next_epoch_ptc + return previous_epoch_ptc + current_epoch_ptc + next_epoch_ptc ``` ### New `onboard_builders_from_pending_deposits` diff --git a/specs/heze/beacon-chain.md b/specs/heze/beacon-chain.md index a3a2a63f38..c6f53aea25 100644 --- a/specs/heze/beacon-chain.md +++ b/specs/heze/beacon-chain.md @@ -150,7 +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] - ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] + ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], 3 * SLOTS_PER_EPOCH] ``` ## Helpers diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py index dd5ac79b81..5c297d0ebc 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py @@ -7,14 +7,6 @@ from eth_consensus_specs.test.helpers.epoch_processing import run_epoch_processing_with -def _compute_epoch_ptc(spec, state, epoch): - start_slot = spec.compute_start_slot_at_epoch(epoch) - return [ - spec.compute_ptc(state, spec.Slot(slot)) - for slot in range(start_slot, start_slot + spec.SLOTS_PER_EPOCH) - ] - - @with_phases([GLOAS]) @spec_state_test @single_phase @@ -25,25 +17,15 @@ def test_process_ptc_window_shifts_all_three_epochs(spec, state): """ spec.process_slots(state, state.slot + 2 * spec.SLOTS_PER_EPOCH - 1) - current_epoch = spec.get_current_epoch(state) - prev_epoch_ptc = _compute_epoch_ptc(spec, state, spec.Epoch(current_epoch - 1)) - curr_epoch_ptc = _compute_epoch_ptc(spec, state, current_epoch) - next_epoch_ptc = _compute_epoch_ptc(spec, state, spec.Epoch(current_epoch + 1)) - - # Set up the 3-epoch window: [prev, curr, next] - state.ptc_window = prev_epoch_ptc + curr_epoch_ptc + next_epoch_ptc - - # Compute what the new next epoch should be after the shift - new_next_epoch = spec.Epoch(current_epoch + spec.MIN_SEED_LOOKAHEAD + 1) - new_next_epoch_ptc = _compute_epoch_ptc(spec, state, new_next_epoch) + SPE = spec.SLOTS_PER_EPOCH + curr_epoch_ptc = list(state.ptc_window[SPE : 2 * SPE]) + next_epoch_ptc = list(state.ptc_window[2 * SPE :]) yield from run_epoch_processing_with(spec, state, "process_ptc_window") # After shift: [curr, next, new_next] - SPE = spec.SLOTS_PER_EPOCH assert list(state.ptc_window[:SPE]) == curr_epoch_ptc assert list(state.ptc_window[SPE : 2 * SPE]) == next_epoch_ptc - assert list(state.ptc_window[2 * SPE :]) == new_next_epoch_ptc # run_epoch_processing_with does not increment the slot, so do it manually state.slot += 1 From 31745d0d05571d346267ac0a148e597062f340b2 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Mon, 23 Mar 2026 15:30:24 -0500 Subject: [PATCH 13/15] Add back MIN_SEED_LOOKAHEAD to ptc_window --- specs/gloas/beacon-chain.md | 20 +++++++------------ specs/gloas/fork.md | 19 +++++++++--------- specs/gloas/validator.md | 4 ++-- specs/heze/beacon-chain.md | 2 +- .../test_process_ptc_window.py | 5 +++-- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 5dc2a00d1e..3c6d040406 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -387,7 +387,7 @@ class BeaconState(Container): # [New in Gloas:EIP7732] payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in Gloas:EIP7732] - ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], 3 * SLOTS_PER_EPOCH] + ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] ``` ## Dataclasses @@ -741,7 +741,7 @@ def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: if epoch < state_epoch: assert epoch + 1 == state_epoch return state.ptc_window[slot % SLOTS_PER_EPOCH] - assert epoch <= state_epoch + 1 + assert epoch <= state_epoch + MIN_SEED_LOOKAHEAD offset = (epoch - state_epoch + 1) * SLOTS_PER_EPOCH return state.ptc_window[offset + slot % SLOTS_PER_EPOCH] ``` @@ -888,18 +888,12 @@ def process_ptc_window(state: BeaconState) -> None: """ Update the cached PTC window. """ - prev = 0 - curr = 1 * SLOTS_PER_EPOCH - next = 2 * SLOTS_PER_EPOCH - - # Previous epoch: shift in PTC from the current epoch - state.ptc_window[prev:curr] = state.ptc_window[curr:next] - # Current epoch: shift in PTC from the next epoch - state.ptc_window[curr:next] = state.ptc_window[next:] - # Next epoch: compute PTC for the epoch - next_epoch = Epoch(get_current_epoch(state) + 2) + # Shift all epochs forward by one + state.ptc_window[: len(state.ptc_window) - SLOTS_PER_EPOCH] = state.ptc_window[SLOTS_PER_EPOCH:] + # Fill in the last epoch + next_epoch = Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1) start_slot = compute_start_slot_at_epoch(next_epoch) - state.ptc_window[next:] = [ + state.ptc_window[len(state.ptc_window) - SLOTS_PER_EPOCH :] = [ compute_ptc(state, Slot(slot)) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH) ] ``` diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 285aef49e7..ea0970fb11 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -35,23 +35,24 @@ Warning: this configuration is not definitive. ```python def initialize_ptc_window( state: BeaconState, -) -> Vector[Vector[ValidatorIndex, PTC_SIZE], 3 * SLOTS_PER_EPOCH]: +) -> Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH]: """ Return the cached PTC window starting from the current epoch. Used to initialize the ``ptc_window`` field in the beacon state at genesis and after forks. """ - previous_epoch_ptc = [ + empty_previous_epoch = [ Vector[ValidatorIndex, PTC_SIZE]([ValidatorIndex(0) for _ in range(PTC_SIZE)]) for _ in range(SLOTS_PER_EPOCH) ] + + ptcs = [] current_epoch = get_current_epoch(state) - current_start = compute_start_slot_at_epoch(current_epoch) - current_epoch_ptc = [ - compute_ptc(state, Slot(current_start + i)) for i in range(SLOTS_PER_EPOCH) - ] - next_start = compute_start_slot_at_epoch(Epoch(current_epoch + 1)) - next_epoch_ptc = [compute_ptc(state, Slot(next_start + i)) for i in range(SLOTS_PER_EPOCH)] - return previous_epoch_ptc + current_epoch_ptc + next_epoch_ptc + for e in range(1 + MIN_SEED_LOOKAHEAD): + epoch = Epoch(current_epoch + e) + start_slot = compute_start_slot_at_epoch(epoch) + ptcs += [compute_ptc(state, Slot(start_slot + i)) for i in range(SLOTS_PER_EPOCH)] + + return empty_previous_epoch + ptcs ``` ### New `onboard_builders_from_pending_deposits` diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index d1d95f72a0..7ec2b57494 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -62,8 +62,8 @@ 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 + max_epoch = Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD) + assert epoch <= max_epoch start_slot = compute_start_slot_at_epoch(epoch) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH): diff --git a/specs/heze/beacon-chain.md b/specs/heze/beacon-chain.md index c6f53aea25..a3a2a63f38 100644 --- a/specs/heze/beacon-chain.md +++ b/specs/heze/beacon-chain.md @@ -150,7 +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] - ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], 3 * SLOTS_PER_EPOCH] + ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] ``` ## Helpers diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py index 5c297d0ebc..8ff53b843a 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_ptc_window.py @@ -10,7 +10,7 @@ @with_phases([GLOAS]) @spec_state_test @single_phase -def test_process_ptc_window_shifts_all_three_epochs(spec, state): +def test_process_ptc_window__shifts_all_epochs(spec, state): """ Verify that process_ptc_window shifts prev/curr/next correctly and that get_ptc returns the right committees afterwards. @@ -18,8 +18,9 @@ def test_process_ptc_window_shifts_all_three_epochs(spec, state): spec.process_slots(state, state.slot + 2 * spec.SLOTS_PER_EPOCH - 1) SPE = spec.SLOTS_PER_EPOCH + # Save current and next epoch sections before the shift curr_epoch_ptc = list(state.ptc_window[SPE : 2 * SPE]) - next_epoch_ptc = list(state.ptc_window[2 * SPE :]) + next_epoch_ptc = list(state.ptc_window[2 * SPE : 3 * SPE]) yield from run_epoch_processing_with(spec, state, "process_ptc_window") From e7b191044449c73f0b6117bfac6e0e49008dd5f1 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Tue, 24 Mar 2026 12:58:09 -0500 Subject: [PATCH 14/15] Fix epoch restrictions in paragraph --- specs/gloas/validator.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 7ec2b57494..94e23d2ee3 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -49,9 +49,10 @@ 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 <= get_current_epoch(state) + MIN_SEED_LOOKAHEAD`, as PTC committee +selection is only stable within the context of the current and next epochs the +lookahead. ```python def get_ptc_assignment( From 89ce53b0918d36a1e17e09e1a65b0d121edbbf6b Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Tue, 24 Mar 2026 16:24:01 -0500 Subject: [PATCH 15/15] Fix typo --- specs/gloas/validator.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 94e23d2ee3..b898a15c4e 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -51,8 +51,8 @@ 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 <= get_current_epoch(state) + MIN_SEED_LOOKAHEAD`, as PTC committee -selection is only stable within the context of the current and next epochs the -lookahead. +selection is only stable within the context of the current and next epochs in +the lookahead. ```python def get_ptc_assignment(