diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index c985f937a5..853b8def76 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4832,7 +4832,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); } @@ -4850,7 +4850,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/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/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 5335c917cb..7b16dbe804 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -30,6 +30,7 @@ pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; pub mod process_operations; +pub mod process_withdrawals; pub mod signature_sets; pub mod tests; mod verify_attestation; @@ -39,7 +40,6 @@ mod verify_deposit; mod verify_exit; mod verify_proposer_slashing; -use crate::common::decrease_balance; use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_metrics, }; @@ -171,13 +171,20 @@ 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)?; - // TODO(EIP-7732): build out process_execution_bid - // process_execution_bid(state, block, verify_signatures, spec)?; + // TODO(EIP-7732): build out process_execution_bid + // process_execution_bid(state, block, verify_signatures, spec)?; + } else { + process_withdrawals::capella::process_withdrawals::( + state, + body.execution_payload()?, + spec, + )?; + process_execution_payload::(state, body, spec)?; + } + } process_randao(state, block, verify_randao, ctxt, spec)?; process_eth1_data(state, block.body().eth1_data())?; @@ -515,17 +522,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 = @@ -626,71 +686,9 @@ pub fn get_expected_withdrawals( .safe_rem(state.validators().len() as u64)?; } - Ok((withdrawals.into(), processed_partial_withdrawals_count)) -} - -/// Apply withdrawals to the state. -/// TODO(EIP-7732): abstract this out and create gloas variant -pub fn process_withdrawals>( - state: &mut BeaconState, - payload: Payload::Ref<'_>, - 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()?; - - 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, - )?; - } - - // 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(()) - } else { - // these shouldn't even be encountered but they're here for completeness - Ok(()) - } + Ok(( + withdrawals.into(), + processed_builder_withdrawals_count, + processed_partial_withdrawals_count, + )) } 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..4082403dc1 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/process_withdrawals.rs @@ -0,0 +1,166 @@ +use super::errors::BlockProcessingError; +use super::get_expected_withdrawals; +use crate::common::decrease_balance; +use safe_arith::SafeArith; +use tree_hash::TreeHash; +use types::{ + AbstractExecPayload, BeaconState, BuilderPendingWithdrawal, ChainSpec, EthSpec, ExecPayload, + List, 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/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 4db17aea5c..4aaf899844 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -2204,6 +2204,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, 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, + ) + } } }