Merge branch 'unstable' of https://github.com/sigp/lighthouse into gloas-block-and-bid-production

This commit is contained in:
Eitan Seri- Levi
2026-02-13 21:33:06 -08:00
84 changed files with 3389 additions and 2614 deletions

View File

@@ -5,9 +5,8 @@ authors = ["Paul Hauner <paul@paulhauner.com>", "Michael Sproul <michael@sigmapr
edition = { workspace = true }
[features]
default = ["legacy-arith"]
default = []
fake_crypto = ["bls/fake_crypto"]
legacy-arith = ["types/legacy-arith"]
arbitrary-fuzz = [
"types/arbitrary-fuzz",
"merkle_proof/arbitrary",

View File

@@ -1,12 +1,17 @@
use crate::consensus_context::ConsensusContext;
use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid};
use errors::{
BlockOperationError, BlockProcessingError, ExecutionPayloadBidInvalid, HeaderInvalid,
};
use rayon::prelude::*;
use safe_arith::{ArithError, SafeArith};
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_builder_pubkey_from_state, get_pubkey_from_state, randao_signature_set,
};
use std::borrow::Cow;
use tree_hash::TreeHash;
use typenum::Unsigned;
use types::*;
use types::{consts::gloas::BUILDER_INDEX_SELF_BUILD, *};
pub use self::verify_attester_slashing::{
get_slashable_indices, get_slashable_indices_modular, verify_attester_slashing,
@@ -176,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>(
@@ -522,3 +527,162 @@ pub fn compute_timestamp_at_slot<E: EthSpec>(
.safe_mul(spec.get_slot_duration().as_secs())
.and_then(|since_genesis| state.genesis_time().safe_add(since_genesis))
}
pub fn can_builder_cover_bid<E: EthSpec>(
state: &BeaconState<E>,
builder_index: BuilderIndex,
builder: &Builder,
bid_amount: u64,
spec: &ChainSpec,
) -> Result<bool, BlockProcessingError> {
let builder_balance = builder.balance;
let pending_withdrawals_amount =
state.get_pending_balance_to_withdraw_for_builder(builder_index)?;
let min_balance = spec
.min_deposit_amount
.safe_add(pending_withdrawals_amount)?;
if builder_balance < min_balance {
Ok(false)
} else {
Ok(builder_balance.safe_sub(min_balance)? >= bid_amount)
}
}
pub fn process_execution_payload_bid<E: EthSpec, Payload: AbstractExecPayload<E>>(
state: &mut BeaconState<E>,
block: BeaconBlockRef<'_, E, Payload>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
// Verify the bid signature
let signed_bid = block.body().signed_execution_payload_bid()?;
let bid = &signed_bid.message;
let amount = bid.value;
let builder_index = bid.builder_index;
// For self-builds, amount must be zero regardless of withdrawal credential prefix
if builder_index == BUILDER_INDEX_SELF_BUILD {
block_verify!(
amount == 0,
ExecutionPayloadBidInvalid::SelfBuildNonZeroAmount.into()
);
block_verify!(
signed_bid.signature.is_infinity(),
ExecutionPayloadBidInvalid::BadSignature.into()
);
} else {
let builder = state.get_builder(builder_index)?;
// Verify that the builder is active
block_verify!(
builder.is_active_at_finalized_epoch(state.finalized_checkpoint().epoch, spec),
ExecutionPayloadBidInvalid::BuilderNotActive(builder_index).into()
);
// Verify that the builder has funds to cover the bid
block_verify!(
can_builder_cover_bid(state, builder_index, builder, amount, spec)?,
ExecutionPayloadBidInvalid::InsufficientBalance {
builder_index,
builder_balance: builder.balance,
bid_value: amount,
}
.into()
);
if verify_signatures.is_true() {
block_verify!(
// We know this is NOT a self-build, so there MUST be a signature set (func does not
// return None).
execution_payload_bid_signature_set(
state,
|i| get_builder_pubkey_from_state(state, i),
signed_bid,
spec
)?
.ok_or(ExecutionPayloadBidInvalid::BadSignature)?
.verify(),
ExecutionPayloadBidInvalid::BadSignature.into()
);
}
}
// Verify commitments are under limit
let max_blobs_per_block = spec.max_blobs_per_block(state.current_epoch()) as usize;
block_verify!(
bid.blob_kzg_commitments.len() <= max_blobs_per_block,
ExecutionPayloadBidInvalid::ExcessBlobCommitments {
max: max_blobs_per_block,
bid: bid.blob_kzg_commitments.len(),
}
.into()
);
// Verify that the bid is for the current slot
block_verify!(
bid.slot == block.slot(),
ExecutionPayloadBidInvalid::SlotMismatch {
bid_slot: bid.slot,
block_slot: block.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()
);
let expected_randao = *state.get_randao_mix(state.current_epoch())?;
block_verify!(
bid.prev_randao == expected_randao,
ExecutionPayloadBidInvalid::PrevRandaoMismatch {
expected: expected_randao,
bid: bid.prev_randao,
}
.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,
},
};
let payment_index = E::SlotsPerEpoch::to_usize()
.safe_add(bid.slot.as_usize().safe_rem(E::SlotsPerEpoch::to_usize())?)?;
*state
.builder_pending_payments_mut()?
.get_mut(payment_index)
.ok_or(BlockProcessingError::BeaconStateError(
BeaconStateError::InvalidBuilderPendingPaymentsIndex(payment_index),
))? = pending_payment;
}
// Cache the execution bid
*state.latest_execution_payload_bid_mut()? = bid.clone();
Ok(())
}

View File

@@ -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(())
}
@@ -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.
///

View File

@@ -99,6 +99,9 @@ pub enum BlockProcessingError {
IncorrectExpectedWithdrawalsVariant,
MissingLastWithdrawal,
PendingAttestationInElectra,
ExecutionPayloadBidInvalid {
reason: ExecutionPayloadBidInvalid,
},
/// Builder payment index out of bounds (Gloas)
BuilderPaymentIndexOutOfBounds(usize),
}
@@ -157,6 +160,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 {
@@ -452,6 +461,38 @@ pub enum ExitInvalid {
PendingWithdrawalInQueue(u64),
}
#[derive(Debug, PartialEq, Clone)]
pub enum ExecutionPayloadBidInvalid {
/// The validator set a non-zero amount for a self-build.
SelfBuildNonZeroAmount,
/// The signature is invalid.
BadSignature,
/// The builder is not active.
BuilderNotActive(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 block slot
SlotMismatch { bid_slot: Slot, block_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,
},
/// The bid's prev randao doesn't match the state.
PrevRandaoMismatch { expected: Hash256, bid: Hash256 },
/// The bid contains more than the maximum number of kzg blob commitments.
ExcessBlobCommitments { max: usize, bid: usize },
}
#[derive(Debug, PartialEq, Clone)]
pub enum BlsExecutionChangeInvalid {
/// The specified validator is not in the state's validator registry.

View File

@@ -9,11 +9,12 @@ use tree_hash::TreeHash;
use typenum::Unsigned;
use types::{
AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError,
ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork,
BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork,
IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof,
SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange,
SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate,
SyncAggregatorSelectionData,
SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot, SignedVoluntaryExit,
SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData,
consts::gloas::BUILDER_INDEX_SELF_BUILD,
};
pub type Result<T> = std::result::Result<T, Error>;
@@ -28,6 +29,9 @@ pub enum Error {
/// Attempted to find the public key of a validator that does not exist. You cannot distinguish
/// between an error and an invalid block in this case.
ValidatorUnknown(u64),
/// Attempted to find the public key of a builder that does not exist. You cannot distinguish
/// between an error and an invalid block in this case.
BuilderUnknown(BuilderIndex),
/// Attempted to find the public key of a validator that does not exist. You cannot distinguish
/// between an error and an invalid block in this case.
ValidatorPubkeyUnknown(PublicKeyBytes),
@@ -53,7 +57,7 @@ impl From<BeaconStateError> for Error {
}
}
/// Helper function to get a public key from a `state`.
/// Helper function to get a validator public key from a `state`.
pub fn get_pubkey_from_state<E>(
state: &BeaconState<E>,
validator_index: usize,
@@ -71,6 +75,25 @@ where
.map(Cow::Owned)
}
/// Helper function to get a builder public key from a `state`.
pub fn get_builder_pubkey_from_state<E>(
state: &BeaconState<E>,
builder_index: BuilderIndex,
) -> Option<Cow<'_, PublicKey>>
where
E: EthSpec,
{
state
.builders()
.ok()?
.get(builder_index as usize)
.and_then(|b| {
let pk: Option<PublicKey> = b.pubkey.decompress().ok();
pk
})
.map(Cow::Owned)
}
/// A signature set that is valid if a block was signed by the expected block producer.
pub fn block_proposal_signature_set<'a, E, F, Payload: AbstractExecPayload<E>>(
state: &'a BeaconState<E>,
@@ -332,6 +355,41 @@ where
Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message))
}
pub fn execution_payload_bid_signature_set<'a, E, F>(
state: &'a BeaconState<E>,
get_builder_pubkey: F,
signed_execution_payload_bid: &'a SignedExecutionPayloadBid<E>,
spec: &'a ChainSpec,
) -> Result<Option<SignatureSet<'a>>>
where
E: EthSpec,
F: Fn(BuilderIndex) -> Option<Cow<'a, PublicKey>>,
{
let execution_payload_bid = &signed_execution_payload_bid.message;
let builder_index = execution_payload_bid.builder_index;
if builder_index == BUILDER_INDEX_SELF_BUILD {
// No signatures to verify in case of a self-build, but consensus code MUST check that
// the signature is the point at infinity.
// See `process_execution_payload_bid`.
return Ok(None);
}
let domain = spec.get_domain(
state.current_epoch(),
Domain::BeaconBuilder,
&state.fork(),
state.genesis_validators_root(),
);
let pubkey = get_builder_pubkey(builder_index).ok_or(Error::BuilderUnknown(builder_index))?;
let message = execution_payload_bid.signing_root(domain);
Ok(Some(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

@@ -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 {
@@ -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).
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,

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
@@ -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

View File

@@ -8,9 +8,10 @@ authors = [
edition = { workspace = true }
[features]
default = ["legacy-arith"]
# Allow saturating arithmetic on slots and epochs. Enabled by default, but deprecated.
legacy-arith = []
default = []
# Enable +, -, *, /, % operators for Slot and Epoch types.
# Operations saturate instead of wrapping.
saturating-arith = []
sqlite = ["dep:rusqlite"]
arbitrary = [
"dep:arbitrary",

View File

@@ -1,5 +1,5 @@
use crate::test_utils::TestRandom;
use crate::{Address, Epoch, ForkName};
use crate::{Address, ChainSpec, Epoch, ForkName};
use bls::PublicKeyBytes;
use context_deserialize::context_deserialize;
use serde::{Deserialize, Serialize};
@@ -24,3 +24,12 @@ pub struct Builder {
pub deposit_epoch: Epoch,
pub withdrawable_epoch: Epoch,
}
impl Builder {
/// Check if a builder is active in a state with `finalized_epoch`.
///
/// This implements `is_active_builder` from the spec.
pub fn is_active_at_finalized_epoch(&self, finalized_epoch: Epoch, spec: &ChainSpec) -> bool {
self.deposit_epoch < finalized_epoch && self.withdrawable_epoch == spec.far_future_epoch
}
}

View File

@@ -22,7 +22,7 @@ use crate::{
test_utils::TestRandom,
};
#[cfg(feature = "legacy-arith")]
#[cfg(feature = "saturating-arith")]
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, Sub, SubAssign};
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]

View File

@@ -117,7 +117,7 @@ macro_rules! impl_safe_arith {
}
// Deprecated: prefer `SafeArith` methods for new code.
#[cfg(feature = "legacy-arith")]
#[cfg(feature = "saturating-arith")]
macro_rules! impl_math_between {
($main: ident, $other: ident) => {
impl Add<$other> for $main {
@@ -321,9 +321,9 @@ macro_rules! impl_common {
impl_u64_eq_ord!($type);
impl_safe_arith!($type, $type);
impl_safe_arith!($type, u64);
#[cfg(feature = "legacy-arith")]
#[cfg(feature = "saturating-arith")]
impl_math_between!($type, $type);
#[cfg(feature = "legacy-arith")]
#[cfg(feature = "saturating-arith")]
impl_math_between!($type, u64);
impl_math!($type);
impl_display!($type);

View File

@@ -9,7 +9,7 @@ use fixed_bytes::FixedBytesExtended;
use int_to_bytes::{int_to_bytes4, int_to_bytes8};
use metastruct::{NumFields, metastruct};
use milhouse::{List, Vector};
use safe_arith::{ArithError, SafeArith};
use safe_arith::{ArithError, SafeArith, SafeArithIter};
use serde::{Deserialize, Deserializer, Serialize};
use ssz::{Decode, DecodeError, Encode, ssz_encode};
use ssz_derive::{Decode, Encode};
@@ -218,6 +218,7 @@ pub enum BeaconStateError {
envelope_epoch: Epoch,
},
InvalidIndicesCount,
InvalidBuilderPendingPaymentsIndex(usize),
InvalidExecutionPayloadAvailabilityIndex(usize),
}
@@ -875,7 +876,7 @@ impl<E: EthSpec> BeaconState<E> {
relative_epoch: RelativeEpoch,
) -> Result<u64, BeaconStateError> {
let cache = self.committee_cache(relative_epoch)?;
Ok(cache.epoch_committee_count() as u64)
Ok(cache.epoch_committee_count()? as u64)
}
/// Return the cached active validator indices at some epoch.
@@ -2149,7 +2150,7 @@ impl<E: EthSpec> BeaconState<E> {
) -> Result<Option<AttestationDuty>, BeaconStateError> {
let cache = self.committee_cache(relative_epoch)?;
Ok(cache.get_attestation_duties(validator_index))
Ok(cache.get_attestation_duties(validator_index)?)
}
/// Check if the attestation is for the block proposed at the attestation slot.
@@ -2749,6 +2750,30 @@ impl<E: EthSpec> BeaconState<E> {
Ok(pending_balance)
}
pub fn get_pending_balance_to_withdraw_for_builder(
&self,
builder_index: BuilderIndex,
) -> Result<u64, BeaconStateError> {
let pending_withdrawals_total = self
.builder_pending_withdrawals()?
.iter()
.filter_map(|withdrawal| {
(withdrawal.builder_index == builder_index).then_some(withdrawal.amount)
})
.safe_sum()?;
let pending_payments_total = self
.builder_pending_payments()?
.iter()
.filter_map(|payment| {
(payment.withdrawal.builder_index == builder_index)
.then_some(payment.withdrawal.amount)
})
.safe_sum()?;
pending_withdrawals_total
.safe_add(pending_payments_total)
.map_err(Into::into)
}
// ******* Electra mutators *******
pub fn queue_excess_active_balance(
@@ -2884,7 +2909,6 @@ impl<E: EthSpec> BeaconState<E> {
}
}
#[allow(clippy::arithmetic_side_effects)]
pub fn rebase_on(&mut self, base: &Self, spec: &ChainSpec) -> Result<(), BeaconStateError> {
// Required for macros (which use type-hints internally).
@@ -3193,7 +3217,6 @@ impl<E: EthSpec> BeaconState<E> {
))
}
#[allow(clippy::arithmetic_side_effects)]
pub fn apply_pending_mutations(&mut self) -> Result<(), BeaconStateError> {
match self {
Self::Base(inner) => {
@@ -3296,7 +3319,6 @@ impl<E: EthSpec> BeaconState<E> {
pub fn get_beacon_state_leaves(&self) -> Vec<Hash256> {
let mut leaves = vec![];
#[allow(clippy::arithmetic_side_effects)]
match self {
BeaconState::Base(state) => {
map_beacon_state_base_fields!(state, |_, field| {

View File

@@ -1,9 +1,7 @@
#![allow(clippy::arithmetic_side_effects)]
use std::{num::NonZeroUsize, ops::Range, sync::Arc};
use educe::Educe;
use safe_arith::SafeArith;
use safe_arith::{ArithError, SafeArith};
use serde::{Deserialize, Serialize};
use ssz::{Decode, DecodeError, Encode, four_byte_option_impl};
use ssz_derive::{Decode, Encode};
@@ -79,7 +77,13 @@ impl CommitteeCache {
.saturating_sub(spec.min_seed_lookahead)
.saturating_sub(1u64);
if reqd_randao_epoch < state.min_randao_epoch() || epoch > state.current_epoch() + 1 {
if reqd_randao_epoch < state.min_randao_epoch()
|| epoch
> state
.current_epoch()
.safe_add(1)
.map_err(BeaconStateError::ArithError)?
{
return Err(BeaconStateError::EpochOutOfBounds);
}
@@ -118,7 +122,7 @@ impl CommitteeCache {
*shuffling_positions
.get_mut(v)
.ok_or(BeaconStateError::ShuffleIndexOutOfBounds(v))? =
NonZeroUsize::new(i + 1).into();
NonZeroUsize::new(i.safe_add(1).map_err(BeaconStateError::ArithError)?).into();
}
Ok(Arc::new(CommitteeCache {
@@ -177,8 +181,9 @@ impl CommitteeCache {
self.slots_per_epoch as usize,
self.committees_per_slot as usize,
index as usize,
);
let committee = self.compute_committee(committee_index)?;
)
.ok()?;
let committee = self.compute_committee(committee_index).ok()??;
Some(BeaconCommittee {
slot,
@@ -212,8 +217,9 @@ impl CommitteeCache {
.initialized_epoch
.ok_or(BeaconStateError::CommitteeCacheUninitialized(None))?;
let capacity = self.epoch_committee_count()?;
initialized_epoch.slot_iter(self.slots_per_epoch).try_fold(
Vec::with_capacity(self.epoch_committee_count()),
Vec::with_capacity(capacity),
|mut vec, slot| {
vec.append(&mut self.get_beacon_committees_at_slot(slot)?);
Ok(vec)
@@ -225,43 +231,53 @@ impl CommitteeCache {
///
/// Returns `None` if the `validator_index` does not exist, does not have duties or `Self` is
/// non-initialized.
pub fn get_attestation_duties(&self, validator_index: usize) -> Option<AttestationDuty> {
let i = self.shuffled_position(validator_index)?;
pub fn get_attestation_duties(
&self,
validator_index: usize,
) -> Result<Option<AttestationDuty>, ArithError> {
let Some(i) = self.shuffled_position(validator_index) else {
return Ok(None);
};
(0..self.epoch_committee_count())
.map(|nth_committee| (nth_committee, self.compute_committee_range(nth_committee)))
.find(|(_, range)| {
if let Some(range) = range {
range.start <= i && range.end > i
} else {
false
}
})
.and_then(|(nth_committee, range)| {
let (slot, index) = self.convert_to_slot_and_index(nth_committee as u64)?;
let range = range?;
let committee_position = i - range.start;
let committee_len = range.end - range.start;
for nth_committee in 0..self.epoch_committee_count()? {
let Some(range) = self.compute_committee_range(nth_committee)? else {
continue;
};
Some(AttestationDuty {
if range.start <= i && range.end > i {
let Some((slot, index)) = self.convert_to_slot_and_index(nth_committee as u64)?
else {
return Ok(None);
};
let committee_position = i.safe_sub(range.start)?;
let committee_len = range.end.safe_sub(range.start)?;
return Ok(Some(AttestationDuty {
slot,
index,
committee_position,
committee_len,
committees_at_slot: self.committees_per_slot(),
})
})
}));
}
}
Ok(None)
}
/// Convert an index addressing the list of all epoch committees into a slot and per-slot index.
fn convert_to_slot_and_index(
&self,
global_committee_index: u64,
) -> Option<(Slot, CommitteeIndex)> {
let epoch_start_slot = self.initialized_epoch?.start_slot(self.slots_per_epoch);
let slot_offset = global_committee_index / self.committees_per_slot;
let index = global_committee_index % self.committees_per_slot;
Some((epoch_start_slot.safe_add(slot_offset).ok()?, index))
) -> Result<Option<(Slot, CommitteeIndex)>, ArithError> {
let Some(epoch) = self.initialized_epoch else {
return Ok(None);
};
let epoch_start_slot = epoch.start_slot(self.slots_per_epoch);
let slot_offset = global_committee_index.safe_div(self.committees_per_slot)?;
let index = global_committee_index.safe_rem(self.committees_per_slot)?;
Ok(Some((epoch_start_slot.safe_add(slot_offset)?, index)))
}
/// Returns the number of active validators in the initialized epoch.
@@ -278,11 +294,8 @@ impl CommitteeCache {
/// Always returns `usize::default()` for a non-initialized epoch.
///
/// Spec v0.12.1
pub fn epoch_committee_count(&self) -> usize {
epoch_committee_count(
self.committees_per_slot as usize,
self.slots_per_epoch as usize,
)
pub fn epoch_committee_count(&self) -> Result<usize, ArithError> {
(self.committees_per_slot as usize).safe_mul(self.slots_per_epoch as usize)
}
/// Returns the number of committees per slot for this cache's epoch.
@@ -293,19 +306,23 @@ impl CommitteeCache {
/// Returns a slice of `self.shuffling` that represents the `index`'th committee in the epoch.
///
/// Spec v0.12.1
fn compute_committee(&self, index: usize) -> Option<&[usize]> {
self.shuffling.get(self.compute_committee_range(index)?)
fn compute_committee(&self, index: usize) -> Result<Option<&[usize]>, ArithError> {
if let Some(range) = self.compute_committee_range(index)? {
Ok(self.shuffling.get(range))
} else {
Ok(None)
}
}
/// Returns a range of `self.shuffling` that represents the `index`'th committee in the epoch.
///
/// To avoid a divide-by-zero, returns `None` if `self.committee_count` is zero.
/// To avoid a divide-by-zero, returns `Ok(None)` if `self.committee_count` is zero.
///
/// Will also return `None` if the index is out of bounds.
/// Will also return `Ok(None)` if the index is out of bounds.
///
/// Spec v0.12.1
fn compute_committee_range(&self, index: usize) -> Option<Range<usize>> {
compute_committee_range_in_epoch(self.epoch_committee_count(), index, self.shuffling.len())
fn compute_committee_range(&self, index: usize) -> Result<Option<Range<usize>>, ArithError> {
compute_committee_range_in_epoch(self.epoch_committee_count()?, index, self.shuffling.len())
}
/// Returns the index of some validator in `self.shuffling`.
@@ -329,8 +346,10 @@ pub fn compute_committee_index_in_epoch(
slots_per_epoch: usize,
committees_per_slot: usize,
committee_index: usize,
) -> usize {
(slot.as_usize() % slots_per_epoch) * committees_per_slot + committee_index
) -> Result<usize, ArithError> {
(slot.as_usize().safe_rem(slots_per_epoch)?)
.safe_mul(committees_per_slot)?
.safe_add(committee_index)
}
/// Computes the range for slicing the shuffled indices to determine the members of a committee.
@@ -341,20 +360,16 @@ pub fn compute_committee_range_in_epoch(
epoch_committee_count: usize,
index_in_epoch: usize,
shuffling_len: usize,
) -> Option<Range<usize>> {
) -> Result<Option<Range<usize>>, ArithError> {
if epoch_committee_count == 0 || index_in_epoch >= epoch_committee_count {
return None;
return Ok(None);
}
let start = (shuffling_len * index_in_epoch) / epoch_committee_count;
let end = (shuffling_len * (index_in_epoch + 1)) / epoch_committee_count;
let start = (shuffling_len.safe_mul(index_in_epoch))?.safe_div(epoch_committee_count)?;
let end =
(shuffling_len.safe_mul(index_in_epoch.safe_add(1)?))?.safe_div(epoch_committee_count)?;
Some(start..end)
}
/// Returns the total number of committees in an epoch.
pub fn epoch_committee_count(committees_per_slot: usize, slots_per_epoch: usize) -> usize {
committees_per_slot * slots_per_epoch
Ok(Some(start..end))
}
/// Returns a list of all `validators` indices where the validator is active at the given

View File

@@ -21,7 +21,7 @@ pub use beacon_state::{
};
pub use committee_cache::{
CommitteeCache, compute_committee_index_in_epoch, compute_committee_range_in_epoch,
epoch_committee_count, get_active_validator_indices,
get_active_validator_indices,
};
pub use epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey};
pub use exit_cache::ExitCache;

View File

@@ -33,9 +33,9 @@ fn default_values() {
assert!(!cache.is_initialized_at(Epoch::new(0)));
assert!(&cache.active_validator_indices().is_empty());
assert_eq!(cache.get_beacon_committee(Slot::new(0), 0), None);
assert_eq!(cache.get_attestation_duties(0), None);
assert_eq!(cache.get_attestation_duties(0), Ok(None));
assert_eq!(cache.active_validator_count(), 0);
assert_eq!(cache.epoch_committee_count(), 0);
assert_eq!(cache.epoch_committee_count(), Ok(0));
assert!(cache.get_beacon_committees_at_slot(Slot::new(0)).is_err());
}