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 1584e932bdf..db4d02e088b 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -13,8 +13,8 @@ use std::cmp::{max, min}; use std::collections::{BTreeSet, HashMap}; use tracing::instrument; use types::{ - ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, DepositData, Epoch, - EthSpec, ExitCache, ForkName, List, ParticipationFlags, PendingDeposit, + ActivationQueue, BeaconState, BeaconStateError, BuilderPendingPayment, ChainSpec, Checkpoint, + DepositData, Epoch, EthSpec, ExitCache, ForkName, List, ParticipationFlags, PendingDeposit, ProgressiveBalancesCache, RelativeEpoch, Unsigned, Validator, Vector, consts::altair::{ NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, @@ -32,6 +32,7 @@ pub struct SinglePassConfig { pub pending_consolidations: bool, pub effective_balance_updates: bool, pub proposer_lookahead: bool, + pub builder_pending_payments: bool, } impl Default for SinglePassConfig { @@ -51,6 +52,7 @@ impl SinglePassConfig { pending_consolidations: true, effective_balance_updates: true, proposer_lookahead: true, + builder_pending_payments: true, } } @@ -64,6 +66,7 @@ impl SinglePassConfig { pending_consolidations: false, effective_balance_updates: false, proposer_lookahead: false, + builder_pending_payments: false, } } } @@ -454,6 +457,12 @@ pub fn process_epoch_single_pass( )?; } + // Process builder pending payments outside the single-pass loop, as they depend on balances for multiple + // validators and cannot be computed accurately inside the loop. + if fork_name.gloas_enabled() && conf.builder_pending_payments { + process_builder_pending_payments(state, state_ctxt, spec)?; + } + // Finally, finish updating effective balance caches. We need this to happen *after* processing // of pending consolidations, which recomputes some effective balances. if conf.effective_balance_updates { @@ -472,7 +481,7 @@ pub fn process_epoch_single_pass( Ok(summary) } -// TOOO(EIP-7917): use balances cache +// TODO(EIP-7917): use balances cache pub fn process_proposer_lookahead( state: &mut BeaconState, spec: &ChainSpec, @@ -502,6 +511,68 @@ pub fn process_proposer_lookahead( Ok(()) } +/// Calculate the quorum threshold for builder payments based on total active balance. +fn get_builder_payment_quorum_threshold( + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result { + let per_slot_balance = state_ctxt + .total_active_balance + .safe_div(E::slots_per_epoch())?; + let quorum = per_slot_balance.safe_mul(spec.builder_payment_threshold_numerator)?; + quorum + .safe_div(spec.builder_payment_threshold_denominator) + .map_err(Error::from) +} + +/// Process builder pending payments, moving qualifying payments to withdrawals. +/// TODO(EIP-7732): Add EF consensus-spec tests for `process_builder_pending_payments` +/// Currently blocked by EF consensus-spec-tests for Gloas not yet integrated. +fn process_builder_pending_payments( + state: &mut BeaconState, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + let quorum = get_builder_payment_quorum_threshold::(state_ctxt, spec)?; + + // Collect qualifying payments + let qualifying_payments = state + .builder_pending_payments()? + .iter() + .take(E::slots_per_epoch() as usize) + .filter(|payment| payment.weight > quorum) + .cloned() + .collect::>(); + + // Update `builder_pending_withdrawals` with qualifying `builder_pending_payments` + qualifying_payments + .into_iter() + .try_for_each(|payment| -> Result<(), Error> { + let exit_queue_epoch = + state.compute_exit_epoch_and_update_churn(payment.withdrawal.amount, spec)?; + let withdrawable_epoch = + exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; + + let mut withdrawal = payment.withdrawal.clone(); + withdrawal.withdrawable_epoch = withdrawable_epoch; + state.builder_pending_withdrawals_mut()?.push(withdrawal)?; + Ok(()) + })?; + + // Move remaining `builder_pending_payments` to start of list and set the rest to default + let new_payments = state + .builder_pending_payments()? + .iter() + .skip(E::slots_per_epoch() as usize) + .cloned() + .chain((0..E::slots_per_epoch() as usize).map(|_| BuilderPendingPayment::default())) + .collect::>(); + + *state.builder_pending_payments_mut()? = Vector::new(new_payments)?; + + Ok(()) +} + fn process_single_inactivity_update( inactivity_score: &mut Cow, validator_info: &ValidatorInfo, diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index 8695054e1e7..10ce216c3c5 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -13,6 +13,7 @@ pub enum Error { EpochProcessingError(EpochProcessingError), ArithError(ArithError), InconsistentStateFork(InconsistentFork), + BitfieldError(ssz::BitfieldError), } impl From for Error { @@ -21,6 +22,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: ssz::BitfieldError) -> Self { + Self::BitfieldError(e) + } +} + /// Advances a state forward by one slot, performing per-epoch processing if required. /// /// If the root of the supplied `state` is known, then it can be passed as `state_root`. If @@ -49,6 +56,18 @@ pub fn per_slot_processing( state.slot_mut().safe_add_assign(1)?; + // Unset the next payload availability + if state.fork_name_unchecked().gloas_enabled() { + let next_slot_index = state + .slot() + .as_usize() + .safe_add(1)? + .safe_rem(E::slots_per_historical_root())?; + state + .execution_payload_availability_mut()? + .set(next_slot_index, false)?; + } + // Process fork upgrades here. Note that multiple upgrades can potentially run // in sequence if they are scheduled in the same Epoch (common in testnets) if state.slot().safe_rem(E::slots_per_epoch())? == 0 {