Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Loading