From 2d96b3f1935d187e852b3adb5a3a9e8ba83521d3 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 19 Jan 2026 15:59:36 +1100 Subject: [PATCH] Extract consensus changes from gloas-envelope-processing --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 +- beacon_node/beacon_chain/tests/store_tests.rs | 4 +- beacon_node/http_api/src/builder_states.rs | 2 +- .../src/bls_to_execution_changes.rs | 22 +- beacon_node/operation_pool/src/lib.rs | 7 +- beacon_node/store/src/consensus_context.rs | 2 + .../common/get_payload_attesting_indices.rs | 45 +++ consensus/state_processing/src/common/mod.rs | 4 + .../state_processing/src/consensus_context.rs | 33 +- .../src/envelope_processing.rs | 284 +++++++++++++++++ consensus/state_processing/src/lib.rs | 1 + .../src/per_block_processing.rs | 296 +++++++++++++----- .../src/per_block_processing/errors.rs | 100 +++++- .../is_valid_indexed_payload_attestation.rs | 50 +++ .../process_operations.rs | 73 ++++- .../process_withdrawals.rs | 167 ++++++++++ .../per_block_processing/signature_sets.rs | 96 +++++- .../verify_payload_attestation.rs | 46 +++ .../src/per_epoch_processing/single_pass.rs | 79 ++++- .../src/per_slot_processing.rs | 19 ++ .../state_processing/src/upgrade/electra.rs | 2 +- consensus/types/src/attestation/mod.rs | 2 + consensus/types/src/attestation/ptc.rs | 24 ++ consensus/types/src/core/consts.rs | 3 + .../execution/execution_payload_envelope.rs | 27 +- .../signed_execution_payload_envelope.rs | 78 ++++- consensus/types/src/state/beacon_state.rs | 222 +++++++++++-- consensus/types/src/validator/validator.rs | 60 +++- consensus/types/tests/state.rs | 9 + testing/ef_tests/src/cases/operations.rs | 11 +- 30 files changed, 1624 insertions(+), 148 deletions(-) create mode 100644 consensus/state_processing/src/common/get_payload_attesting_indices.rs create mode 100644 consensus/state_processing/src/envelope_processing.rs create mode 100644 consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs create mode 100644 consensus/state_processing/src/per_block_processing/process_withdrawals.rs create mode 100644 consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs create mode 100644 consensus/types/src/attestation/ptc.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e3de8d7324..1ffde9c11e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4794,7 +4794,7 @@ impl BeaconChain { let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch()); if head_state.current_epoch() == proposal_epoch { return get_expected_withdrawals(&unadvanced_state, &self.spec) - .map(|(withdrawals, _)| withdrawals) + .map(|(withdrawals, _, _)| withdrawals) .map_err(Error::PrepareProposerFailed); } @@ -4812,7 +4812,7 @@ impl BeaconChain { &self.spec, )?; get_expected_withdrawals(&advanced_state, &self.spec) - .map(|(withdrawals, _)| withdrawals) + .map(|(withdrawals, _, _)| withdrawals) .map_err(Error::PrepareProposerFailed) } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ba0621ae72..314090d9e0 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1412,7 +1412,7 @@ async fn proposer_shuffling_changing_with_lookahead() { let consolidation_request: ConsolidationRequest = ConsolidationRequest { source_address: validator_to_topup - .get_execution_withdrawal_address(spec) + .get_execution_withdrawal_address(spec, ForkName::Fulu) .unwrap(), source_pubkey: validator_to_topup.pubkey, target_pubkey: validator_to_topup.pubkey, @@ -1491,7 +1491,7 @@ async fn proposer_shuffling_changing_with_lookahead() { let validator = current_epoch_state .get_validator(validator_to_topup_index) .unwrap(); - assert!(validator.has_compounding_withdrawal_credential(spec)); + assert!(validator.has_compounding_withdrawal_credential(spec, ForkName::Fulu)); assert_eq!(validator.effective_balance, 95_000_000_000); // The shuffling for the current epoch from `prev_epoch_state` should match the shuffling diff --git a/beacon_node/http_api/src/builder_states.rs b/beacon_node/http_api/src/builder_states.rs index 7c05dd00d2..74228961fb 100644 --- a/beacon_node/http_api/src/builder_states.rs +++ b/beacon_node/http_api/src/builder_states.rs @@ -32,7 +32,7 @@ pub fn get_next_withdrawals( } match get_expected_withdrawals(&state, &chain.spec) { - Ok((withdrawals, _)) => Ok(withdrawals), + Ok((withdrawals, _, _)) => Ok(withdrawals), Err(e) => Err(warp_utils::reject::custom_server_error(format!( "failed to get expected withdrawal: {:?}", e diff --git a/beacon_node/operation_pool/src/bls_to_execution_changes.rs b/beacon_node/operation_pool/src/bls_to_execution_changes.rs index 485f21b5c8..4458b2f70d 100644 --- a/beacon_node/operation_pool/src/bls_to_execution_changes.rs +++ b/beacon_node/operation_pool/src/bls_to_execution_changes.rs @@ -113,16 +113,18 @@ impl BlsToExecutionChanges { .validators() .get(validator_index as usize) .is_none_or(|validator| { - let prune = validator.has_execution_withdrawal_credential(spec) - && head_block - .message() - .body() - .bls_to_execution_changes() - .map_or(true, |recent_changes| { - !recent_changes - .iter() - .any(|c| c.message.validator_index == validator_index) - }); + let prune = validator.has_execution_withdrawal_credential( + spec, + head_state.fork_name_unchecked(), + ) && head_block + .message() + .body() + .bls_to_execution_changes() + .map_or(true, |recent_changes| { + !recent_changes + .iter() + .any(|c| c.message.validator_index == validator_index) + }); if prune { validator_indices_pruned.push(validator_index); } diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 81423d6abd..71f54e9d2d 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -582,7 +582,12 @@ impl OperationPool { address_change.signature_is_still_valid(&state.fork()) && state .get_validator(address_change.as_inner().message.validator_index as usize) - .is_ok_and(|validator| !validator.has_execution_withdrawal_credential(spec)) + .is_ok_and(|validator| { + !validator.has_execution_withdrawal_credential( + spec, + state.fork_name_unchecked(), + ) + }) }, |address_change| address_change.as_inner().clone(), E::MaxBlsToExecutionChanges::to_usize(), diff --git a/beacon_node/store/src/consensus_context.rs b/beacon_node/store/src/consensus_context.rs index 281106d9aa..9b492d6887 100644 --- a/beacon_node/store/src/consensus_context.rs +++ b/beacon_node/store/src/consensus_context.rs @@ -35,6 +35,8 @@ impl OnDiskConsensusContext { proposer_index, current_block_root, indexed_attestations, + indexed_payload_attestations: _, + // TODO(EIP-7732): add indexed_payload_attestations to the on-disk format. } = ctxt; OnDiskConsensusContext { slot, diff --git a/consensus/state_processing/src/common/get_payload_attesting_indices.rs b/consensus/state_processing/src/common/get_payload_attesting_indices.rs new file mode 100644 index 0000000000..47f58025ca --- /dev/null +++ b/consensus/state_processing/src/common/get_payload_attesting_indices.rs @@ -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( + state: &BeaconState, + slot: Slot, + payload_attestation: &PayloadAttestation, + spec: &ChainSpec, +) -> Result, BlockOperationError> { + 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( + state: &BeaconState, + slot: Slot, + payload_attestation: &PayloadAttestation, + spec: &ChainSpec, +) -> Result, 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::::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) +} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index 0287748fd0..e550a6c48b 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -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; diff --git a/consensus/state_processing/src/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 07d554e303..a7af510f71 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -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 { pub current_block_root: Option, /// Cache of indexed attestations constructed during block processing. pub indexed_attestations: HashMap>, + /// Cache of indexed payload attestations constructed during block processing. + pub indexed_payload_attestations: HashMap>, } #[derive(Debug, PartialEq, Clone)] @@ -55,6 +62,7 @@ impl ConsensusContext { proposer_index: None, current_block_root: None, indexed_attestations: HashMap::new(), + indexed_payload_attestations: HashMap::new(), } } @@ -177,6 +185,25 @@ impl ConsensusContext { .map(|indexed_attestation| (*indexed_attestation).to_ref()) } + pub fn get_indexed_payload_attestation<'a>( + &'a mut self, + state: &BeaconState, + slot: Slot, + payload_attestation: &'a PayloadAttestation, + spec: &ChainSpec, + ) -> Result<&'a IndexedPayloadAttestation, BlockOperationError> + { + 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() } diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs new file mode 100644 index 0000000000..9ee2be3f18 --- /dev/null +++ b/consensus/state_processing/src/envelope_processing.rs @@ -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 for EnvelopeProcessingError { + fn from(e: BeaconStateError) -> Self { + EnvelopeProcessingError::BeaconStateError(e) + } +} + +impl From for EnvelopeProcessingError { + fn from(e: BlockProcessingError) -> Self { + EnvelopeProcessingError::BlockProcessingError(e) + } +} + +impl From 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( + state: &mut BeaconState, + parent_state_root: Option, + signed_envelope: &SignedExecutionPayloadEnvelope, + 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(()) +} diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index 9b2696c6d5..e37c526579 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -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; diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index cd1c1b9849..cb0bc5046f 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -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>( // 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::(state, body.execution_payload()?, spec)?; - process_execution_payload::(state, body, spec)?; + if state.fork_name_unchecked().gloas_enabled() { + process_withdrawals::gloas::process_withdrawals::(state, spec)?; + process_execution_payload_bid(state, block, verify_signatures, spec)?; + } else { + process_withdrawals::capella::process_withdrawals::( + state, + body.execution_payload()?, + spec, + )?; + process_execution_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( /// 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( state: &BeaconState, spec: &ChainSpec, -) -> Result<(Withdrawals, Option), BlockProcessingError> { +) -> Result<(Withdrawals, Option, Option), 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::::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( 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( 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( 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( 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>( +pub fn process_execution_payload_bid>( state: &mut BeaconState, - 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(()) } diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index ff7c0204e2..f3f5ef96af 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -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 for BlockProcessingError { @@ -147,6 +154,12 @@ impl From for BlockProcessingError { } } +impl From for BlockProcessingError { + fn from(reason: ExecutionPayloadBidInvalid) -> Self { + Self::ExecutionPayloadBidInvalid { reason } + } +} + impl From> for BlockProcessingError { fn from(e: BlockOperationError) -> BlockProcessingError { match e { @@ -200,7 +213,8 @@ impl_into_block_processing_error_with_index!( AttestationInvalid, DepositInvalid, ExitInvalid, - BlsExecutionChangeInvalid + BlsExecutionChangeInvalid, + PayloadAttestationInvalid ); pub type HeaderValidationError = BlockOperationError; @@ -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> + for BlockOperationError +{ + fn from(e: BlockOperationError) -> 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. diff --git a/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs new file mode 100644 index 0000000000..45ccdf35e2 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs @@ -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 { + BlockOperationError::invalid(reason) +} + +pub fn is_valid_indexed_payload_attestation( + state: &BeaconState, + indexed_payload_attestation: &IndexedPayloadAttestation, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError> { + // 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> { + 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(()) +} diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 8afeeb685b..1ee5fd349c 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -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>( 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( 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( .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( 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( 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( 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( + state: &mut BeaconState, + payload_attestation: &PayloadAttestation, + att_index: usize, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + 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, + payload_attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> +where + I: Iterator>, +{ + // 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, + ) + }) +} diff --git a/consensus/state_processing/src/per_block_processing/process_withdrawals.rs b/consensus/state_processing/src/per_block_processing/process_withdrawals.rs new file mode 100644 index 0000000000..eef365333e --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/process_withdrawals.rs @@ -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( + state: &BeaconState, + withdrawal: &BuilderPendingWithdrawal, +) -> Result { + 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( + state: &mut BeaconState, + expected_withdrawals: Withdrawals, + partial_withdrawals_count: Option, + 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>( + state: &mut BeaconState, + 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( + state: &mut BeaconState, + 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(()) + } +} diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 0e936007ee..89fdc33009 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -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 = std::result::Result; @@ -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, + get_pubkey: F, + signature: &'a AggregateSignature, + indexed_payload_attestation: &'b IndexedPayloadAttestation, + spec: &'a ChainSpec, +) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + 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, + get_pubkey: F, + signed_envelope: &'a SignedExecutionPayloadEnvelope, + spec: &'a ChainSpec, +) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + 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, + get_pubkey: F, + signed_execution_payload_bid: &'a SignedExecutionPayloadBid, + spec: &'a ChainSpec, +) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + // 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, diff --git a/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs new file mode 100644 index 0000000000..a65f132462 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs @@ -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, + payload_attestation: &'ctxt PayloadAttestation, + ctxt: &'ctxt mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError> { + 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(()) +} diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 3e07803aa6..57912234a2 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -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( )?; } + // 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( Ok(summary) } -// TOOO(EIP-7917): use balances cache +// TODO(EIP-7917): use balances cache pub fn process_proposer_lookahead( state: &mut BeaconState, spec: &ChainSpec, @@ -503,6 +512,68 @@ pub fn process_proposer_lookahead( Ok(()) } +/// Calculate the quorum threshold for builder payments based on total active balance. +fn get_builder_payment_quorum_threshold( + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result { + let per_slot_balance = state_ctxt + .total_active_balance + .safe_div(E::slots_per_epoch())?; + let quorum = per_slot_balance.safe_mul(spec.builder_payment_threshold_numerator)?; + quorum + .safe_div(spec.builder_payment_threshold_denominator) + .map_err(Error::from) +} + +/// Process builder pending payments, moving qualifying payments to withdrawals. +/// TODO(EIP-7732): Add EF consensus-spec tests for `process_builder_pending_payments` +/// Currently blocked by EF consensus-spec-tests for Gloas not yet integrated. +fn process_builder_pending_payments( + state: &mut BeaconState, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + let quorum = get_builder_payment_quorum_threshold::(state_ctxt, spec)?; + + // Collect qualifying payments + let qualifying_payments = state + .builder_pending_payments()? + .iter() + .take(E::slots_per_epoch() as usize) + .filter(|payment| payment.weight > quorum) + .cloned() + .collect::>(); + + // Update `builder_pending_withdrawals` with qualifying `builder_pending_payments` + qualifying_payments + .into_iter() + .try_for_each(|payment| -> Result<(), Error> { + let exit_queue_epoch = + state.compute_exit_epoch_and_update_churn(payment.withdrawal.amount, spec)?; + let withdrawable_epoch = + exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; + + let mut withdrawal = payment.withdrawal.clone(); + withdrawal.withdrawable_epoch = withdrawable_epoch; + state.builder_pending_withdrawals_mut()?.push(withdrawal)?; + Ok(()) + })?; + + // Move remaining `builder_pending_payments` to start of list and set the rest to default + let new_payments = state + .builder_pending_payments()? + .iter() + .skip(E::slots_per_epoch() as usize) + .cloned() + .chain((0..E::slots_per_epoch() as usize).map(|_| BuilderPendingPayment::default())) + .collect::>(); + + *state.builder_pending_payments_mut()? = Vector::new(new_payments)?; + + Ok(()) +} + fn process_single_inactivity_update( inactivity_score: &mut Cow, validator_info: &ValidatorInfo, diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index 0f8e5dc52d..1d35d28c27 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -14,6 +14,7 @@ pub enum Error { EpochProcessingError(EpochProcessingError), ArithError(ArithError), InconsistentStateFork(InconsistentFork), + BitfieldError(ssz::BitfieldError), } impl From for Error { @@ -22,6 +23,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: ssz::BitfieldError) -> Self { + Self::BitfieldError(e) + } +} + /// Advances a state forward by one slot, performing per-epoch processing if required. /// /// If the root of the supplied `state` is known, then it can be passed as `state_root`. If @@ -50,6 +57,18 @@ pub fn per_slot_processing( state.slot_mut().safe_add_assign(1)?; + // Unset the next payload availability + if state.fork_name_unchecked().gloas_enabled() { + let next_slot_index = state + .slot() + .as_usize() + .safe_add(1)? + .safe_rem(E::slots_per_historical_root())?; + state + .execution_payload_availability_mut()? + .set(next_slot_index, false)?; + } + // Process fork upgrades here. Note that multiple upgrades can potentially run // in sequence if they are scheduled in the same Epoch (common in testnets) if state.slot().safe_rem(E::slots_per_epoch())? == 0 { diff --git a/consensus/state_processing/src/upgrade/electra.rs b/consensus/state_processing/src/upgrade/electra.rs index 258b28a45b..a84b81d85c 100644 --- a/consensus/state_processing/src/upgrade/electra.rs +++ b/consensus/state_processing/src/upgrade/electra.rs @@ -82,7 +82,7 @@ pub fn upgrade_to_electra( // 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)?; } } diff --git a/consensus/types/src/attestation/mod.rs b/consensus/types/src/attestation/mod.rs index 586d99bd90..5b59b83e72 100644 --- a/consensus/types/src/attestation/mod.rs +++ b/consensus/types/src/attestation/mod.rs @@ -11,6 +11,7 @@ mod payload_attestation; mod payload_attestation_data; mod payload_attestation_message; mod pending_attestation; +mod ptc; mod selection_proof; mod shuffling_id; mod signed_aggregate_and_proof; @@ -36,6 +37,7 @@ pub use payload_attestation::PayloadAttestation; pub use payload_attestation_data::PayloadAttestationData; pub use payload_attestation_message::PayloadAttestationMessage; pub use pending_attestation::PendingAttestation; +pub use ptc::PTC; pub use selection_proof::SelectionProof; pub use shuffling_id::AttestationShufflingId; pub use signed_aggregate_and_proof::{ diff --git a/consensus/types/src/attestation/ptc.rs b/consensus/types/src/attestation/ptc.rs new file mode 100644 index 0000000000..39fbee33a2 --- /dev/null +++ b/consensus/types/src/attestation/ptc.rs @@ -0,0 +1,24 @@ +use crate::EthSpec; +use ssz_types::FixedVector; + +/// TODO(EIP-7732): is it easier to return u64 or usize? +#[derive(Clone, Debug, PartialEq)] +pub struct PTC(pub FixedVector); + +impl<'a, E: EthSpec> IntoIterator for &'a PTC { + type Item = &'a usize; + type IntoIter = std::slice::Iter<'a, usize>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl IntoIterator for PTC { + type Item = usize; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/consensus/types/src/core/consts.rs b/consensus/types/src/core/consts.rs index b6d63c47a8..6876159c0f 100644 --- a/consensus/types/src/core/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -25,3 +25,6 @@ pub mod bellatrix { pub mod deneb { pub use kzg::VERSIONED_HASH_VERSION_KZG; } +pub mod gloas { + pub const BUILDER_INDEX_SELF_BUILD: u64 = u64::MAX; +} diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 64e03cec5a..a82517a16e 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -1,3 +1,4 @@ +use crate::consts::gloas::BUILDER_INDEX_SELF_BUILD; use crate::test_utils::TestRandom; use crate::{ EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, KzgCommitments, @@ -17,8 +18,10 @@ use tree_hash_derive::TreeHash; pub struct ExecutionPayloadEnvelope { pub payload: ExecutionPayloadGloas, pub execution_requests: ExecutionRequests, + // The builder index is private so that callers are forced to handle the case where it equals + // BUILDER_INDEX_SELF_BUILD. #[serde(with = "serde_utils::quoted_u64")] - pub builder_index: u64, + builder_index: u64, pub beacon_block_root: Hash256, pub slot: Slot, pub blob_kzg_commitments: KzgCommitments, @@ -27,6 +30,28 @@ pub struct ExecutionPayloadEnvelope { impl SignedRoot for ExecutionPayloadEnvelope {} +impl ExecutionPayloadEnvelope { + /// Fetch the validator index of the builder of this execution payload. + /// + /// This falls back to the provided `proposer_index` if the builder index indicates + /// self-building. + pub fn builder_index(&self, proposer_index: u64) -> u64 { + if self.builder_index == BUILDER_INDEX_SELF_BUILD { + proposer_index + } else { + self.builder_index + } + } + + /// Fetch the raw builder index, which may be `BUILDER_INDEX_SELF_BUILD` to indicate + /// self-building. + /// + /// This method should be used sparingly. + pub fn raw_builder_index(&self) -> u64 { + self.builder_index + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/execution/signed_execution_payload_envelope.rs b/consensus/types/src/execution/signed_execution_payload_envelope.rs index 1641041615..58f0ae0565 100644 --- a/consensus/types/src/execution/signed_execution_payload_envelope.rs +++ b/consensus/types/src/execution/signed_execution_payload_envelope.rs @@ -1,6 +1,9 @@ use crate::test_utils::TestRandom; -use crate::{EthSpec, ExecutionPayloadEnvelope}; -use bls::Signature; +use crate::{ + BeaconState, BeaconStateError, ChainSpec, Domain, Epoch, EthSpec, ExecutionBlockHash, + ExecutionPayloadEnvelope, Fork, Hash256, SignedRoot, Slot, +}; +use bls::{PublicKey, Signature}; use educe::Educe; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -15,6 +18,77 @@ pub struct SignedExecutionPayloadEnvelope { pub signature: Signature, } +impl SignedExecutionPayloadEnvelope { + pub fn slot(&self) -> Slot { + self.message.slot + } + + pub fn epoch(&self) -> Epoch { + self.slot().epoch(E::slots_per_epoch()) + } + + pub fn beacon_block_root(&self) -> Hash256 { + self.message.beacon_block_root + } + + pub fn block_hash(&self) -> ExecutionBlockHash { + self.message.payload.block_hash + } + + /// Verify `self.signature`. + /// + /// The `parent_state` is the post-state of the beacon block with + /// block_root = self.message.beacon_block_root + /// TODO(EIP-7732): maybe delete this function later (it is inefficient) + pub fn verify_signature_with_state( + &self, + parent_state: &BeaconState, + spec: &ChainSpec, + ) -> Result { + let proposer_index = parent_state.latest_block_header().proposer_index; + let builder_index = self.message.builder_index(proposer_index) as usize; + let domain = spec.get_domain( + parent_state.current_epoch(), + Domain::BeaconBuilder, + &parent_state.fork(), + parent_state.genesis_validators_root(), + ); + let pubkey = parent_state + .validators() + .get(builder_index) + .and_then(|v| { + let pk: Option = v.pubkey.decompress().ok(); + pk + }) + .ok_or(BeaconStateError::UnknownValidator(builder_index))?; + let message = self.message.signing_root(domain); + + Ok(self.signature.verify(&pubkey, message)) + } + + /// Verify `self.signature`. + pub fn verify_signature( + &self, + pubkey: &PublicKey, + fork: &Fork, + genesis_validators_root: Hash256, + spec: &ChainSpec, + ) -> bool { + // Signed envelopes using the new BeaconBuilder domain per the spec: + // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-verify_execution_payload_envelope_signature + let domain = spec.get_domain( + self.epoch(), + Domain::BeaconBuilder, + fork, + genesis_validators_root, + ); + + let message = self.message.signing_root(domain); + + self.signature.verify(pubkey, message) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index f3e5ae411b..3ca559c03b 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -25,7 +25,7 @@ use typenum::Unsigned; use crate::{ BuilderPendingPayment, BuilderPendingWithdrawal, ExecutionBlockHash, ExecutionPayloadBid, attestation::{ - AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, ParticipationFlags, + AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, PTC, ParticipationFlags, PendingAttestation, }, block::{BeaconBlock, BeaconBlockHeader, SignedBeaconBlockHash}, @@ -168,6 +168,7 @@ pub enum BeaconStateError { TotalActiveBalanceDiffUninitialized, GeneralizedIndexNotSupported(usize), IndexNotSupported(usize), + BuilderPendingPaymentsIndexNotSupported(usize), InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), PartialWithdrawalCountInvalid(usize), @@ -195,6 +196,8 @@ pub enum BeaconStateError { ProposerLookaheadOutOfBounds { i: usize, }, + InvalidIndicesCount, + PleaseNotifyTheDevs(String), } /// Control whether an epoch-indexed field can be indexed at the next epoch or not. @@ -1115,13 +1118,22 @@ impl BeaconState { } } + let gloas_enabled = self.fork_name_unchecked().gloas_enabled(); epoch .slot_iter(E::slots_per_epoch()) .map(|slot| { let mut preimage = seed.to_vec(); preimage.append(&mut int_to_bytes8(slot.as_u64())); let seed = hash(&preimage); - self.compute_proposer_index(indices, &seed, spec) + + if gloas_enabled { + self.compute_balance_weighted_selection(indices, &seed, 1, true, spec)? + .first() + .copied() + .ok_or(BeaconStateError::InsufficientValidators) + } else { + self.compute_proposer_index(indices, &seed, spec) + } }) .collect() } @@ -1378,39 +1390,50 @@ impl BeaconState { let epoch = self.current_epoch().safe_add(1)?; let active_validator_indices = self.get_active_validator_indices(epoch, spec)?; - let active_validator_count = active_validator_indices.len(); - let seed = self.get_seed(epoch, Domain::SyncCommittee, spec)?; - let max_effective_balance = spec.max_effective_balance_for_fork(self.fork_name_unchecked()); - let max_random_value = if self.fork_name_unchecked().electra_enabled() { - MAX_RANDOM_VALUE - } else { - MAX_RANDOM_BYTE - }; - let mut i = 0; - let mut sync_committee_indices = Vec::with_capacity(E::SyncCommitteeSize::to_usize()); - while sync_committee_indices.len() < E::SyncCommitteeSize::to_usize() { - let shuffled_index = compute_shuffled_index( - i.safe_rem(active_validator_count)?, - active_validator_count, + if self.fork_name_unchecked().gloas_enabled() { + self.compute_balance_weighted_selection( + &active_validator_indices, seed.as_slice(), - spec.shuffle_round_count, + E::SyncCommitteeSize::to_usize(), + true, + spec, ) - .ok_or(BeaconStateError::UnableToShuffle)?; - let candidate_index = *active_validator_indices - .get(shuffled_index) - .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(shuffled_index))?; - let random_value = self.shuffling_random_value(i, seed.as_slice())?; - let effective_balance = self.get_validator(candidate_index)?.effective_balance; - if effective_balance.safe_mul(max_random_value)? - >= max_effective_balance.safe_mul(random_value)? - { - sync_committee_indices.push(candidate_index); + } else { + let active_validator_count = active_validator_indices.len(); + let max_effective_balance = + spec.max_effective_balance_for_fork(self.fork_name_unchecked()); + let max_random_value = if self.fork_name_unchecked().electra_enabled() { + MAX_RANDOM_VALUE + } else { + MAX_RANDOM_BYTE + }; + + let mut i = 0; + let mut sync_committee_indices = Vec::with_capacity(E::SyncCommitteeSize::to_usize()); + while sync_committee_indices.len() < E::SyncCommitteeSize::to_usize() { + let shuffled_index = compute_shuffled_index( + i.safe_rem(active_validator_count)?, + active_validator_count, + seed.as_slice(), + spec.shuffle_round_count, + ) + .ok_or(BeaconStateError::UnableToShuffle)?; + let candidate_index = *active_validator_indices + .get(shuffled_index) + .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(shuffled_index))?; + let random_value = self.shuffling_random_value(i, seed.as_slice())?; + let effective_balance = self.get_validator(candidate_index)?.effective_balance; + if effective_balance.safe_mul(max_random_value)? + >= max_effective_balance.safe_mul(random_value)? + { + sync_committee_indices.push(candidate_index); + } + i.safe_add_assign(1)?; } - i.safe_add_assign(1)?; + Ok(sync_committee_indices) } - Ok(sync_committee_indices) } /// Compute the next sync committee. @@ -2292,6 +2315,7 @@ impl BeaconState { } } + /// Return true if the parent block was full (both beacon block and execution payload were present). pub fn is_parent_block_full(&self) -> bool { match self { BeaconState::Base(_) | BeaconState::Altair(_) => false, @@ -2594,11 +2618,16 @@ impl BeaconState { .map_err(Into::into) } + // TODO(EIP-7732): The consensus spec PR for this change mentions that some EF tests will be needed but haven't been created yet. + // We should integrate them once they are available. + // https://github.com/ethereum/consensus-specs/pull/4513 pub fn get_pending_balance_to_withdraw( &self, validator_index: usize, ) -> Result { let mut pending_balance = 0; + + // Sum pending partial withdrawals for withdrawal in self .pending_partial_withdrawals()? .iter() @@ -2606,6 +2635,27 @@ impl BeaconState { { pending_balance.safe_add_assign(withdrawal.amount)?; } + + // Sum builder pending withdrawals + if let Ok(builder_pending_withdrawals) = self.builder_pending_withdrawals() { + for withdrawal in builder_pending_withdrawals + .iter() + .filter(|withdrawal| withdrawal.builder_index as usize == validator_index) + { + pending_balance.safe_add_assign(withdrawal.amount)?; + } + } + + // Sum builder pending payments + if let Ok(builder_pending_payments) = self.builder_pending_payments() { + for payment in builder_pending_payments + .iter() + .filter(|payment| payment.withdrawal.builder_index as usize == validator_index) + { + pending_balance.safe_add_assign(payment.withdrawal.amount)?; + } + } + Ok(pending_balance) } @@ -2878,6 +2928,120 @@ impl BeaconState { Ok(()) } + + /// Get the PTC + /// Requires the committee cache to be initialized. + /// TODO(EIP-7732): definitely gonna have to cache this.. + pub fn get_ptc(&self, slot: Slot, spec: &ChainSpec) -> Result, BeaconStateError> { + let committee_cache = self.committee_cache_at_slot(slot)?; + let committees = committee_cache.get_beacon_committees_at_slot(slot)?; + + let seed = self.get_ptc_attester_seed(slot, spec)?; + + let committee_indices: Vec = committees + .iter() + .flat_map(|committee| committee.committee.iter().copied()) + .collect(); + let selected_indices = self.compute_balance_weighted_selection( + &committee_indices, + &seed, + E::ptc_size(), + false, + spec, + )?; + + Ok(PTC(FixedVector::new(selected_indices)?)) + } + + /// Compute the seed to use for the ptc attester selection at the given `slot`. + /// + /// Spec v0.12.1 + pub fn get_ptc_attester_seed( + &self, + slot: Slot, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { + let epoch = slot.epoch(E::slots_per_epoch()); + let mut preimage = self + .get_seed(epoch, Domain::PTCAttester, spec)? + .as_slice() + .to_vec(); + preimage.append(&mut int_to_bytes8(slot.as_u64())); + Ok(hash(&preimage)) + } + + /// Return size indices sampled by effective balance, using indices as candidates. + /// + /// If shuffle_indices is True, candidate indices are themselves sampled from indices + /// by shuffling it, otherwise indices is traversed in order. + fn compute_balance_weighted_selection( + &self, + indices: &[usize], + seed: &[u8], + size: usize, + shuffle_indices: bool, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { + let total = indices.len(); + if total == 0 { + return Err(BeaconStateError::InvalidIndicesCount); + } + + let mut selected = Vec::with_capacity(size); + let mut count = 0usize; + + while selected.len() < size { + let mut next_index = count.safe_rem(total)?; + + if shuffle_indices { + next_index = + compute_shuffled_index(next_index, total, seed, spec.shuffle_round_count) + .ok_or(BeaconStateError::UnableToShuffle)?; + } + + let candidate_index = indices + .get(next_index) + .ok_or(BeaconStateError::InvalidIndicesCount)?; + + if self.compute_balance_weighted_acceptance(*candidate_index, seed, count, spec)? { + selected.push(*candidate_index); + } + + count.safe_add_assign(1)?; + } + + Ok(selected) + } + + /// Return whether to accept the selection of the validator `index`, with probability + /// proportional to its `effective_balance`, and randomness given by `seed` and `iteration`. + fn compute_balance_weighted_acceptance( + &self, + index: usize, + seed: &[u8], + iteration: usize, + spec: &ChainSpec, + ) -> Result { + // TODO(EIP-7732): Consider grabbing effective balances from the epoch cache here. + // Note that this function will be used in a loop, so using cached values could be nice for performance. + // However, post-gloas, this function will be used in `compute_proposer_indices`, `get_next_sync_committee_indices`, and `get_ptc`, which has ~15 call sites in total + // so we will need to check each one to ensure epoch cache is initialized first, if we deem a good idea. + // Currently, we can't test if making the change would work since the test suite is not ready for gloas. + let effective_balance = self.get_effective_balance(index)?; + let max_effective_balance = spec.max_effective_balance_for_fork(self.fork_name_unchecked()); + + let random_value = self.shuffling_random_value(iteration, seed)?; + + // this codepath should technically never be hit pre-gloas, but added this defensively + let max_random_value = if self.fork_name_unchecked().electra_enabled() { + MAX_RANDOM_VALUE + } else { + MAX_RANDOM_BYTE + }; + + Ok(effective_balance.safe_mul(max_random_value)? + >= max_effective_balance.safe_mul(random_value)?) + } } impl ForkVersionDecode for BeaconState { diff --git a/consensus/types/src/validator/validator.rs b/consensus/types/src/validator/validator.rs index 7898ab9073..ce8ea4def8 100644 --- a/consensus/types/src/validator/validator.rs +++ b/consensus/types/src/validator/validator.rs @@ -165,13 +165,41 @@ impl Validator { } /// Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential. - pub fn has_compounding_withdrawal_credential(&self, spec: &ChainSpec) -> bool { + pub fn has_compounding_withdrawal_credential( + &self, + spec: &ChainSpec, + current_fork: ForkName, + ) -> bool { + if current_fork.gloas_enabled() { + self.has_compounding_withdrawal_credential_gloas(spec) + } else { + self.has_compounding_withdrawal_credential_electra(spec) + } + } + + /// Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential + pub fn has_compounding_withdrawal_credential_electra(&self, spec: &ChainSpec) -> bool { is_compounding_withdrawal_credential(self.withdrawal_credentials, spec) } + /// Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential or an 0x03 prefixed "builder" withdrawal credential + pub fn has_compounding_withdrawal_credential_gloas(&self, spec: &ChainSpec) -> bool { + is_compounding_withdrawal_credential(self.withdrawal_credentials, spec) + || is_builder_withdrawal_credential(self.withdrawal_credentials, spec) + } + + /// Check if ``validator`` has an 0x03 prefixed "builder" withdrawal credential. + pub fn has_builder_withdrawal_credential(&self, spec: &ChainSpec) -> bool { + is_builder_withdrawal_credential(self.withdrawal_credentials, spec) + } + /// Get the execution withdrawal address if this validator has one initialized. - pub fn get_execution_withdrawal_address(&self, spec: &ChainSpec) -> Option
{ - self.has_execution_withdrawal_credential(spec) + pub fn get_execution_withdrawal_address( + &self, + spec: &ChainSpec, + current_fork: ForkName, + ) -> Option
{ + self.has_execution_withdrawal_credential(spec, current_fork) .then(|| { self.withdrawal_credentials .as_slice() @@ -202,7 +230,7 @@ impl Validator { current_fork: ForkName, ) -> bool { if current_fork.electra_enabled() { - self.is_fully_withdrawable_validator_electra(balance, epoch, spec) + self.is_fully_withdrawable_validator_electra(balance, epoch, spec, current_fork) } else { self.is_fully_withdrawable_validator_capella(balance, epoch, spec) } @@ -226,8 +254,9 @@ impl Validator { balance: u64, epoch: Epoch, spec: &ChainSpec, + current_fork: ForkName, ) -> bool { - self.has_execution_withdrawal_credential(spec) + self.has_execution_withdrawal_credential(spec, current_fork) && self.withdrawable_epoch <= epoch && balance > 0 } @@ -267,21 +296,25 @@ impl Validator { let max_effective_balance = self.get_max_effective_balance(spec, current_fork); let has_max_effective_balance = self.effective_balance == max_effective_balance; let has_excess_balance = balance > max_effective_balance; - self.has_execution_withdrawal_credential(spec) + self.has_execution_withdrawal_credential(spec, current_fork) && has_max_effective_balance && has_excess_balance } /// Returns `true` if the validator has a 0x01 or 0x02 prefixed withdrawal credential. - pub fn has_execution_withdrawal_credential(&self, spec: &ChainSpec) -> bool { - self.has_compounding_withdrawal_credential(spec) + pub fn has_execution_withdrawal_credential( + &self, + spec: &ChainSpec, + current_fork: ForkName, + ) -> bool { + self.has_compounding_withdrawal_credential(spec, current_fork) || self.has_eth1_withdrawal_credential(spec) } /// Returns the max effective balance for a validator in gwei. pub fn get_max_effective_balance(&self, spec: &ChainSpec, current_fork: ForkName) -> u64 { if current_fork >= ForkName::Electra { - if self.has_compounding_withdrawal_credential(spec) { + if self.has_compounding_withdrawal_credential(spec, current_fork) { spec.max_effective_balance_electra } else { spec.min_activation_balance @@ -319,6 +352,15 @@ pub fn is_compounding_withdrawal_credential( .unwrap_or(false) } +/// Check if the withdrawal credential has the builder withdrawal prefix (0x03). +pub fn is_builder_withdrawal_credential(withdrawal_credentials: Hash256, spec: &ChainSpec) -> bool { + withdrawal_credentials + .as_slice() + .first() + .map(|prefix_byte| *prefix_byte == spec.builder_withdrawal_prefix_byte) + .unwrap_or(false) +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/tests/state.rs b/consensus/types/tests/state.rs index 63ab3b8084..54abf73f0f 100644 --- a/consensus/types/tests/state.rs +++ b/consensus/types/tests/state.rs @@ -55,6 +55,15 @@ async fn build_state(validator_count: usize) -> BeaconState { .head_beacon_state_cloned() } +// TODO(EIP-7732): Add tests for PTC (Payload Timeliness Committee) functions: +// - get_ptc: Test committee selection, size, balance-weighted selection +// - get_ptc_attester_seed: Test seed generation and determinism +// - compute_balance_weighted_selection: Test selection algorithm with various balances +// - compute_balance_weighted_acceptance: Test acceptance probability +// These tests require being able to build Gloas states with initialized committee caches, +// which currently fails due to incomplete Gloas block structure as mentioned here: +// https://github.com/sigp/lighthouse/pull/8273 +// Similar to existing committee_consistency_test suite for get_beacon_committee. async fn test_beacon_proposer_index() { let spec = E::default_spec(); diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index a53bce927c..63b46945c2 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -419,8 +419,15 @@ impl Operation for WithdrawalsPayload { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - // TODO(EIP-7732): implement separate gloas and non-gloas variants of process_withdrawals - process_withdrawals::<_, FullPayload<_>>(state, self.payload.to_ref(), spec) + if state.fork_name_unchecked().gloas_enabled() { + process_withdrawals::gloas::process_withdrawals(state, spec) + } else { + process_withdrawals::capella::process_withdrawals::<_, FullPayload<_>>( + state, + self.payload.to_ref(), + spec, + ) + } } }