From 4a13cdb22aaef45f8beaa2e1dc802f6e9ca26ab7 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 10:30:29 +1100 Subject: [PATCH 01/13] Update test vectors to v1.7.0-alpha.4 --- testing/ef_tests/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 48378a4c958..ab24ea35a04 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,6 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.3 +CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.4 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) From e6c766207805b95c65e28a23216bc52d1ff3c052 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 12:28:33 +1100 Subject: [PATCH 02/13] Claude's WIP --- .../src/per_epoch_processing/errors.rs | 1 + .../src/per_epoch_processing/single_pass.rs | 44 ++++++++++++ .../state_processing/src/upgrade/gloas.rs | 48 +++++++++++++ consensus/types/src/core/eth_spec.rs | 20 +++++- consensus/types/src/state/beacon_state.rs | 67 ++++++++++++++++++- consensus/types/src/state/committee_cache.rs | 37 +++++++++- .../ef_tests/src/cases/epoch_processing.rs | 19 +++++- testing/ef_tests/src/lib.rs | 2 +- testing/ef_tests/tests/tests.rs | 6 ++ 9 files changed, 235 insertions(+), 9 deletions(-) diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index 4818dcbf670..b58b29d360f 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -32,6 +32,7 @@ pub enum EpochProcessingError { MissingExitBalanceToConsume, PendingDepositsLogicError, ProposerLookaheadOutOfBounds(usize), + PtcWindowOutOfBounds(usize), } impl From for EpochProcessingError { diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 4eb1e36628b..1954a85a00e 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -479,6 +479,10 @@ pub fn process_epoch_single_pass( process_proposer_lookahead(state, spec)?; } + if conf.proposer_lookahead && fork_name.gloas_enabled() { + process_ptc_window(state, spec)?; + } + Ok(summary) } @@ -512,6 +516,46 @@ pub fn process_proposer_lookahead( Ok(()) } +pub fn process_ptc_window( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + let slots_per_epoch = E::slots_per_epoch() as usize; + let ptc_window_length = E::ptc_window_length(); + let mut window = state.ptc_window()?.clone().to_vec(); + + // Shift all epochs forward by one (drop the oldest epoch) + let shifted: Vec<_> = window[slots_per_epoch..].to_vec(); + window[..ptc_window_length.safe_sub(slots_per_epoch)?].clone_from_slice(&shifted); + + // Compute PTC for the new lookahead epoch + let next_epoch = state + .current_epoch() + .safe_add(spec.min_seed_lookahead.as_u64())? + .safe_add(1)?; + let start_slot = next_epoch.start_slot(E::slots_per_epoch()); + + // Build a committee cache for the lookahead epoch (beyond the normal Next bound) + let committee_cache = state.initialize_committee_cache_for_lookahead(next_epoch, spec)?; + + let last_epoch_start = ptc_window_length.safe_sub(slots_per_epoch)?; + for i in 0..slots_per_epoch { + let slot = start_slot.safe_add(i as u64)?; + let ptc = state.compute_ptc_with_cache(slot, &committee_cache, spec)?; + let ptc_u64: Vec = ptc.into_iter().map(|v| v as u64).collect(); + let entry = ssz_types::FixedVector::new(ptc_u64) + .map_err(|e| Error::BeaconStateError(BeaconStateError::SszTypesError(e)))?; + let index = last_epoch_start.safe_add(i)?; + *window + .get_mut(index) + .ok_or(Error::PtcWindowOutOfBounds(index))? = entry; + } + + *state.ptc_window_mut()? = Vector::new(window)?; + + Ok(()) +} + /// Calculate the quorum threshold for builder payments based on total active balance. fn get_builder_payment_quorum_threshold( state_ctxt: &StateContext, diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 7a88383ab0d..7fafe7c52ac 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -2,6 +2,7 @@ use crate::per_block_processing::{ is_valid_deposit_signature, process_operations::apply_deposit_for_builder, }; use milhouse::{List, Vector}; +use safe_arith::SafeArith; use ssz_types::BitVector; use std::collections::HashSet; use std::mem; @@ -109,6 +110,14 @@ pub fn upgrade_state_to_gloas( builder_pending_withdrawals: List::default(), // Empty list initially, latest_block_hash: pre.latest_execution_payload_header.block_hash, payload_expected_withdrawals: List::default(), + ptc_window: Vector::new(vec![ + ssz_types::FixedVector::new(vec![ + 0u64; + E::PTCSize::to_usize() + ]) + .map_err(Error::SszTypesError)?; + E::PtcWindowLength::to_usize() + ])?, // placeholder, will be initialized below // Caches total_active_balance: pre.total_active_balance, progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), @@ -120,10 +129,49 @@ pub fn upgrade_state_to_gloas( }); // [New in Gloas:EIP7732] onboard_builders_from_pending_deposits(&mut post, spec)?; + initialize_ptc_window(&mut post, spec)?; Ok(post) } +/// Initialize the `ptc_window` field in the beacon state at fork transition. +/// +/// The window contains: +/// - One epoch of empty entries (previous epoch) +/// - Computed PTC for the current epoch through `1 + MIN_SEED_LOOKAHEAD` epochs +fn initialize_ptc_window( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + let slots_per_epoch = E::slots_per_epoch() as usize; + let ptc_size = E::ptc_size(); + + // Empty previous epoch + let empty_entry = + ssz_types::FixedVector::new(vec![0u64; ptc_size]).map_err(Error::SszTypesError)?; + let mut window: Vec> = + vec![empty_entry; slots_per_epoch]; + + // Compute PTC for current epoch + lookahead epochs + let current_epoch = state.current_epoch(); + for e in 0..=(spec.min_seed_lookahead.as_u64()) { + let epoch = current_epoch.safe_add(e)?; + let committee_cache = state.initialize_committee_cache_for_lookahead(epoch, spec)?; + let start_slot = epoch.start_slot(E::slots_per_epoch()); + for i in 0..slots_per_epoch { + let slot = start_slot.safe_add(i as u64)?; + let ptc = state.compute_ptc_with_cache(slot, &committee_cache, spec)?; + let ptc_u64: Vec = ptc.into_iter().map(|v| v as u64).collect(); + let entry = ssz_types::FixedVector::new(ptc_u64).map_err(Error::SszTypesError)?; + window.push(entry); + } + } + + *state.ptc_window_mut()? = Vector::new(window)?; + + Ok(()) +} + /// Applies any pending deposit for builders, effectively onboarding builders at the fork. fn onboard_builders_from_pending_deposits( state: &mut BeaconState, diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index a4b22da3f88..0fd9b7a609e 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -6,9 +6,9 @@ use std::{ use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use typenum::{ - U0, U1, U2, U4, U8, U16, U17, U32, U64, U128, U256, U512, U625, U1024, U2048, U4096, U8192, - U16384, U65536, U131072, U262144, U1048576, U16777216, U33554432, U134217728, U1073741824, - U1099511627776, UInt, Unsigned, bit::B0, + U0, U1, U2, U4, U8, U16, U17, U24, U32, U48, U64, U96, U128, U256, U512, U625, U1024, U2048, + U4096, U8192, U16384, U65536, U131072, U262144, U1048576, U16777216, U33554432, U134217728, + U1073741824, U1099511627776, UInt, Unsigned, bit::B0, }; use crate::core::{ChainSpec, Epoch}; @@ -176,6 +176,7 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + * New in Gloas */ type PTCSize: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type PtcWindowLength: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxPayloadAttestations: Unsigned + Clone + Sync + Send + Debug + PartialEq; type BuilderPendingPaymentsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type BuilderPendingWithdrawalsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; @@ -428,6 +429,11 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + Self::PTCSize::to_usize() } + /// Returns the `PtcWindowLength` constant for this specification. + fn ptc_window_length() -> usize { + Self::PtcWindowLength::to_usize() + } + /// Returns the `MaxPayloadAttestations` constant for this specification. fn max_payload_attestations() -> usize { Self::MaxPayloadAttestations::to_usize() @@ -515,6 +521,7 @@ impl EthSpec for MainnetEthSpec { type MaxWithdrawalRequestsPerPayload = U16; type MaxPendingDepositsPerEpoch = U16; type PTCSize = U512; + type PtcWindowLength = U96; // 3 * SLOTS_PER_EPOCH type MaxPayloadAttestations = U4; type MaxBuildersPerWithdrawalsSweep = U16384; @@ -561,6 +568,7 @@ impl EthSpec for MinimalEthSpec { type ProposerLookaheadSlots = U16; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH type BuilderPendingPaymentsLimit = U16; // 2 * SLOTS_PER_EPOCH = 2 * 8 = 16 type PTCSize = U2; + type PtcWindowLength = U24; // 3 * SLOTS_PER_EPOCH type MaxBuildersPerWithdrawalsSweep = U16; params_from_eth_spec!(MainnetEthSpec { @@ -668,6 +676,7 @@ impl EthSpec for GnosisEthSpec { type ProposerLookaheadSlots = U32; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH type BuilderRegistryLimit = U1099511627776; type PTCSize = U512; + type PtcWindowLength = U48; // 3 * SLOTS_PER_EPOCH type MaxPayloadAttestations = U2; type MaxBuildersPerWithdrawalsSweep = U16384; @@ -694,6 +703,11 @@ mod test { E::proposer_lookahead_slots(), (spec.min_seed_lookahead.as_usize() + 1) * E::slots_per_epoch() as usize ); + assert_eq!( + E::ptc_window_length(), + 3 * E::slots_per_epoch() as usize, + "PtcWindowLength must equal 3 * SLOTS_PER_EPOCH" + ); } #[test] diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index f431055c5fe..a1c5a24651b 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -667,6 +667,11 @@ where #[superstruct(only(Gloas))] pub payload_expected_withdrawals: List, + #[compare_fields(as_iter)] + #[test_random(default)] + #[superstruct(only(Gloas))] + pub ptc_window: Vector, E::PtcWindowLength>, + // Caching (not in the spec) #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] @@ -2431,6 +2436,18 @@ impl BeaconState { CommitteeCache::initialized(self, epoch, spec) } + /// Like [`initialize_committee_cache`](Self::initialize_committee_cache), but allows epochs + /// beyond `current_epoch + 1`. Only checks that the required randao seed is available. + /// + /// Used by PTC window computation which needs shufflings for lookahead epochs. + pub fn initialize_committee_cache_for_lookahead( + &self, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { + CommitteeCache::initialized_for_lookahead(self, epoch, spec) + } + /// Advances the cache for this state into the next epoch. /// /// This should be used if the `slot` of this state is advanced beyond an epoch boundary. @@ -3086,10 +3103,56 @@ impl BeaconState { /// Get the payload timeliness committee for the given `slot`. /// - /// Requires the committee cache to be initialized. - /// TODO(EIP-7732): definitely gonna have to cache this.. + /// Post-Gloas, reads from the cached `ptc_window`. Pre-Gloas, computes on the fly. pub fn get_ptc(&self, slot: Slot, spec: &ChainSpec) -> Result, BeaconStateError> { + if let Ok(ptc_window) = self.ptc_window() { + let epoch = slot.epoch(E::slots_per_epoch()); + let state_epoch = self.current_epoch(); + let slots_per_epoch = E::slots_per_epoch() as usize; + let slot_in_epoch = slot.as_usize() % slots_per_epoch; + + let index = if epoch < state_epoch { + // Previous epoch + if epoch.safe_add(1)? != state_epoch { + return Err(BeaconStateError::SlotOutOfBounds); + } + slot_in_epoch + } else { + // Current epoch or lookahead + let epoch_offset = epoch.as_u64().saturating_sub(state_epoch.as_u64()) as usize; + if epoch_offset > spec.min_seed_lookahead.as_usize() { + return Err(BeaconStateError::SlotOutOfBounds); + } + (epoch_offset + 1) * slots_per_epoch + slot_in_epoch + }; + + let entry = ptc_window + .get(index) + .ok_or(BeaconStateError::SlotOutOfBounds)?; + + // Convert from FixedVector to PTC (FixedVector) + let indices: Vec = entry.iter().map(|&v| v as usize).collect(); + Ok(PTC(FixedVector::new(indices)?)) + } else { + self.compute_ptc(slot, spec) + } + } + + /// Compute the payload timeliness committee for the given `slot` from scratch. + /// + /// Requires the committee cache to be initialized for the slot's epoch. + pub fn compute_ptc(&self, slot: Slot, spec: &ChainSpec) -> Result, BeaconStateError> { let committee_cache = self.committee_cache_at_slot(slot)?; + self.compute_ptc_with_cache(slot, committee_cache, spec) + } + + /// Compute the PTC for a slot using a specific committee cache. + pub fn compute_ptc_with_cache( + &self, + slot: Slot, + committee_cache: &CommitteeCache, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { let committees = committee_cache.get_beacon_committees_at_slot(slot)?; let seed = self.get_ptc_attester_seed(slot, spec)?; diff --git a/consensus/types/src/state/committee_cache.rs b/consensus/types/src/state/committee_cache.rs index 4a28f3c6892..2e74ab760cb 100644 --- a/consensus/types/src/state/committee_cache.rs +++ b/consensus/types/src/state/committee_cache.rs @@ -62,6 +62,9 @@ fn compare_shuffling_positions(xs: &Vec, ys: &Vec( state: &BeaconState, @@ -81,12 +84,44 @@ impl CommitteeCache { || epoch > state .current_epoch() - .safe_add(1) + .safe_add(1u64) .map_err(BeaconStateError::ArithError)? { return Err(BeaconStateError::EpochOutOfBounds); } + Self::initialized_unchecked(state, epoch, spec) + } + + /// Return a new, fully initialized cache for a lookahead epoch. + /// + /// Like [`initialized`](Self::initialized), but allows epochs beyond `current_epoch + 1`. + /// The only bound enforced is that the required randao seed is available in the state. + /// + /// This is used by PTC window computation, which needs committee shufflings for + /// `current_epoch + 1 + MIN_SEED_LOOKAHEAD`. + pub fn initialized_for_lookahead( + state: &BeaconState, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { + let reqd_randao_epoch = epoch + .saturating_sub(spec.min_seed_lookahead) + .saturating_sub(1u64); + + if reqd_randao_epoch < state.min_randao_epoch() { + return Err(BeaconStateError::EpochOutOfBounds); + } + + Self::initialized_unchecked(state, epoch, spec) + } + + /// Core committee cache construction. Callers are responsible for bounds-checking `epoch`. + fn initialized_unchecked( + state: &BeaconState, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { // May cause divide-by-zero errors. if E::slots_per_epoch() == 0 { return Err(BeaconStateError::ZeroSlotsPerEpoch); diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index 7a90fc70d0b..f0991c0106c 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -12,7 +12,7 @@ use state_processing::per_epoch_processing::effective_balance_updates::{ process_effective_balance_updates, process_effective_balance_updates_slow, }; use state_processing::per_epoch_processing::single_pass::{ - SinglePassConfig, process_epoch_single_pass, process_proposer_lookahead, + SinglePassConfig, process_epoch_single_pass, process_proposer_lookahead, process_ptc_window, }; use state_processing::per_epoch_processing::{ altair, base, @@ -80,6 +80,8 @@ pub struct ParticipationFlagUpdates; #[derive(Debug)] pub struct ProposerLookahead; #[derive(Debug)] +pub struct PtcWindow; +#[derive(Debug)] pub struct BuilderPendingPayments; type_name!( @@ -102,6 +104,7 @@ type_name!(SyncCommitteeUpdates, "sync_committee_updates"); type_name!(InactivityUpdates, "inactivity_updates"); type_name!(ParticipationFlagUpdates, "participation_flag_updates"); type_name!(ProposerLookahead, "proposer_lookahead"); +type_name!(PtcWindow, "ptc_window"); type_name!(BuilderPendingPayments, "builder_pending_payments"); impl EpochTransition for JustificationAndFinalization { @@ -296,6 +299,16 @@ impl EpochTransition for ProposerLookahead { } } +impl EpochTransition for PtcWindow { + fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { + if state.fork_name_unchecked().gloas_enabled() { + process_ptc_window(state, spec) + } else { + Ok(()) + } + } +} + impl EpochTransition for BuilderPendingPayments { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { process_epoch_single_pass( @@ -373,7 +386,9 @@ impl> Case for EpochProcessing { return false; } - if !fork_name.gloas_enabled() && T::name() == "builder_pending_payments" { + if !fork_name.gloas_enabled() + && (T::name() == "builder_pending_payments" || T::name() == "ptc_window") + { return false; } diff --git a/testing/ef_tests/src/lib.rs b/testing/ef_tests/src/lib.rs index 94b19b66446..5587bbed413 100644 --- a/testing/ef_tests/src/lib.rs +++ b/testing/ef_tests/src/lib.rs @@ -3,7 +3,7 @@ pub use cases::{ BuilderPendingPayments, Case, EffectiveBalanceUpdates, Eth1DataReset, ExecutionPayloadBidBlock, FeatureName, HistoricalRootsUpdate, HistoricalSummariesUpdate, InactivityUpdates, JustificationAndFinalization, ParticipationFlagUpdates, ParticipationRecordUpdates, - PendingBalanceDeposits, PendingConsolidations, ProposerLookahead, RandaoMixesReset, + PendingBalanceDeposits, PendingConsolidations, ProposerLookahead, PtcWindow, RandaoMixesReset, RegistryUpdates, RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, WithdrawalsPayload, }; diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 3893df2ef74..3254bb6e900 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -960,6 +960,12 @@ fn epoch_processing_proposer_lookahead() { EpochProcessingHandler::::default().run(); } +#[test] +fn epoch_processing_ptc_window() { + EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); +} + #[test] fn epoch_processing_builder_pending_payments() { EpochProcessingHandler::::default().run(); From 8463771c250db84453c9aff15848caf2a04064c3 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 12:39:21 +1100 Subject: [PATCH 03/13] Some cleanup --- .../src/per_epoch_processing/single_pass.rs | 5 ++++- consensus/types/src/state/beacon_state.rs | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 1954a85a00e..606b5f74deb 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -34,6 +34,7 @@ pub struct SinglePassConfig { pub effective_balance_updates: bool, pub proposer_lookahead: bool, pub builder_pending_payments: bool, + pub ptc_window: bool, } impl Default for SinglePassConfig { @@ -54,6 +55,7 @@ impl SinglePassConfig { effective_balance_updates: true, proposer_lookahead: true, builder_pending_payments: true, + ptc_window: true, } } @@ -68,6 +70,7 @@ impl SinglePassConfig { effective_balance_updates: false, proposer_lookahead: false, builder_pending_payments: false, + ptc_window: false, } } } @@ -479,7 +482,7 @@ pub fn process_epoch_single_pass( process_proposer_lookahead(state, spec)?; } - if conf.proposer_lookahead && fork_name.gloas_enabled() { + if conf.ptc_window && fork_name.gloas_enabled() { process_ptc_window(state, spec)?; } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index a1c5a24651b..ae4cc856e3e 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -3109,7 +3109,7 @@ impl BeaconState { let epoch = slot.epoch(E::slots_per_epoch()); let state_epoch = self.current_epoch(); let slots_per_epoch = E::slots_per_epoch() as usize; - let slot_in_epoch = slot.as_usize() % slots_per_epoch; + let slot_in_epoch = slot.as_usize().safe_rem(slots_per_epoch)?; let index = if epoch < state_epoch { // Previous epoch @@ -3123,7 +3123,10 @@ impl BeaconState { if epoch_offset > spec.min_seed_lookahead.as_usize() { return Err(BeaconStateError::SlotOutOfBounds); } - (epoch_offset + 1) * slots_per_epoch + slot_in_epoch + epoch_offset + .safe_add(1)? + .safe_mul(slots_per_epoch)? + .safe_add(slot_in_epoch)? }; let entry = ptc_window From 8865df42623b3c2e3a1f85db8fbbefccabe81ff2 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 13:01:44 +1100 Subject: [PATCH 04/13] Use milhouse types in process_ptc_window --- .../src/per_epoch_processing/errors.rs | 1 - .../src/per_epoch_processing/single_pass.rs | 25 +++++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index b58b29d360f..4818dcbf670 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -32,7 +32,6 @@ pub enum EpochProcessingError { MissingExitBalanceToConsume, PendingDepositsLogicError, ProposerLookaheadOutOfBounds(usize), - PtcWindowOutOfBounds(usize), } impl From for EpochProcessingError { diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 606b5f74deb..c1354513bcc 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -524,12 +524,15 @@ pub fn process_ptc_window( spec: &ChainSpec, ) -> Result<(), Error> { let slots_per_epoch = E::slots_per_epoch() as usize; - let ptc_window_length = E::ptc_window_length(); - let mut window = state.ptc_window()?.clone().to_vec(); - // Shift all epochs forward by one (drop the oldest epoch) - let shifted: Vec<_> = window[slots_per_epoch..].to_vec(); - window[..ptc_window_length.safe_sub(slots_per_epoch)?].clone_from_slice(&shifted); + // Convert Vector -> List to use tree-efficient pop_front. + let ptc_window = state.ptc_window()?.clone(); + let mut window: List<_, E::PtcWindowLength> = List::from(ptc_window); + + // Drop the oldest epoch from the front (reuses shared tree nodes). + window + .pop_front(slots_per_epoch) + .map_err(|e| Error::BeaconStateError(BeaconStateError::MilhouseError(e)))?; // Compute PTC for the new lookahead epoch let next_epoch = state @@ -541,20 +544,20 @@ pub fn process_ptc_window( // Build a committee cache for the lookahead epoch (beyond the normal Next bound) let committee_cache = state.initialize_committee_cache_for_lookahead(next_epoch, spec)?; - let last_epoch_start = ptc_window_length.safe_sub(slots_per_epoch)?; for i in 0..slots_per_epoch { let slot = start_slot.safe_add(i as u64)?; let ptc = state.compute_ptc_with_cache(slot, &committee_cache, spec)?; let ptc_u64: Vec = ptc.into_iter().map(|v| v as u64).collect(); let entry = ssz_types::FixedVector::new(ptc_u64) .map_err(|e| Error::BeaconStateError(BeaconStateError::SszTypesError(e)))?; - let index = last_epoch_start.safe_add(i)?; - *window - .get_mut(index) - .ok_or(Error::PtcWindowOutOfBounds(index))? = entry; + window + .push(entry) + .map_err(|e| Error::BeaconStateError(BeaconStateError::MilhouseError(e)))?; } - *state.ptc_window_mut()? = Vector::new(window)?; + // Convert List back to Vector. + *state.ptc_window_mut()? = Vector::try_from(window) + .map_err(|e| Error::BeaconStateError(BeaconStateError::MilhouseError(e)))?; Ok(()) } From 21ef721b6567108a2e1b98c2dcd4978627a27b92 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 13:36:16 +1100 Subject: [PATCH 05/13] Optimisation to reuse next epoch committee cache --- .../src/per_epoch_processing/altair.rs | 11 ++++-- .../src/per_epoch_processing/single_pass.rs | 36 ++++++++++++++----- consensus/types/src/state/beacon_state.rs | 11 ++++++ .../ef_tests/src/cases/epoch_processing.rs | 2 +- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/consensus/state_processing/src/per_epoch_processing/altair.rs b/consensus/state_processing/src/per_epoch_processing/altair.rs index d9e69647304..683d92d836f 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair.rs @@ -51,8 +51,8 @@ pub fn process_epoch( // without loss of correctness. let current_epoch_progressive_balances = state.progressive_balances_cache().clone(); let current_epoch_total_active_balance = state.get_total_active_balance()?; - let participation_summary = - process_epoch_single_pass(state, spec, SinglePassConfig::default())?; + let epoch_result = process_epoch_single_pass(state, spec, SinglePassConfig::default())?; + let participation_summary = epoch_result.summary; // Reset eth1 data votes. process_eth1_data_reset(state)?; @@ -79,6 +79,13 @@ pub fn process_epoch( // Rotate the epoch caches to suit the epoch transition. state.advance_caches()?; + + // Install the lookahead committee cache (built during PTC window processing) as the Next + // cache. After advance_caches, the lookahead epoch becomes the Next relative epoch. + if let Some(cache) = epoch_result.lookahead_committee_cache { + state.set_committee_cache(RelativeEpoch::Next, cache)?; + } + update_progressive_balances_on_epoch_transition(state, spec)?; Ok(EpochProcessingSummary::Altair { diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index c1354513bcc..976607aa764 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -12,12 +12,13 @@ use milhouse::{Cow, List, Vector}; use safe_arith::{SafeArith, SafeArithIter}; use std::cmp::{max, min}; use std::collections::{BTreeSet, HashMap}; +use std::sync::Arc; use tracing::instrument; use typenum::Unsigned; use types::{ ActivationQueue, BeaconState, BeaconStateError, BuilderPendingPayment, ChainSpec, Checkpoint, - DepositData, Epoch, EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit, - ProgressiveBalancesCache, RelativeEpoch, Validator, + CommitteeCache, DepositData, Epoch, EthSpec, ExitCache, ForkName, ParticipationFlags, + PendingDeposit, ProgressiveBalancesCache, RelativeEpoch, Validator, consts::altair::{ NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, @@ -142,12 +143,20 @@ impl ValidatorInfo { } } +/// Result of single-pass epoch processing. +pub struct SinglePassEpochResult { + pub summary: ParticipationEpochSummary, + /// Committee cache for the lookahead epoch, built during PTC window processing. + /// Can be installed as the Next committee cache after `advance_caches`. + pub lookahead_committee_cache: Option>, +} + #[instrument(skip_all)] pub fn process_epoch_single_pass( state: &mut BeaconState, spec: &ChainSpec, conf: SinglePassConfig, -) -> Result, Error> { +) -> Result, Error> { initialize_epoch_cache(state, spec)?; initialize_progressive_balances_cache(state, spec)?; state.build_exit_cache(spec)?; @@ -482,11 +491,16 @@ pub fn process_epoch_single_pass( process_proposer_lookahead(state, spec)?; } - if conf.ptc_window && fork_name.gloas_enabled() { - process_ptc_window(state, spec)?; - } + let lookahead_committee_cache = if conf.ptc_window && fork_name.gloas_enabled() { + Some(process_ptc_window(state, spec)?) + } else { + None + }; - Ok(summary) + Ok(SinglePassEpochResult { + summary, + lookahead_committee_cache, + }) } // TOOO(EIP-7917): use balances cache @@ -519,10 +533,14 @@ pub fn process_proposer_lookahead( Ok(()) } +/// Process the PTC window, returning the committee cache built for the lookahead epoch. +/// +/// The returned cache can be injected into the state's Next committee cache slot after +/// `advance_caches` is called during the epoch transition, avoiding redundant recomputation. pub fn process_ptc_window( state: &mut BeaconState, spec: &ChainSpec, -) -> Result<(), Error> { +) -> Result, Error> { let slots_per_epoch = E::slots_per_epoch() as usize; // Convert Vector -> List to use tree-efficient pop_front. @@ -559,7 +577,7 @@ pub fn process_ptc_window( *state.ptc_window_mut()? = Vector::try_from(window) .map_err(|e| Error::BeaconStateError(BeaconStateError::MilhouseError(e)))?; - Ok(()) + Ok(committee_cache) } /// Calculate the quorum threshold for builder payments based on total active balance. diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index ae4cc856e3e..e8d5982ead1 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -2518,6 +2518,17 @@ impl BeaconState { .ok_or(BeaconStateError::CommitteeCachesOutOfBounds(index)) } + /// Set the committee cache for the given `relative_epoch` to `cache`. + pub fn set_committee_cache( + &mut self, + relative_epoch: RelativeEpoch, + cache: Arc, + ) -> Result<(), BeaconStateError> { + let i = Self::committee_cache_index(relative_epoch); + *self.committee_cache_at_index_mut(i)? = cache; + Ok(()) + } + /// Returns the cache for some `RelativeEpoch`. Returns an error if the cache has not been /// initialized. pub fn committee_cache( diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index f0991c0106c..a032aa917fe 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -302,7 +302,7 @@ impl EpochTransition for ProposerLookahead { impl EpochTransition for PtcWindow { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { if state.fork_name_unchecked().gloas_enabled() { - process_ptc_window(state, spec) + process_ptc_window(state, spec).map(|_| ()) } else { Ok(()) } From ed8a554a44c6e1fc51c4f49c684245dddec5df27 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 14:17:37 +1100 Subject: [PATCH 06/13] Fix ChainSpec (minimal version of PR #9008) --- consensus/types/src/core/chain_spec.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 8a2b3a23e8c..159c66ceeb0 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -1235,7 +1235,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, - min_builder_withdrawability_delay: Epoch::new(4096), + min_builder_withdrawability_delay: Epoch::new(64), max_request_payloads: 128, /* @@ -1381,6 +1381,7 @@ impl ChainSpec { // Gloas gloas_fork_version: [0x07, 0x00, 0x00, 0x01], gloas_fork_epoch: None, + min_builder_withdrawability_delay: Epoch::new(2), /* * Derived time values (set by `compute_derived_values()`) @@ -1391,6 +1392,9 @@ impl ChainSpec { sync_message_due: Duration::from_millis(1999), contribution_and_proof_due: Duration::from_millis(4000), + // Networking Fulu + blob_schedule: BlobSchedule::default(), + // Other network_id: 2, // lighthouse testnet network id deposit_chain_id: 5, @@ -1631,7 +1635,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, - min_builder_withdrawability_delay: Epoch::new(4096), + min_builder_withdrawability_delay: Epoch::new(64), max_request_payloads: 128, /* @@ -2064,6 +2068,10 @@ pub struct Config { #[serde(default = "default_contribution_due_bps")] #[serde(with = "serde_utils::quoted_u64")] contribution_due_bps: u64, + + #[serde(default = "default_min_builder_withdrawability_delay")] + #[serde(with = "serde_utils::quoted_u64")] + min_builder_withdrawability_delay: u64, } fn default_bellatrix_fork_version() -> [u8; 4] { @@ -2289,6 +2297,10 @@ const fn default_contribution_due_bps() -> u64 { 6667 } +const fn default_min_builder_withdrawability_delay() -> u64 { + 64 +} + fn max_blocks_by_root_request_common(max_request_blocks: u64) -> usize { let max_request_blocks = max_request_blocks as usize; RuntimeVariableList::::new( @@ -2525,6 +2537,8 @@ impl Config { aggregate_due_bps: spec.aggregate_due_bps, sync_message_due_bps: spec.sync_message_due_bps, contribution_due_bps: spec.contribution_due_bps, + + min_builder_withdrawability_delay: spec.min_builder_withdrawability_delay.as_u64(), } } @@ -2616,6 +2630,7 @@ impl Config { aggregate_due_bps, sync_message_due_bps, contribution_due_bps, + min_builder_withdrawability_delay, } = self; if preset_base != E::spec_name().to_string().as_str() { @@ -2705,6 +2720,8 @@ impl Config { sync_message_due_bps, contribution_due_bps, + min_builder_withdrawability_delay: Epoch::new(min_builder_withdrawability_delay), + ..chain_spec.clone() }; Some(spec.compute_derived_values::()) From 41c7814afe991cee8eacb055cc43b61f9cf1be3c Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 14:40:15 +1100 Subject: [PATCH 07/13] Update EF test scaffolding --- testing/ef_tests/check_all_files_accessed.py | 6 +++++- testing/ef_tests/src/cases/operations.rs | 6 +----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index dd6be143067..2daafada31c 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -53,6 +53,8 @@ "tests/.*/gloas/fork_choice/.*", # Ignore MatrixEntry SSZ tests for now. "tests/.*/.*/ssz_static/MatrixEntry/.*", + # TODO: partial data column not implemented yet + "tests/.*/.*/ssz_static/PartialDataColumn.*/.*", # TODO(gloas): Ignore Gloas light client stuff for now "tests/.*/gloas/ssz_static/LightClient.*/.*", # Execution payload header is irrelevant after Gloas, this type will probably be deleted. @@ -73,7 +75,9 @@ "tests/.*/compute_verify_cell_kzg_proof_batch_challenge/.*", "tests/.*/compute_challenge/.*", # We don't need these manifest files at the moment. - "tests/.*/manifest.yaml" + "tests/.*/manifest.yaml", + # TODO: gossip condition tests not implemented yet + "tests/.*/.*/networking/.*" ] diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 798c66b6666..1399815763b 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -717,11 +717,7 @@ impl> LoadCase for Operations { // Check BLS setting here before SSZ deserialization, as most types require signatures // to be valid. let operation_path = path.join(O::filename()); - let (operation, bls_error) = if !operation_path.is_file() { - // Some test cases (e.g. builder_voluntary_exit__success) have no operation file. - // TODO(gloas): remove this once the test vectors are fixed - (None, None) - } else if metadata.bls_setting.unwrap_or_default().check().is_ok() { + let (operation, bls_error) = if metadata.bls_setting.unwrap_or_default().check().is_ok() { match O::decode(&operation_path, fork_name, spec) { Ok(op) => (Some(op), None), Err(Error::InvalidBLSInput(error)) => (None, Some(error)), From d25159e9fd4a4f8fb84dd6766c20810e5ce94b65 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 15:47:57 +1100 Subject: [PATCH 08/13] Update mainnet config.yaml --- .../mainnet/config.yaml | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index 5df6370abe2..051a6b95439 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -56,21 +56,21 @@ ELECTRA_FORK_EPOCH: 364032 # May 7, 2025, 10:05:11am UTC FULU_FORK_VERSION: 0x06000000 FULU_FORK_EPOCH: 411392 # December 3, 2025, 09:49:11pm UTC # Gloas -GLOAS_FORK_VERSION: 0x07000000 # temporary stub +GLOAS_FORK_VERSION: 0x07000000 GLOAS_FORK_EPOCH: 18446744073709551615 -# EIP7441 -EIP7441_FORK_VERSION: 0x08000000 # temporary stub -EIP7441_FORK_EPOCH: 18446744073709551615 -# EIP7805 -EIP7805_FORK_VERSION: 0x0a000000 # temporary stub -EIP7805_FORK_EPOCH: 18446744073709551615 +# Heze +HEZE_FORK_VERSION: 0x08000000 +HEZE_FORK_EPOCH: 18446744073709551615 # EIP7928 -EIP7928_FORK_VERSION: 0x0b000000 # temporary stub +EIP7928_FORK_VERSION: 0xe7928000 # temporary stub EIP7928_FORK_EPOCH: 18446744073709551615 +# EIP8025 +EIP8025_FORK_VERSION: 0xe8025000 # temporary stub +EIP8025_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- -# 12 seconds (*deprecated*) +# 12 seconds SECONDS_PER_SLOT: 12 # 12000 milliseconds SLOT_DURATION_MS: 12000 @@ -96,8 +96,8 @@ SYNC_MESSAGE_DUE_BPS: 3333 CONTRIBUTION_DUE_BPS: 6667 # Gloas -# 2**12 (= 4,096) epochs -MIN_BUILDER_WITHDRAWABILITY_DELAY: 4096 +# 2**6 (= 64) epochs +MIN_BUILDER_WITHDRAWABILITY_DELAY: 64 # 2500 basis points, 25% of SLOT_DURATION_MS ATTESTATION_DUE_BPS_GLOAS: 2500 # 5000 basis points, 50% of SLOT_DURATION_MS @@ -109,7 +109,7 @@ CONTRIBUTION_DUE_BPS_GLOAS: 5000 # 7500 basis points, 75% of SLOT_DURATION_MS PAYLOAD_ATTESTATION_DUE_BPS: 7500 -# EIP7805 +# Heze # 7500 basis points, 75% of SLOT_DURATION_MS VIEW_FREEZE_CUTOFF_BPS: 7500 # 6667 basis points, ~67% of SLOT_DURATION_MS @@ -166,8 +166,6 @@ MAX_PAYLOAD_SIZE: 10485760 MAX_REQUEST_BLOCKS: 1024 # 2**8 (= 256) epochs EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 -# MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2 (= 33,024) epochs -MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 # 2**5 (= 32) slots ATTESTATION_PROPAGATION_SLOT_RANGE: 32 # 500ms @@ -180,8 +178,6 @@ SUBNETS_PER_NODE: 2 ATTESTATION_SUBNET_COUNT: 64 # 0 bits ATTESTATION_SUBNET_EXTRA_BITS: 0 -# ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS (= 6 + 0) bits -ATTESTATION_SUBNET_PREFIX_BITS: 6 # Deneb # 2**7 (= 128) blocks @@ -192,24 +188,18 @@ MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 BLOB_SIDECAR_SUBNET_COUNT: 6 # 6 blobs MAX_BLOBS_PER_BLOCK: 6 -# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK (= 128 * 6) sidecars -MAX_REQUEST_BLOB_SIDECARS: 768 # Electra # 9 subnets BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 # 9 blobs MAX_BLOBS_PER_BLOCK_ELECTRA: 9 -# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA (= 128 * 9) sidecars -MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 # Fulu # 2**7 (= 128) groups NUMBER_OF_CUSTODY_GROUPS: 128 # 2**7 (= 128) subnets DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 -# MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS (= 128 * 128) sidecars -MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 # 2**3 (= 8) samples SAMPLES_PER_SLOT: 8 # 2**2 (= 4) sidecars @@ -225,18 +215,13 @@ MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 # 2**7 (= 128) payloads MAX_REQUEST_PAYLOADS: 128 -# EIP7441 -# 2**8 (= 256) epochs -EPOCHS_PER_SHUFFLING_PHASE: 256 -# 2**1 (= 2) epochs -PROPOSER_SELECTION_GAP: 2 - -# EIP7805 +# Heze # 2**4 (= 16) inclusion lists MAX_REQUEST_INCLUSION_LIST: 16 # 2**13 (= 8,192) bytes MAX_BYTES_PER_INCLUSION_LIST: 8192 + # Blob Scheduling # --------------------------------------------------------------- From 6827ce3b082650a5d672548ea3f863db2912aacf Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 16:21:54 +1100 Subject: [PATCH 09/13] Simplify --- consensus/state_processing/src/upgrade/gloas.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 7fafe7c52ac..1c46719db15 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -4,6 +4,7 @@ use crate::per_block_processing::{ use milhouse::{List, Vector}; use safe_arith::SafeArith; use ssz_types::BitVector; +use ssz_types::FixedVector; use std::collections::HashSet; use std::mem; use typenum::Unsigned; @@ -103,21 +104,11 @@ pub fn upgrade_state_to_gloas( vec![0xFFu8; E::SlotsPerHistoricalRoot::to_usize() / 8].into(), ) .map_err(|_| Error::InvalidBitfield)?, - builder_pending_payments: Vector::new(vec![ - BuilderPendingPayment::default(); - E::builder_pending_payments_limit() - ])?, + builder_pending_payments: Vector::from_elem(BuilderPendingPayment::default())?, builder_pending_withdrawals: List::default(), // Empty list initially, latest_block_hash: pre.latest_execution_payload_header.block_hash, payload_expected_withdrawals: List::default(), - ptc_window: Vector::new(vec![ - ssz_types::FixedVector::new(vec![ - 0u64; - E::PTCSize::to_usize() - ]) - .map_err(Error::SszTypesError)?; - E::PtcWindowLength::to_usize() - ])?, // placeholder, will be initialized below + ptc_window: Vector::from_elem(FixedVector::from_elem(0))?, // placeholder, will be initialized below // Caches total_active_balance: pre.total_active_balance, progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), From f00e44fe913bd5cd2b52affd9ec159df5b0b52fd Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 16:54:02 +1100 Subject: [PATCH 10/13] Simplify initialize_ptc_window --- consensus/state_processing/src/upgrade/gloas.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 1c46719db15..b39ee6048f7 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -135,17 +135,13 @@ fn initialize_ptc_window( spec: &ChainSpec, ) -> Result<(), Error> { let slots_per_epoch = E::slots_per_epoch() as usize; - let ptc_size = E::ptc_size(); - // Empty previous epoch - let empty_entry = - ssz_types::FixedVector::new(vec![0u64; ptc_size]).map_err(Error::SszTypesError)?; - let mut window: Vec> = - vec![empty_entry; slots_per_epoch]; + let empty_previous_epoch = vec![FixedVector::::from_elem(0); slots_per_epoch]; + let mut ptcs = empty_previous_epoch; // Compute PTC for current epoch + lookahead epochs let current_epoch = state.current_epoch(); - for e in 0..=(spec.min_seed_lookahead.as_u64()) { + for e in 0..=spec.min_seed_lookahead.as_u64() { let epoch = current_epoch.safe_add(e)?; let committee_cache = state.initialize_committee_cache_for_lookahead(epoch, spec)?; let start_slot = epoch.start_slot(E::slots_per_epoch()); @@ -153,12 +149,12 @@ fn initialize_ptc_window( let slot = start_slot.safe_add(i as u64)?; let ptc = state.compute_ptc_with_cache(slot, &committee_cache, spec)?; let ptc_u64: Vec = ptc.into_iter().map(|v| v as u64).collect(); - let entry = ssz_types::FixedVector::new(ptc_u64).map_err(Error::SszTypesError)?; - window.push(entry); + let entry = FixedVector::new(ptc_u64)?; + ptcs.push(entry); } } - *state.ptc_window_mut()? = Vector::new(window)?; + *state.ptc_window_mut()? = Vector::new(ptcs)?; Ok(()) } From 6c4b6370e43a797de130eeb404241b9488d3a25a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 17:16:30 +1100 Subject: [PATCH 11/13] Clean up PTC window length checks --- consensus/types/src/core/eth_spec.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index 0fd9b7a609e..36d61fbbf9b 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -521,7 +521,7 @@ impl EthSpec for MainnetEthSpec { type MaxWithdrawalRequestsPerPayload = U16; type MaxPendingDepositsPerEpoch = U16; type PTCSize = U512; - type PtcWindowLength = U96; // 3 * SLOTS_PER_EPOCH + type PtcWindowLength = U96; // (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH type MaxPayloadAttestations = U4; type MaxBuildersPerWithdrawalsSweep = U16384; @@ -568,7 +568,7 @@ impl EthSpec for MinimalEthSpec { type ProposerLookaheadSlots = U16; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH type BuilderPendingPaymentsLimit = U16; // 2 * SLOTS_PER_EPOCH = 2 * 8 = 16 type PTCSize = U2; - type PtcWindowLength = U24; // 3 * SLOTS_PER_EPOCH + type PtcWindowLength = U24; // (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH type MaxBuildersPerWithdrawalsSweep = U16; params_from_eth_spec!(MainnetEthSpec { @@ -676,7 +676,7 @@ impl EthSpec for GnosisEthSpec { type ProposerLookaheadSlots = U32; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH type BuilderRegistryLimit = U1099511627776; type PTCSize = U512; - type PtcWindowLength = U48; // 3 * SLOTS_PER_EPOCH + type PtcWindowLength = U48; // (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH type MaxPayloadAttestations = U2; type MaxBuildersPerWithdrawalsSweep = U16384; @@ -705,8 +705,8 @@ mod test { ); assert_eq!( E::ptc_window_length(), - 3 * E::slots_per_epoch() as usize, - "PtcWindowLength must equal 3 * SLOTS_PER_EPOCH" + (spec.min_seed_lookahead.as_usize() + 2) * E::slots_per_epoch() as usize, + "PtcWindowLength must equal (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH" ); } From f9ca8e384aa6007dc39da49f618fab5abff972f6 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Mar 2026 17:35:32 +1100 Subject: [PATCH 12/13] Simplify get_ptc --- consensus/types/src/state/beacon_state.rs | 62 ++++++++++------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index e8d5982ead1..a033272b9d9 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -3112,44 +3112,38 @@ impl BeaconState { } } - /// Get the payload timeliness committee for the given `slot`. - /// - /// Post-Gloas, reads from the cached `ptc_window`. Pre-Gloas, computes on the fly. + /// Get the payload timeliness committee for the given `slot` from the `ptc_window`. pub fn get_ptc(&self, slot: Slot, spec: &ChainSpec) -> Result, BeaconStateError> { - if let Ok(ptc_window) = self.ptc_window() { - let epoch = slot.epoch(E::slots_per_epoch()); - let state_epoch = self.current_epoch(); - let slots_per_epoch = E::slots_per_epoch() as usize; - let slot_in_epoch = slot.as_usize().safe_rem(slots_per_epoch)?; + let ptc_window = self.ptc_window()?; + let epoch = slot.epoch(E::slots_per_epoch()); + let state_epoch = self.current_epoch(); + let slots_per_epoch = E::slots_per_epoch() as usize; + let slot_in_epoch = slot.as_usize().safe_rem(slots_per_epoch)?; - let index = if epoch < state_epoch { - // Previous epoch - if epoch.safe_add(1)? != state_epoch { - return Err(BeaconStateError::SlotOutOfBounds); - } - slot_in_epoch - } else { - // Current epoch or lookahead - let epoch_offset = epoch.as_u64().saturating_sub(state_epoch.as_u64()) as usize; - if epoch_offset > spec.min_seed_lookahead.as_usize() { - return Err(BeaconStateError::SlotOutOfBounds); - } - epoch_offset - .safe_add(1)? - .safe_mul(slots_per_epoch)? - .safe_add(slot_in_epoch)? - }; + let index = if epoch < state_epoch { + if epoch.safe_add(1)? != state_epoch { + return Err(BeaconStateError::SlotOutOfBounds); + } + slot_in_epoch + } else { + if epoch > state_epoch.safe_add(spec.min_seed_lookahead)? { + return Err(BeaconStateError::SlotOutOfBounds); + } + let offset = epoch + .safe_sub(state_epoch)? + .safe_add(1)? + .as_usize() + .safe_mul(slots_per_epoch)?; + offset.safe_add(slot_in_epoch)? + }; - let entry = ptc_window - .get(index) - .ok_or(BeaconStateError::SlotOutOfBounds)?; + let entry = ptc_window + .get(index) + .ok_or(BeaconStateError::SlotOutOfBounds)?; - // Convert from FixedVector to PTC (FixedVector) - let indices: Vec = entry.iter().map(|&v| v as usize).collect(); - Ok(PTC(FixedVector::new(indices)?)) - } else { - self.compute_ptc(slot, spec) - } + // Convert from FixedVector to PTC (FixedVector) + let indices: Vec = entry.iter().map(|&v| v as usize).collect(); + Ok(PTC(FixedVector::new(indices)?)) } /// Compute the payload timeliness committee for the given `slot` from scratch. From afcf22bae734acf666a050fe9a2714414bba5bc9 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 31 Mar 2026 12:29:23 +1100 Subject: [PATCH 13/13] Revert dodgy merge --- .../built_in_network_configs/mainnet/config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index 051a6b95439..02bf37cb551 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -64,9 +64,6 @@ HEZE_FORK_EPOCH: 18446744073709551615 # EIP7928 EIP7928_FORK_VERSION: 0xe7928000 # temporary stub EIP7928_FORK_EPOCH: 18446744073709551615 -# EIP8025 -EIP8025_FORK_VERSION: 0xe8025000 # temporary stub -EIP8025_FORK_EPOCH: 18446744073709551615 # Time parameters # ---------------------------------------------------------------