Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion consensus/state_processing/src/per_block_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ pub fn per_block_processing<E: EthSpec, Payload: AbstractExecPayload<E>>(
let body = block.body();
if state.fork_name_unchecked().gloas_enabled() {
withdrawals::gloas::process_withdrawals::<E>(state, spec)?;
// TODO(EIP-7732): process execution payload bid
process_execution_payload_bid(state, block, verify_signatures, spec)?;
} else {
if state.fork_name_unchecked().capella_enabled() {
withdrawals::capella_electra::process_withdrawals::<E, Payload>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ where
self.include_exits(block)?;
self.include_sync_aggregate(block)?;
self.include_bls_to_execution_changes(block)?;
self.include_execution_payload_bid(block)?;

Ok(())
}
Expand Down Expand Up @@ -357,6 +358,27 @@ where
Ok(())
}

/// Include the signature of the block's execution payload bid.
pub fn include_execution_payload_bid<Payload: AbstractExecPayload<E>>(
&mut self,
block: &'a SignedBeaconBlock<E, Payload>,
) -> Result<()> {
if let Ok(signed_execution_payload_bid) =
block.message().body().signed_execution_payload_bid()
{
// TODO(gloas): if we implement a global builder pubkey cache we need to inject it here
if let Some(signature_set) = execution_payload_bid_signature_set(
self.state,
|builder_index| get_builder_pubkey_from_state(self.state, builder_index),
signed_execution_payload_bid,
self.spec,
)? {
self.sets.push(signature_set);
}
}
Ok(())
}

/// Verify all the signatures that have been included in `self`, returning `true` if and only if
/// all the signatures are valid.
///
Expand Down
67 changes: 64 additions & 3 deletions consensus/state_processing/src/per_epoch_processing/single_pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ use std::collections::{BTreeSet, HashMap};
use tracing::instrument;
use typenum::Unsigned;
use types::{
ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, DepositData, Epoch,
EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit, ProgressiveBalancesCache,
RelativeEpoch, Validator,
ActivationQueue, BeaconState, BeaconStateError, BuilderPendingPayment, ChainSpec, Checkpoint,
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,
Expand All @@ -33,6 +33,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 {
Expand All @@ -52,6 +53,7 @@ impl SinglePassConfig {
pending_consolidations: true,
effective_balance_updates: true,
proposer_lookahead: true,
builder_pending_payments: true,
}
}

Expand All @@ -65,6 +67,7 @@ impl SinglePassConfig {
pending_consolidations: false,
effective_balance_updates: false,
proposer_lookahead: false,
builder_pending_payments: false,
}
}
}
Expand Down Expand Up @@ -455,6 +458,12 @@ pub fn process_epoch_single_pass<E: EthSpec>(
)?;
}

// 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 {
Expand Down Expand Up @@ -503,6 +512,58 @@ pub fn process_proposer_lookahead<E: EthSpec>(
Ok(())
}

/// Calculate the quorum threshold for builder payments based on total active balance.
fn get_builder_payment_quorum_threshold<E: EthSpec>(
state_ctxt: &StateContext,
spec: &ChainSpec,
) -> Result<u64, Error> {
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)
}

/// Processes the builder pending payments from the previous epoch.
fn process_builder_pending_payments<E: EthSpec>(
state: &mut BeaconState<E>,
state_ctxt: &StateContext,
spec: &ChainSpec,
) -> Result<(), Error> {
let quorum = get_builder_payment_quorum_threshold::<E>(state_ctxt, spec)?;

// Collect qualifying payments and append to `builder_pending_withdrawals`.
// We use this pattern rather than a loop to avoid multiple borrows of the state's fields.
let new_pending_builder_withdrawals = state
.builder_pending_payments()?
.iter()
.take(E::SlotsPerEpoch::to_usize())
.filter(|payment| payment.weight >= quorum)
.map(|payment| payment.withdrawal.clone())
.collect::<Vec<_>>();
for payment_withdrawal in new_pending_builder_withdrawals {
state
.builder_pending_withdrawals_mut()?
.push(payment_withdrawal)?;
}

// NOTE: this could be a little more memory-efficient with some juggling to reuse parts
// of the persistent tree (could convert to list, use pop_front, convert back).
Copy link
Collaborator

@dapplion dapplion Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The builder_pending_payments vector is tiny, doesn't matter much

let updated_payments = state
.builder_pending_payments()?
.iter()
.skip(E::SlotsPerEpoch::to_usize())
.cloned()
.chain((0..E::SlotsPerEpoch::to_usize()).map(|_| BuilderPendingPayment::default()))
.collect::<Vec<_>>();

*state.builder_pending_payments_mut()? = Vector::new(updated_payments)?;

Ok(())
}

fn process_single_inactivity_update(
inactivity_score: &mut Cow<u64>,
validator_info: &ValidatorInfo,
Expand Down
19 changes: 19 additions & 0 deletions consensus/state_processing/src/per_slot_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub enum Error {
EpochProcessingError(EpochProcessingError),
ArithError(ArithError),
InconsistentStateFork(InconsistentFork),
BitfieldError(ssz::BitfieldError),
}

impl From<ArithError> for Error {
Expand All @@ -22,6 +23,12 @@ impl From<ArithError> for Error {
}
}

impl From<ssz::BitfieldError> 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
Expand All @@ -48,6 +55,18 @@ pub fn per_slot_processing<E: EthSpec>(
None
};

// 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)?;
}

state.slot_mut().safe_add_assign(1)?;

// Process fork upgrades here. Note that multiple upgrades can potentially run
Expand Down
8 changes: 1 addition & 7 deletions testing/ef_tests/check_all_files_accessed.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,9 @@
"tests/.*/eip7805",
# TODO(gloas): remove these ignores as more Gloas operations are implemented
"tests/.*/gloas/operations/payload_attestation/.*",
# TODO(EIP-7732): remove these ignores as Gloas consensus is implemented
"tests/.*/gloas/epoch_processing/.*",
"tests/.*/gloas/finality/.*",
# TODO(gloas): remove these ignores as Gloas consensus is implemented
"tests/.*/gloas/fork/.*",
"tests/.*/gloas/fork_choice/.*",
"tests/.*/gloas/networking/.*",
"tests/.*/gloas/rewards/.*",
"tests/.*/gloas/sanity/.*",
"tests/.*/gloas/transition/.*",
# Ignore MatrixEntry SSZ tests for now.
"tests/.*/.*/ssz_static/MatrixEntry/.*",
# TODO(gloas): Ignore Gloas light client stuff for now
Expand Down
21 changes: 21 additions & 0 deletions testing/ef_tests/src/cases/epoch_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ pub struct InactivityUpdates;
pub struct ParticipationFlagUpdates;
#[derive(Debug)]
pub struct ProposerLookahead;
#[derive(Debug)]
pub struct BuilderPendingPayments;

type_name!(
JustificationAndFinalization,
Expand All @@ -100,6 +102,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!(BuilderPendingPayments, "builder_pending_payments");

impl<E: EthSpec> EpochTransition<E> for JustificationAndFinalization {
fn run(state: &mut BeaconState<E>, spec: &ChainSpec) -> Result<(), EpochProcessingError> {
Expand Down Expand Up @@ -293,6 +296,20 @@ impl<E: EthSpec> EpochTransition<E> for ProposerLookahead {
}
}

impl<E: EthSpec> EpochTransition<E> for BuilderPendingPayments {
fn run(state: &mut BeaconState<E>, spec: &ChainSpec) -> Result<(), EpochProcessingError> {
process_epoch_single_pass(
state,
spec,
SinglePassConfig {
builder_pending_payments: true,
..SinglePassConfig::disable_all()
},
)
.map(|_| ())
}
}

impl<E: EthSpec, T: EpochTransition<E>> LoadCase for EpochProcessing<E, T> {
fn load_from_dir(path: &Path, fork_name: ForkName) -> Result<Self, Error> {
let spec = &testing_spec::<E>(fork_name);
Expand Down Expand Up @@ -356,6 +373,10 @@ impl<E: EthSpec, T: EpochTransition<E>> Case for EpochProcessing<E, T> {
return false;
}

if !fork_name.gloas_enabled() && T::name() == "builder_pending_payments" {
return false;
}

true
}

Expand Down
Loading