Extract consensus changes from gloas-envelope-processing

This commit is contained in:
Michael Sproul
2026-01-19 15:59:36 +11:00
parent 58b153cac5
commit 2d96b3f193
30 changed files with 1624 additions and 148 deletions

View File

@@ -0,0 +1,45 @@
use crate::per_block_processing::errors::{
BlockOperationError, PayloadAttestationInvalid as Invalid,
};
use ssz_types::VariableList;
use typenum::Unsigned;
use types::*;
pub fn get_indexed_payload_attestation<E: EthSpec>(
state: &BeaconState<E>,
slot: Slot,
payload_attestation: &PayloadAttestation<E>,
spec: &ChainSpec,
) -> Result<IndexedPayloadAttestation<E>, BlockOperationError<Invalid>> {
let attesting_indices = get_payload_attesting_indices(state, slot, payload_attestation, spec)?;
Ok(IndexedPayloadAttestation {
attesting_indices: VariableList::new(attesting_indices)?,
data: payload_attestation.data.clone(),
signature: payload_attestation.signature.clone(),
})
}
pub fn get_payload_attesting_indices<E: EthSpec>(
state: &BeaconState<E>,
slot: Slot,
payload_attestation: &PayloadAttestation<E>,
spec: &ChainSpec,
) -> Result<Vec<u64>, BeaconStateError> {
let ptc = state.get_ptc(slot, spec)?;
let bitlist = &payload_attestation.aggregation_bits;
if bitlist.len() != E::PTCSize::to_usize() {
return Err(BeaconStateError::InvalidBitfield);
}
let mut attesting_indices = Vec::<u64>::new();
for (i, index) in ptc.into_iter().enumerate() {
if let Ok(true) = bitlist.get(i) {
attesting_indices.push(index as u64);
}
}
attesting_indices.sort_unstable();
Ok(attesting_indices)
}

View File

@@ -1,6 +1,7 @@
mod deposit_data_tree;
mod get_attestation_participation;
mod get_attesting_indices;
mod get_payload_attesting_indices;
mod initiate_validator_exit;
mod slash_validator;
@@ -13,6 +14,9 @@ pub use get_attestation_participation::get_attestation_participation_flag_indice
pub use get_attesting_indices::{
attesting_indices_base, attesting_indices_electra, get_attesting_indices_from_state,
};
pub use get_payload_attesting_indices::{
get_indexed_payload_attestation, get_payload_attesting_indices,
};
pub use initiate_validator_exit::initiate_validator_exit;
pub use slash_validator::slash_validator;

View File

@@ -1,11 +1,16 @@
use crate::EpochCacheError;
use crate::common::{attesting_indices_base, attesting_indices_electra};
use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError};
use crate::common::{
attesting_indices_base, attesting_indices_electra, get_indexed_payload_attestation,
};
use crate::per_block_processing::errors::{
AttestationInvalid, BlockOperationError, PayloadAttestationInvalid,
};
use std::collections::{HashMap, hash_map::Entry};
use tree_hash::TreeHash;
use types::{
AbstractExecPayload, AttestationRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec,
Hash256, IndexedAttestation, IndexedAttestationRef, SignedBeaconBlock, Slot,
Hash256, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation,
PayloadAttestation, SignedBeaconBlock, Slot,
};
#[derive(Debug, PartialEq, Clone)]
@@ -22,6 +27,8 @@ pub struct ConsensusContext<E: EthSpec> {
pub current_block_root: Option<Hash256>,
/// Cache of indexed attestations constructed during block processing.
pub indexed_attestations: HashMap<Hash256, IndexedAttestation<E>>,
/// Cache of indexed payload attestations constructed during block processing.
pub indexed_payload_attestations: HashMap<Hash256, IndexedPayloadAttestation<E>>,
}
#[derive(Debug, PartialEq, Clone)]
@@ -55,6 +62,7 @@ impl<E: EthSpec> ConsensusContext<E> {
proposer_index: None,
current_block_root: None,
indexed_attestations: HashMap::new(),
indexed_payload_attestations: HashMap::new(),
}
}
@@ -177,6 +185,25 @@ impl<E: EthSpec> ConsensusContext<E> {
.map(|indexed_attestation| (*indexed_attestation).to_ref())
}
pub fn get_indexed_payload_attestation<'a>(
&'a mut self,
state: &BeaconState<E>,
slot: Slot,
payload_attestation: &'a PayloadAttestation<E>,
spec: &ChainSpec,
) -> Result<&'a IndexedPayloadAttestation<E>, BlockOperationError<PayloadAttestationInvalid>>
{
let key = payload_attestation.tree_hash_root();
match self.indexed_payload_attestations.entry(key) {
Entry::Occupied(occupied) => Ok(occupied.into_mut()),
Entry::Vacant(vacant) => {
let indexed_payload_attestation =
get_indexed_payload_attestation(state, slot, payload_attestation, spec)?;
Ok(vacant.insert(indexed_payload_attestation))
}
}
}
pub fn num_cached_indexed_attestations(&self) -> usize {
self.indexed_attestations.len()
}

View File

@@ -0,0 +1,284 @@
use crate::BlockProcessingError;
use crate::VerifySignatures;
use crate::per_block_processing::compute_timestamp_at_slot;
use crate::per_block_processing::process_operations::{
process_consolidation_requests, process_deposit_requests, process_withdrawal_requests,
};
use safe_arith::{ArithError, SafeArith};
use tree_hash::TreeHash;
use types::{
BeaconState, BeaconStateError, BuilderPendingPayment, ChainSpec, EthSpec, ExecutionBlockHash,
Hash256, SignedExecutionPayloadEnvelope, Slot,
};
// TODO(EIP-7732): don't use this redefinition..
macro_rules! envelope_verify {
($condition: expr, $result: expr) => {
if !$condition {
return Err($result);
}
};
}
#[derive(Debug, Clone)]
pub enum EnvelopeProcessingError {
/// Bad Signature
BadSignature,
BeaconStateError(BeaconStateError),
BlockProcessingError(BlockProcessingError),
ArithError(ArithError),
/// Envelope doesn't match latest beacon block header
LatestBlockHeaderMismatch {
envelope_root: Hash256,
block_header_root: Hash256,
},
/// Envelope doesn't match latest beacon block slot
SlotMismatch {
envelope_slot: Slot,
parent_state_slot: Slot,
},
/// The withdrawals root doesn't match the state's latest withdrawals root
WithdrawalsRootMismatch {
state: Hash256,
envelope: Hash256,
},
// The gas limit doesn't match the committed bid
GasLimitMismatch {
committed_bid: u64,
envelope: u64,
},
// The block hash doesn't match the committed bid
BlockHashMismatch {
committed_bid: ExecutionBlockHash,
envelope: ExecutionBlockHash,
},
// The parent hash doesn't match the previous execution payload
ParentHashMismatch {
state: ExecutionBlockHash,
envelope: ExecutionBlockHash,
},
/// The blob KZG commitments root doesn't match the committed bid
BlobKzgCommitmentsRootMismatch {
committed_bid: Hash256,
envelope: Hash256,
},
// The previous randao didn't match the payload
PrevRandaoMismatch {
state: Hash256,
envelope: Hash256,
},
// The timestamp didn't match the payload
TimestampMismatch {
state: u64,
envelope: u64,
},
// Blob committments exceeded the maximum
BlobLimitExceeded {
max: usize,
envelope: usize,
},
// Invalid state root
InvalidStateRoot {
state: Hash256,
envelope: Hash256,
},
// BitFieldError
BitFieldError(ssz::BitfieldError),
// Some kind of error calculating the builder payment index
BuilderPaymentIndexOutOfBounds(usize),
}
impl From<BeaconStateError> for EnvelopeProcessingError {
fn from(e: BeaconStateError) -> Self {
EnvelopeProcessingError::BeaconStateError(e)
}
}
impl From<BlockProcessingError> for EnvelopeProcessingError {
fn from(e: BlockProcessingError) -> Self {
EnvelopeProcessingError::BlockProcessingError(e)
}
}
impl From<ArithError> for EnvelopeProcessingError {
fn from(e: ArithError) -> Self {
EnvelopeProcessingError::ArithError(e)
}
}
/// Processes a `SignedExecutionPayloadEnvelope`
///
/// This function does all the state modifications inside `process_execution_payload()`
pub fn envelope_processing<E: EthSpec>(
state: &mut BeaconState<E>,
parent_state_root: Option<Hash256>,
signed_envelope: &SignedExecutionPayloadEnvelope<E>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), EnvelopeProcessingError> {
if verify_signatures.is_true() {
// Verify Signed Envelope Signature
// TODO(EIP-7732): there is probably a more efficient way to do this..
if !signed_envelope.verify_signature_with_state(state, spec)? {
return Err(EnvelopeProcessingError::BadSignature);
}
}
let envelope = &signed_envelope.message;
let payload = &envelope.payload;
let execution_requests = &envelope.execution_requests;
// Cache latest block header state root
if state.latest_block_header().state_root == Hash256::default() {
let previous_state_root = parent_state_root
.map(Ok)
.unwrap_or_else(|| state.canonical_root())?;
state.latest_block_header_mut().state_root = previous_state_root;
}
// Verify consistency with the beacon block
envelope_verify!(
envelope.beacon_block_root == state.latest_block_header().tree_hash_root(),
EnvelopeProcessingError::LatestBlockHeaderMismatch {
envelope_root: envelope.beacon_block_root,
block_header_root: state.latest_block_header().tree_hash_root(),
}
);
envelope_verify!(
envelope.slot == state.slot(),
EnvelopeProcessingError::SlotMismatch {
envelope_slot: envelope.slot,
parent_state_slot: state.slot(),
}
);
// Verify consistency with the committed bid
let committed_bid = state.latest_execution_payload_bid()?;
// builder index match already verified
if committed_bid.blob_kzg_commitments_root != envelope.blob_kzg_commitments.tree_hash_root() {
return Err(EnvelopeProcessingError::BlobKzgCommitmentsRootMismatch {
committed_bid: committed_bid.blob_kzg_commitments_root,
envelope: envelope.blob_kzg_commitments.tree_hash_root(),
});
};
// Verify the withdrawals root
envelope_verify!(
payload.withdrawals.tree_hash_root() == *state.latest_withdrawals_root()?,
EnvelopeProcessingError::WithdrawalsRootMismatch {
state: *state.latest_withdrawals_root()?,
envelope: payload.withdrawals.tree_hash_root(),
}
);
// Verify the gas limit
envelope_verify!(
payload.gas_limit == committed_bid.gas_limit,
EnvelopeProcessingError::GasLimitMismatch {
committed_bid: committed_bid.gas_limit,
envelope: payload.gas_limit,
}
);
// Verify the block hash
envelope_verify!(
committed_bid.block_hash == payload.block_hash,
EnvelopeProcessingError::BlockHashMismatch {
committed_bid: committed_bid.block_hash,
envelope: payload.block_hash,
}
);
// Verify consistency of the parent hash with respect to the previous execution payload
envelope_verify!(
payload.parent_hash == *state.latest_block_hash()?,
EnvelopeProcessingError::ParentHashMismatch {
state: *state.latest_block_hash()?,
envelope: payload.parent_hash,
}
);
// Verify prev_randao
envelope_verify!(
payload.prev_randao == *state.get_randao_mix(state.current_epoch())?,
EnvelopeProcessingError::PrevRandaoMismatch {
state: *state.get_randao_mix(state.current_epoch())?,
envelope: payload.prev_randao,
}
);
// Verify the timestamp
let state_timestamp = compute_timestamp_at_slot(state, state.slot(), spec)?;
envelope_verify!(
payload.timestamp == state_timestamp,
EnvelopeProcessingError::TimestampMismatch {
state: state_timestamp,
envelope: payload.timestamp,
}
);
// Verify the commitments are under limit
let max_blobs = spec.max_blobs_per_block(state.current_epoch()) as usize;
envelope_verify!(
envelope.blob_kzg_commitments.len() <= max_blobs,
EnvelopeProcessingError::BlobLimitExceeded {
max: max_blobs,
envelope: envelope.blob_kzg_commitments.len(),
}
);
// process electra operations
process_deposit_requests(state, &execution_requests.deposits, spec)?;
process_withdrawal_requests(state, &execution_requests.withdrawals, spec)?;
process_consolidation_requests(state, &execution_requests.consolidations, spec)?;
// queue the builder payment
let payment_index = E::slots_per_epoch()
.safe_add(state.slot().as_u64().safe_rem(E::slots_per_epoch())?)?
as usize;
let mut payment = state
.builder_pending_payments()?
.get(payment_index)
.ok_or(EnvelopeProcessingError::BuilderPaymentIndexOutOfBounds(
payment_index,
))?
.clone();
let amount = payment.withdrawal.amount;
if amount > 0 {
let exit_queue_epoch = state.compute_exit_epoch_and_update_churn(amount, spec)?;
payment.withdrawal.withdrawable_epoch =
exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?;
state
.builder_pending_withdrawals_mut()?
.push(payment.withdrawal)
.map_err(|e| EnvelopeProcessingError::BeaconStateError(e.into()))?;
}
*state
.builder_pending_payments_mut()?
.get_mut(payment_index)
.ok_or(EnvelopeProcessingError::BuilderPaymentIndexOutOfBounds(
payment_index,
))? = BuilderPendingPayment::default();
// cache the execution payload hash
let availability_index = state
.slot()
.safe_rem(E::slots_per_historical_root() as u64)?
.as_usize();
state
.execution_payload_availability_mut()?
.set(availability_index, true)
.map_err(EnvelopeProcessingError::BitFieldError)?;
*state.latest_block_hash_mut()? = payload.block_hash;
// verify the state root
envelope_verify!(
envelope.state_root == state.canonical_root()?,
EnvelopeProcessingError::InvalidStateRoot {
state: state.canonical_root()?,
envelope: envelope.state_root,
}
);
Ok(())
}

View File

@@ -20,6 +20,7 @@ pub mod all_caches;
pub mod block_replayer;
pub mod common;
pub mod consensus_context;
pub mod envelope_processing;
pub mod epoch_cache;
pub mod genesis;
pub mod per_block_processing;

View File

@@ -1,8 +1,12 @@
use self::errors::ExecutionPayloadBidInvalid;
use crate::consensus_context::ConsensusContext;
use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid};
use rayon::prelude::*;
use safe_arith::{ArithError, SafeArith, SafeArithIter};
use signature_sets::{block_proposal_signature_set, get_pubkey_from_state, randao_signature_set};
use signature_sets::{
block_proposal_signature_set, execution_payload_bid_signature_set, get_pubkey_from_state,
randao_signature_set,
};
use std::borrow::Cow;
use tree_hash::TreeHash;
use typenum::Unsigned;
@@ -15,6 +19,7 @@ pub use self::verify_proposer_slashing::verify_proposer_slashing;
pub use altair::sync_committee::process_sync_aggregate;
pub use block_signature_verifier::{BlockSignatureVerifier, ParallelSignatureSets};
pub use is_valid_indexed_attestation::is_valid_indexed_attestation;
pub use is_valid_indexed_payload_attestation::is_valid_indexed_payload_attestation;
pub use process_operations::process_operations;
pub use verify_attestation::{
verify_attestation_for_block_inclusion, verify_attestation_for_state,
@@ -30,7 +35,9 @@ pub mod block_signature_verifier;
pub mod deneb;
pub mod errors;
mod is_valid_indexed_attestation;
mod is_valid_indexed_payload_attestation;
pub mod process_operations;
pub mod process_withdrawals;
pub mod signature_sets;
pub mod tests;
mod verify_attestation;
@@ -38,9 +45,9 @@ mod verify_attester_slashing;
mod verify_bls_to_execution_change;
mod verify_deposit;
mod verify_exit;
mod verify_payload_attestation;
mod verify_proposer_slashing;
use crate::common::decrease_balance;
use crate::common::update_progressive_balances_cache::{
initialize_progressive_balances_cache, update_progressive_balances_metrics,
};
@@ -172,14 +179,19 @@ pub fn per_block_processing<E: EthSpec, Payload: AbstractExecPayload<E>>(
// previous block.
if is_execution_enabled(state, block.body()) {
let body = block.body();
// TODO(EIP-7732): build out process_withdrawals variant for gloas
process_withdrawals::<E, Payload>(state, body.execution_payload()?, spec)?;
process_execution_payload::<E, Payload>(state, body, spec)?;
if state.fork_name_unchecked().gloas_enabled() {
process_withdrawals::gloas::process_withdrawals::<E>(state, spec)?;
process_execution_payload_bid(state, block, verify_signatures, spec)?;
} else {
process_withdrawals::capella::process_withdrawals::<E, Payload>(
state,
body.execution_payload()?,
spec,
)?;
process_execution_payload::<E, Payload>(state, body, spec)?;
}
}
// TODO(EIP-7732): build out process_execution_bid
// process_execution_bid(state, block, verify_signatures, spec)?;
process_randao(state, block, verify_randao, ctxt, spec)?;
process_eth1_data(state, block.body().eth1_data())?;
process_operations(state, block.body(), verify_signatures, ctxt, spec)?;
@@ -516,17 +528,70 @@ pub fn compute_timestamp_at_slot<E: EthSpec>(
/// Compute the next batch of withdrawals which should be included in a block.
///
/// https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-get_expected_withdrawals
/// https://ethereum.github.io/consensus-specs/specs/gloas/beacon-chain/#modified-get_expected_withdrawals
#[allow(clippy::type_complexity)]
pub fn get_expected_withdrawals<E: EthSpec>(
state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<(Withdrawals<E>, Option<usize>), BlockProcessingError> {
) -> Result<(Withdrawals<E>, Option<usize>, Option<usize>), BlockProcessingError> {
let epoch = state.current_epoch();
let mut withdrawal_index = state.next_withdrawal_index()?;
let mut validator_index = state.next_withdrawal_validator_index()?;
let mut withdrawals = Vec::<Withdrawal>::with_capacity(E::max_withdrawals_per_payload());
let fork_name = state.fork_name_unchecked();
// [New in Gloas:EIP7732]
// Sweep for builder payments
let processed_builder_withdrawals_count =
if let Ok(builder_pending_withdrawals) = state.builder_pending_withdrawals() {
let mut processed_builder_withdrawals_count = 0;
for withdrawal in builder_pending_withdrawals {
if withdrawal.withdrawable_epoch > epoch
|| withdrawals.len().safe_add(1)? == E::max_withdrawals_per_payload()
{
break;
}
if process_withdrawals::is_builder_payment_withdrawable(state, withdrawal)? {
let total_withdrawn = withdrawals
.iter()
.filter_map(|w| {
(w.validator_index == withdrawal.builder_index).then_some(w.amount)
})
.safe_sum()?;
let balance = state
.get_balance(withdrawal.builder_index as usize)?
.safe_sub(total_withdrawn)?;
let builder = state.get_validator(withdrawal.builder_index as usize)?;
let withdrawable_balance = if builder.slashed {
std::cmp::min(balance, withdrawal.amount)
} else if balance > spec.min_activation_balance {
std::cmp::min(
balance.safe_sub(spec.min_activation_balance)?,
withdrawal.amount,
)
} else {
0
};
if withdrawable_balance > 0 {
withdrawals.push(Withdrawal {
index: withdrawal_index,
validator_index: withdrawal.builder_index,
address: withdrawal.fee_recipient,
amount: withdrawable_balance,
});
withdrawal_index.safe_add_assign(1)?;
}
}
processed_builder_withdrawals_count.safe_add_assign(1)?;
}
Some(processed_builder_withdrawals_count)
} else {
None
};
// [New in Electra:EIP7251]
// Consume pending partial withdrawals
let processed_partial_withdrawals_count =
@@ -566,7 +631,7 @@ pub fn get_expected_withdrawals<E: EthSpec>(
index: withdrawal_index,
validator_index: withdrawal.validator_index,
address: validator
.get_execution_withdrawal_address(spec)
.get_execution_withdrawal_address(spec, state.fork_name_unchecked())
.ok_or(BeaconStateError::NonExecutionAddressWithdrawalCredential)?,
amount: withdrawable_balance,
});
@@ -603,7 +668,7 @@ pub fn get_expected_withdrawals<E: EthSpec>(
index: withdrawal_index,
validator_index,
address: validator
.get_execution_withdrawal_address(spec)
.get_execution_withdrawal_address(spec, state.fork_name_unchecked())
.ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?,
amount: balance,
});
@@ -613,7 +678,7 @@ pub fn get_expected_withdrawals<E: EthSpec>(
index: withdrawal_index,
validator_index,
address: validator
.get_execution_withdrawal_address(spec)
.get_execution_withdrawal_address(spec, state.fork_name_unchecked())
.ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?,
amount: balance.safe_sub(validator.get_max_effective_balance(spec, fork_name))?,
});
@@ -631,72 +696,163 @@ pub fn get_expected_withdrawals<E: EthSpec>(
withdrawals
.try_into()
.map_err(BlockProcessingError::SszTypesError)?,
processed_builder_withdrawals_count,
processed_partial_withdrawals_count,
))
}
/// Apply withdrawals to the state.
/// TODO(EIP-7732): abstract this out and create gloas variant
pub fn process_withdrawals<E: EthSpec, Payload: AbstractExecPayload<E>>(
pub fn process_execution_payload_bid<E: EthSpec, Payload: AbstractExecPayload<E>>(
state: &mut BeaconState<E>,
payload: Payload::Ref<'_>,
block: BeaconBlockRef<'_, E, Payload>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
if state.fork_name_unchecked().capella_enabled() {
let (expected_withdrawals, processed_partial_withdrawals_count) =
get_expected_withdrawals(state, spec)?;
let expected_root = expected_withdrawals.tree_hash_root();
let withdrawals_root = payload.withdrawals_root()?;
// Verify the bid signature
let signed_bid = block.body().signed_execution_payload_bid()?;
if expected_root != withdrawals_root {
return Err(BlockProcessingError::WithdrawalsRootMismatch {
expected: expected_root,
found: withdrawals_root,
});
}
let bid = &signed_bid.message;
let amount = bid.value;
let builder_index = bid.builder_index;
let builder = state.get_validator(builder_index as usize)?;
for withdrawal in expected_withdrawals.iter() {
decrease_balance(
state,
withdrawal.validator_index as usize,
withdrawal.amount,
)?;
}
// Update pending partial withdrawals [New in Electra:EIP7251]
if let Some(processed_partial_withdrawals_count) = processed_partial_withdrawals_count {
state
.pending_partial_withdrawals_mut()?
.pop_front(processed_partial_withdrawals_count)?;
}
// Update the next withdrawal index if this block contained withdrawals
if let Some(latest_withdrawal) = expected_withdrawals.last() {
*state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?;
// Update the next validator index to start the next withdrawal sweep
if expected_withdrawals.len() == E::max_withdrawals_per_payload() {
// Next sweep starts after the latest withdrawal's validator index
let next_validator_index = latest_withdrawal
.validator_index
.safe_add(1)?
.safe_rem(state.validators().len() as u64)?;
*state.next_withdrawal_validator_index_mut()? = next_validator_index;
}
}
// Advance sweep by the max length of the sweep if there was not a full set of withdrawals
if expected_withdrawals.len() != E::max_withdrawals_per_payload() {
let next_validator_index = state
.next_withdrawal_validator_index()?
.safe_add(spec.max_validators_per_withdrawals_sweep)?
.safe_rem(state.validators().len() as u64)?;
*state.next_withdrawal_validator_index_mut()? = next_validator_index;
}
Ok(())
// For self-builds, amount must be zero regardless of withdrawal credential prefix
if builder_index == block.proposer_index() {
block_verify!(amount == 0, ExecutionPayloadBidInvalid::BadAmount.into());
// TODO(EIP-7732): check with team if we should use ExecutionPayloadBidInvalid::BadSignature or a new error variant for this, like BadSelfBuildSignature
block_verify!(
signed_bid.signature.is_infinity(),
ExecutionPayloadBidInvalid::BadSignature.into()
);
} else {
// these shouldn't even be encountered but they're here for completeness
Ok(())
// Non-self builds require builder withdrawal credential
block_verify!(
builder.has_builder_withdrawal_credential(spec),
ExecutionPayloadBidInvalid::BadWithdrawalCredentials.into()
);
if verify_signatures.is_true() {
block_verify!(
execution_payload_bid_signature_set(
state,
|i| get_pubkey_from_state(state, i),
signed_bid,
spec
)?
.verify(),
ExecutionPayloadBidInvalid::BadSignature.into()
);
}
}
// Verify builder is active and not slashed
block_verify!(
builder.is_active_at(state.current_epoch()),
ExecutionPayloadBidInvalid::BuilderNotActive(builder_index).into()
);
block_verify!(
!builder.slashed,
ExecutionPayloadBidInvalid::BuilderSlashed(builder_index).into()
);
// Only perform payment related checks if amount > 0
if amount > 0 {
// Check that the builder has funds to cover the bid
let pending_payments = state
.builder_pending_payments()?
.iter()
.filter_map(|payment| {
if payment.withdrawal.builder_index == builder_index {
Some(payment.withdrawal.amount)
} else {
None
}
})
.safe_sum()?;
let pending_withdrawals = state
.builder_pending_withdrawals()?
.iter()
.filter_map(|withdrawal| {
if withdrawal.builder_index == builder_index {
Some(withdrawal.amount)
} else {
None
}
})
.safe_sum()?;
let builder_balance = state.get_balance(builder_index as usize)?;
block_verify!(
builder_balance
>= amount
.safe_add(pending_payments)?
.safe_add(pending_withdrawals)?
.safe_add(spec.min_activation_balance)?,
ExecutionPayloadBidInvalid::InsufficientBalance {
builder_index,
builder_balance,
bid_value: amount,
}
.into()
);
}
// Verify that the bid is for the current slot
block_verify!(
bid.slot == block.slot(),
ExecutionPayloadBidInvalid::SlotMismatch {
state_slot: block.slot(),
bid_slot: bid.slot,
}
.into()
);
// Verify that the bid is for the right parent block
let latest_block_hash = state.latest_block_hash()?;
block_verify!(
bid.parent_block_hash == *latest_block_hash,
ExecutionPayloadBidInvalid::ParentBlockHashMismatch {
state_block_hash: *latest_block_hash,
bid_parent_hash: bid.parent_block_hash,
}
.into()
);
block_verify!(
bid.parent_block_root == block.parent_root(),
ExecutionPayloadBidInvalid::ParentBlockRootMismatch {
block_parent_root: block.parent_root(),
bid_parent_root: bid.parent_block_root,
}
.into()
);
// Record the pending payment if there is some payment
if amount > 0 {
let pending_payment = BuilderPendingPayment {
weight: 0,
withdrawal: BuilderPendingWithdrawal {
fee_recipient: bid.fee_recipient,
amount,
builder_index,
withdrawable_epoch: spec.far_future_epoch,
},
};
let payment_index = (E::slots_per_epoch()
.safe_add(bid.slot.as_u64().safe_rem(E::slots_per_epoch())?)?)
as usize;
*state
.builder_pending_payments_mut()?
.get_mut(payment_index)
.ok_or(BlockProcessingError::BeaconStateError(
BeaconStateError::BuilderPendingPaymentsIndexNotSupported(payment_index),
))? = pending_payment;
}
// Cache the execution bid
*state.latest_execution_payload_bid_mut()? = bid.clone();
Ok(())
}

View File

@@ -41,6 +41,10 @@ pub enum BlockProcessingError {
index: usize,
reason: AttestationInvalid,
},
PayloadAttestationInvalid {
index: usize,
reason: PayloadAttestationInvalid,
},
DepositInvalid {
index: usize,
reason: DepositInvalid,
@@ -91,6 +95,9 @@ pub enum BlockProcessingError {
},
WithdrawalCredentialsInvalid,
PendingAttestationInElectra,
ExecutionPayloadBidInvalid {
reason: ExecutionPayloadBidInvalid,
},
}
impl From<BeaconStateError> for BlockProcessingError {
@@ -147,6 +154,12 @@ impl From<milhouse::Error> for BlockProcessingError {
}
}
impl From<ExecutionPayloadBidInvalid> for BlockProcessingError {
fn from(reason: ExecutionPayloadBidInvalid) -> Self {
Self::ExecutionPayloadBidInvalid { reason }
}
}
impl From<BlockOperationError<HeaderInvalid>> for BlockProcessingError {
fn from(e: BlockOperationError<HeaderInvalid>) -> BlockProcessingError {
match e {
@@ -200,7 +213,8 @@ impl_into_block_processing_error_with_index!(
AttestationInvalid,
DepositInvalid,
ExitInvalid,
BlsExecutionChangeInvalid
BlsExecutionChangeInvalid,
PayloadAttestationInvalid
);
pub type HeaderValidationError = BlockOperationError<HeaderInvalid>;
@@ -401,6 +415,58 @@ pub enum IndexedAttestationInvalid {
SignatureSetError(SignatureSetError),
}
#[derive(Debug, PartialEq, Clone)]
pub enum PayloadAttestationInvalid {
/// Block root does not match the parent beacon block root.
BlockRootMismatch {
expected: Hash256,
found: Hash256,
},
/// The attestation slot is not the previous slot.
SlotMismatch {
expected: Slot,
found: Slot,
},
BadIndexedPayloadAttestation(IndexedPayloadAttestationInvalid),
}
impl From<BlockOperationError<IndexedPayloadAttestationInvalid>>
for BlockOperationError<PayloadAttestationInvalid>
{
fn from(e: BlockOperationError<IndexedPayloadAttestationInvalid>) -> Self {
match e {
BlockOperationError::Invalid(e) => BlockOperationError::invalid(
PayloadAttestationInvalid::BadIndexedPayloadAttestation(e),
),
BlockOperationError::BeaconStateError(e) => BlockOperationError::BeaconStateError(e),
BlockOperationError::SignatureSetError(e) => BlockOperationError::SignatureSetError(e),
BlockOperationError::SszTypesError(e) => BlockOperationError::SszTypesError(e),
BlockOperationError::BitfieldError(e) => BlockOperationError::BitfieldError(e),
BlockOperationError::ConsensusContext(e) => BlockOperationError::ConsensusContext(e),
BlockOperationError::ArithError(e) => BlockOperationError::ArithError(e),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum IndexedPayloadAttestationInvalid {
/// The number of indices is 0.
IndicesEmpty,
/// The validator indices were not in increasing order.
///
/// The error occurred between the given `index` and `index + 1`
BadValidatorIndicesOrdering(usize),
/// The validator index is unknown. One cannot slash one who does not exist.
UnknownValidator(u64),
/// The indexed attestation aggregate signature was not valid.
BadSignature,
/// There was an error whilst attempting to get a set of signatures. The signatures may have
/// been invalid or an internal error occurred.
SignatureSetError(SignatureSetError),
/// Invalid Payload Status
PayloadStatusInvalid,
}
#[derive(Debug, PartialEq, Clone)]
pub enum DepositInvalid {
/// The signature (proof-of-possession) does not match the given pubkey.
@@ -440,6 +506,38 @@ pub enum ExitInvalid {
PendingWithdrawalInQueue(u64),
}
#[derive(Debug, PartialEq, Clone)]
pub enum ExecutionPayloadBidInvalid {
/// The builder sent a 0 amount
BadAmount,
/// The signature is invalid.
BadSignature,
/// The builder's withdrawal credential is invalid
BadWithdrawalCredentials,
/// The builder is not an active validator.
BuilderNotActive(u64),
/// The builder is slashed
BuilderSlashed(u64),
/// The builder has insufficient balance to cover the bid
InsufficientBalance {
builder_index: u64,
builder_balance: u64,
bid_value: u64,
},
/// Bid slot doesn't match state slot
SlotMismatch { state_slot: Slot, bid_slot: Slot },
/// The bid's parent block hash doesn't match the state's latest block hash
ParentBlockHashMismatch {
state_block_hash: ExecutionBlockHash,
bid_parent_hash: ExecutionBlockHash,
},
/// The bid's parent block root doesn't match the block's parent root
ParentBlockRootMismatch {
block_parent_root: Hash256,
bid_parent_root: Hash256,
},
}
#[derive(Debug, PartialEq, Clone)]
pub enum BlsExecutionChangeInvalid {
/// The specified validator is not in the state's validator registry.

View File

@@ -0,0 +1,50 @@
use super::errors::{BlockOperationError, IndexedPayloadAttestationInvalid as Invalid};
use super::signature_sets::{get_pubkey_from_state, indexed_payload_attestation_signature_set};
use crate::VerifySignatures;
use itertools::Itertools;
use types::*;
fn error(reason: Invalid) -> BlockOperationError<Invalid> {
BlockOperationError::invalid(reason)
}
pub fn is_valid_indexed_payload_attestation<E: EthSpec>(
state: &BeaconState<E>,
indexed_payload_attestation: &IndexedPayloadAttestation<E>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockOperationError<Invalid>> {
// Verify indices are non-empty and sorted (duplicates allowed)
let indices = &indexed_payload_attestation.attesting_indices;
verify!(!indices.is_empty(), Invalid::IndicesEmpty);
let check_sorted = |list: &[u64]| -> Result<(), BlockOperationError<Invalid>> {
list.iter()
.tuple_windows()
.enumerate()
.try_for_each(|(i, (x, y))| {
if x <= y {
Ok(())
} else {
Err(error(Invalid::BadValidatorIndicesOrdering(i)))
}
})?;
Ok(())
};
check_sorted(indices)?;
if verify_signatures.is_true() {
verify!(
indexed_payload_attestation_signature_set(
state,
|i| get_pubkey_from_state(state, i),
&indexed_payload_attestation.signature,
indexed_payload_attestation,
spec
)?
.verify(),
Invalid::BadSignature
);
}
Ok(())
}

View File

@@ -5,6 +5,7 @@ use crate::common::{
slash_validator,
};
use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex};
use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation;
use ssz_types::FixedVector;
use typenum::U33;
use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR};
@@ -38,7 +39,15 @@ pub fn process_operations<E: EthSpec, Payload: AbstractExecPayload<E>>(
process_bls_to_execution_changes(state, bls_to_execution_changes, verify_signatures, spec)?;
}
if state.fork_name_unchecked().electra_enabled() {
if state.fork_name_unchecked().gloas_enabled() {
process_payload_attestations(
state,
block_body.payload_attestations()?.iter(),
verify_signatures,
ctxt,
spec,
)?;
} else if state.fork_name_unchecked().electra_enabled() {
state.update_pubkey_cache()?;
process_deposit_requests(state, &block_body.execution_requests()?.deposits, spec)?;
process_withdrawal_requests(state, &block_body.execution_requests()?.withdrawals, spec)?;
@@ -514,9 +523,10 @@ pub fn process_withdrawal_requests<E: EthSpec>(
let validator = state.get_validator(validator_index)?;
// Verify withdrawal credentials
let has_correct_credential = validator.has_execution_withdrawal_credential(spec);
let has_correct_credential =
validator.has_execution_withdrawal_credential(spec, state.fork_name_unchecked());
let is_correct_source_address = validator
.get_execution_withdrawal_address(spec)
.get_execution_withdrawal_address(spec, state.fork_name_unchecked())
.map(|addr| addr == request.source_address)
.unwrap_or(false);
@@ -561,7 +571,7 @@ pub fn process_withdrawal_requests<E: EthSpec>(
.safe_add(pending_balance_to_withdraw)?;
// Only allow partial withdrawals with compounding withdrawal credentials
if validator.has_compounding_withdrawal_credential(spec)
if validator.has_compounding_withdrawal_credential(spec, state.fork_name_unchecked())
&& has_sufficient_effective_balance
&& has_excess_balance
{
@@ -730,7 +740,9 @@ pub fn process_consolidation_request<E: EthSpec>(
let source_validator = state.get_validator(source_index)?;
// Verify the source withdrawal credentials
if let Some(withdrawal_address) = source_validator.get_execution_withdrawal_address(spec) {
if let Some(withdrawal_address) =
source_validator.get_execution_withdrawal_address(spec, state.fork_name_unchecked())
{
if withdrawal_address != consolidation_request.source_address {
return Ok(());
}
@@ -741,7 +753,7 @@ pub fn process_consolidation_request<E: EthSpec>(
let target_validator = state.get_validator(target_index)?;
// Verify the target has compounding withdrawal credentials
if !target_validator.has_compounding_withdrawal_credential(spec) {
if !target_validator.has_compounding_withdrawal_credential(spec, state.fork_name_unchecked()) {
return Ok(());
}
@@ -787,3 +799,52 @@ pub fn process_consolidation_request<E: EthSpec>(
Ok(())
}
// TODO(EIP-7732): Add test cases for `process_payload_attestations` to
// `consensus/state_processing/src/per_block_processing/tests.rs`.
// The tests will require being able to build Gloas blocks with PayloadAttestations,
// which currently fails due to incomplete Gloas block structure as mentioned here
// https://github.com/sigp/lighthouse/pull/8273
pub fn process_payload_attestation<E: EthSpec>(
state: &mut BeaconState<E>,
payload_attestation: &PayloadAttestation<E>,
att_index: usize,
verify_signatures: VerifySignatures,
ctxt: &mut ConsensusContext<E>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
verify_payload_attestation(state, payload_attestation, ctxt, verify_signatures, spec)
.map_err(|e| e.into_with_index(att_index))
}
pub fn process_payload_attestations<'a, E: EthSpec, I>(
state: &mut BeaconState<E>,
payload_attestations: I,
verify_signatures: VerifySignatures,
ctxt: &mut ConsensusContext<E>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError>
where
I: Iterator<Item = &'a PayloadAttestation<E>>,
{
// Ensure required caches are all built. These should be no-ops during regular operation.
// TODO(EIP-7732): verify necessary caches
state.build_committee_cache(RelativeEpoch::Current, spec)?;
state.build_committee_cache(RelativeEpoch::Previous, spec)?;
initialize_epoch_cache(state, spec)?;
initialize_progressive_balances_cache(state, spec)?;
state.build_slashings_cache()?;
payload_attestations
.enumerate()
.try_for_each(|(i, payload_attestation)| {
process_payload_attestation(
state,
payload_attestation,
i,
verify_signatures,
ctxt,
spec,
)
})
}

View File

@@ -0,0 +1,167 @@
use super::errors::BlockProcessingError;
use super::get_expected_withdrawals;
use crate::common::decrease_balance;
use milhouse::List;
use safe_arith::SafeArith;
use tree_hash::TreeHash;
use types::{
AbstractExecPayload, BeaconState, BuilderPendingWithdrawal, ChainSpec, EthSpec, ExecPayload,
Withdrawals,
};
/// Check if a builder payment is withdrawable.
/// A builder payment is withdrawable if the builder is not slashed or
/// the builder's withdrawable epoch has been reached.
pub fn is_builder_payment_withdrawable<E: EthSpec>(
state: &BeaconState<E>,
withdrawal: &BuilderPendingWithdrawal,
) -> Result<bool, BlockProcessingError> {
let builder = state.get_validator(withdrawal.builder_index as usize)?;
let current_epoch = state.current_epoch();
Ok(builder.withdrawable_epoch >= current_epoch || !builder.slashed)
}
fn process_withdrawals_common<E: EthSpec>(
state: &mut BeaconState<E>,
expected_withdrawals: Withdrawals<E>,
partial_withdrawals_count: Option<usize>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
match state {
BeaconState::Capella(_)
| BeaconState::Deneb(_)
| BeaconState::Electra(_)
| BeaconState::Fulu(_)
| BeaconState::Gloas(_) => {
// Update pending partial withdrawals [New in Electra:EIP7251]
if let Some(partial_withdrawals_count) = partial_withdrawals_count {
state
.pending_partial_withdrawals_mut()?
.pop_front(partial_withdrawals_count)?;
}
// Update the next withdrawal index if this block contained withdrawals
if let Some(latest_withdrawal) = expected_withdrawals.last() {
*state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?;
// Update the next validator index to start the next withdrawal sweep
if expected_withdrawals.len() == E::max_withdrawals_per_payload() {
// Next sweep starts after the latest withdrawal's validator index
let next_validator_index = latest_withdrawal
.validator_index
.safe_add(1)?
.safe_rem(state.validators().len() as u64)?;
*state.next_withdrawal_validator_index_mut()? = next_validator_index;
}
}
// Advance sweep by the max length of the sweep if there was not a full set of withdrawals
if expected_withdrawals.len() != E::max_withdrawals_per_payload() {
let next_validator_index = state
.next_withdrawal_validator_index()?
.safe_add(spec.max_validators_per_withdrawals_sweep)?
.safe_rem(state.validators().len() as u64)?;
*state.next_withdrawal_validator_index_mut()? = next_validator_index;
}
Ok(())
}
// these shouldn't even be encountered but they're here for completeness
BeaconState::Base(_) | BeaconState::Altair(_) | BeaconState::Bellatrix(_) => Ok(()),
}
}
pub mod capella {
use super::*;
/// Apply withdrawals to the state.
pub fn process_withdrawals<E: EthSpec, Payload: AbstractExecPayload<E>>(
state: &mut BeaconState<E>,
payload: Payload::Ref<'_>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
// check if capella enabled because this function will run on the merge block where the fork is technically still Bellatrix
if state.fork_name_unchecked().capella_enabled() {
let (expected_withdrawals, _, partial_withdrawals_count) =
get_expected_withdrawals(state, spec)?;
let expected_root = expected_withdrawals.tree_hash_root();
let withdrawals_root = payload.withdrawals_root()?;
if expected_root != withdrawals_root {
return Err(BlockProcessingError::WithdrawalsRootMismatch {
expected: expected_root,
found: withdrawals_root,
});
}
for withdrawal in expected_withdrawals.iter() {
decrease_balance(
state,
withdrawal.validator_index as usize,
withdrawal.amount,
)?;
}
process_withdrawals_common(state, expected_withdrawals, partial_withdrawals_count, spec)
} else {
// these shouldn't even be encountered but they're here for completeness
Ok(())
}
}
}
pub mod gloas {
use super::*;
// TODO(EIP-7732): Add comprehensive tests for Gloas `process_withdrawals`:
// Similar to Capella version, these will be tested via:
// 1. EF consensus-spec tests in `testing/ef_tests/src/cases/operations.rs`
// 2. Integration tests via full block processing
// These tests would currently fail due to incomplete Gloas block structure as mentioned here, so we will implement them after block and payload processing is in a good state.
// https://github.com/sigp/lighthouse/pull/8273
/// Apply withdrawals to the state.
pub fn process_withdrawals<E: EthSpec>(
state: &mut BeaconState<E>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
if !state.is_parent_block_full() {
return Ok(());
}
let (expected_withdrawals, builder_withdrawals_count, partial_withdrawals_count) =
get_expected_withdrawals(state, spec)?;
*state.latest_withdrawals_root_mut()? = expected_withdrawals.tree_hash_root();
for withdrawal in expected_withdrawals.iter() {
decrease_balance(
state,
withdrawal.validator_index as usize,
withdrawal.amount,
)?;
}
if let (Ok(builder_pending_withdrawals), Some(builder_count)) = (
state.builder_pending_withdrawals(),
builder_withdrawals_count,
) {
let mut updated_builder_withdrawals =
Vec::with_capacity(E::builder_pending_withdrawals_limit());
for (i, withdrawal) in builder_pending_withdrawals.iter().enumerate() {
if i < builder_count {
if !is_builder_payment_withdrawable(state, withdrawal)? {
updated_builder_withdrawals.push(withdrawal.clone());
}
} else {
updated_builder_withdrawals.push(withdrawal.clone());
}
}
*state.builder_pending_withdrawals_mut()? = List::new(updated_builder_withdrawals)?;
}
process_withdrawals_common(state, expected_withdrawals, partial_withdrawals_count, spec)?;
Ok(())
}
}

View File

@@ -10,10 +10,11 @@ use typenum::Unsigned;
use types::{
AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError,
ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork,
IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof,
SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange,
SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate,
SyncAggregatorSelectionData,
IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing,
SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader,
SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid,
SignedExecutionPayloadEnvelope, SignedRoot, SignedVoluntaryExit, SigningData, Slot,
SyncAggregate, SyncAggregatorSelectionData,
};
pub type Result<T> = std::result::Result<T, Error>;
@@ -299,6 +300,35 @@ where
Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message))
}
pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>(
state: &'a BeaconState<E>,
get_pubkey: F,
signature: &'a AggregateSignature,
indexed_payload_attestation: &'b IndexedPayloadAttestation<E>,
spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>>
where
E: EthSpec,
F: Fn(usize) -> Option<Cow<'a, PublicKey>>,
{
let mut pubkeys = Vec::with_capacity(indexed_payload_attestation.attesting_indices.len());
for &validator_idx in indexed_payload_attestation.attesting_indices.iter() {
pubkeys.push(
get_pubkey(validator_idx as usize).ok_or(Error::ValidatorUnknown(validator_idx))?,
);
}
let domain = spec.compute_domain(
Domain::PTCAttester,
spec.genesis_fork_version,
state.genesis_validators_root(),
);
let message = indexed_payload_attestation.data.signing_root(domain);
Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message))
}
/// Returns the signature set for the given `indexed_attestation` but pubkeys are supplied directly
/// instead of from the state.
pub fn indexed_attestation_signature_set_from_pubkeys<'a, 'b, E, F>(
@@ -332,6 +362,64 @@ where
Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message))
}
pub fn execution_envelope_signature_set<'a, E, F>(
state: &'a BeaconState<E>,
get_pubkey: F,
signed_envelope: &'a SignedExecutionPayloadEnvelope<E>,
spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>>
where
E: EthSpec,
F: Fn(usize) -> Option<Cow<'a, PublicKey>>,
{
let proposer_index = state.latest_block_header().proposer_index;
let builder_index = signed_envelope.message.builder_index(proposer_index);
let domain = spec.get_domain(
state.current_epoch(),
Domain::BeaconBuilder,
&state.fork(),
state.genesis_validators_root(),
);
let message = signed_envelope.message.signing_root(domain);
let pubkey =
get_pubkey(builder_index as usize).ok_or(Error::ValidatorUnknown(builder_index))?;
Ok(SignatureSet::single_pubkey(
&signed_envelope.signature,
pubkey,
message,
))
}
pub fn execution_payload_bid_signature_set<'a, E, F>(
state: &'a BeaconState<E>,
get_pubkey: F,
signed_execution_payload_bid: &'a SignedExecutionPayloadBid,
spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>>
where
E: EthSpec,
F: Fn(usize) -> Option<Cow<'a, PublicKey>>,
{
// TODO(EIP-7732): needs to handle self building!
let domain = spec.get_domain(
state.current_epoch(),
Domain::BeaconBuilder,
&state.fork(),
state.genesis_validators_root(),
);
let execution_payload_bid = &signed_execution_payload_bid.message;
let pubkey = get_pubkey(execution_payload_bid.builder_index as usize)
.ok_or(Error::ValidatorUnknown(execution_payload_bid.builder_index))?;
let message = execution_payload_bid.signing_root(domain);
Ok(SignatureSet::single_pubkey(
&signed_execution_payload_bid.signature,
pubkey,
message,
))
}
/// Returns the signature set for the given `attester_slashing` and corresponding `pubkeys`.
pub fn attester_slashing_signature_sets<'a, E, F>(
state: &'a BeaconState<E>,

View File

@@ -0,0 +1,46 @@
use super::VerifySignatures;
use super::errors::{BlockOperationError, PayloadAttestationInvalid as Invalid};
use crate::ConsensusContext;
use crate::per_block_processing::is_valid_indexed_payload_attestation;
use safe_arith::SafeArith;
use types::*;
pub fn verify_payload_attestation<'ctxt, E: EthSpec>(
state: &mut BeaconState<E>,
payload_attestation: &'ctxt PayloadAttestation<E>,
ctxt: &'ctxt mut ConsensusContext<E>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockOperationError<Invalid>> {
let data = &payload_attestation.data;
// Check that the attestation is for the parent beacon block
verify!(
data.beacon_block_root == state.latest_block_header().parent_root,
Invalid::BlockRootMismatch {
expected: state.latest_block_header().parent_root,
found: data.beacon_block_root,
}
);
// Check that the attestation is for the previous slot
verify!(
data.slot.safe_add(1)? == state.slot(),
Invalid::SlotMismatch {
expected: state.slot().saturating_sub(Slot::new(1)),
found: data.slot,
}
);
let indexed_payload_attestation =
ctxt.get_indexed_payload_attestation(state, data.slot, payload_attestation, spec)?;
is_valid_indexed_payload_attestation(
state,
indexed_payload_attestation,
verify_signatures,
spec,
)?;
Ok(())
}

View File

@@ -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,
@@ -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 {
@@ -52,6 +53,7 @@ impl SinglePassConfig {
pending_consolidations: true,
effective_balance_updates: true,
proposer_lookahead: true,
builder_pending_payments: true,
}
}
@@ -65,6 +67,7 @@ impl SinglePassConfig {
pending_consolidations: false,
effective_balance_updates: false,
proposer_lookahead: false,
builder_pending_payments: false,
}
}
}
@@ -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 {
@@ -473,7 +482,7 @@ pub fn process_epoch_single_pass<E: EthSpec>(
Ok(summary)
}
// TOOO(EIP-7917): use balances cache
// TODO(EIP-7917): use balances cache
pub fn process_proposer_lookahead<E: EthSpec>(
state: &mut BeaconState<E>,
spec: &ChainSpec,
@@ -503,6 +512,68 @@ 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)
}
/// 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<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
let qualifying_payments = state
.builder_pending_payments()?
.iter()
.take(E::slots_per_epoch() as usize)
.filter(|payment| payment.weight > quorum)
.cloned()
.collect::<Vec<_>>();
// 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::<Vec<_>>();
*state.builder_pending_payments_mut()? = Vector::new(new_payments)?;
Ok(())
}
fn process_single_inactivity_update(
inactivity_score: &mut Cow<u64>,
validator_info: &ValidatorInfo,

View File

@@ -14,6 +14,7 @@ pub enum Error {
EpochProcessingError(EpochProcessingError),
ArithError(ArithError),
InconsistentStateFork(InconsistentFork),
BitfieldError(ssz::BitfieldError),
}
impl From<ArithError> for Error {
@@ -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
@@ -50,6 +57,18 @@ pub fn per_slot_processing<E: EthSpec>(
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 {

View File

@@ -82,7 +82,7 @@ pub fn upgrade_to_electra<E: EthSpec>(
// Ensure early adopters of compounding credentials go through the activation churn
let validators = post.validators().clone();
for (index, validator) in validators.iter().enumerate() {
if validator.has_compounding_withdrawal_credential(spec) {
if validator.has_compounding_withdrawal_credential(spec, post.fork_name_unchecked()) {
post.queue_excess_active_balance(index, spec)?;
}
}