diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9d16296f77..ec79153785 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4889,7 +4889,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(Into::into) .map_err(Error::PrepareProposerFailed); } @@ -4907,7 +4907,7 @@ impl BeaconChain { &self.spec, )?; get_expected_withdrawals(&advanced_state, &self.spec) - .map(|(withdrawals, _)| withdrawals) + .map(Into::into) .map_err(Error::PrepareProposerFailed) } diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 9459b1acd7..bdf3ab9594 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -371,7 +371,7 @@ pub fn get_execution_payload( let latest_execution_payload_header_block_hash = latest_execution_payload_header.block_hash(); let latest_execution_payload_header_gas_limit = latest_execution_payload_header.gas_limit(); let withdrawals = if state.fork_name_unchecked().capella_enabled() { - Some(get_expected_withdrawals(state, spec)?.0.into()) + Some(Withdrawals::::from(get_expected_withdrawals(state, spec)?).into()) } else { None }; diff --git a/beacon_node/http_api/src/builder_states.rs b/beacon_node/http_api/src/builder_states.rs index 7c05dd00d2..73e01debcd 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(expected_withdrawals) => Ok(expected_withdrawals.into()), Err(e) => Err(warp_utils::reject::custom_server_error(format!( "failed to get expected withdrawal: {:?}", e diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index b04c812773..21458057c4 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -634,7 +634,7 @@ pub async fn proposer_boost_re_org_test( assert_eq!(state_b.slot(), slot_b); let pre_advance_withdrawals = get_expected_withdrawals(&state_b, &harness.chain.spec) .unwrap() - .0 + .withdrawals() .to_vec(); complete_state_advance(&mut state_b, None, slot_c, &harness.chain.spec).unwrap(); @@ -724,7 +724,7 @@ pub async fn proposer_boost_re_org_test( get_expected_withdrawals(&state_b, &harness.chain.spec) } .unwrap() - .0 + .withdrawals() .to_vec(); let payload_attribs_withdrawals = payload_attribs.withdrawals().unwrap(); assert_eq!(expected_withdrawals, *payload_attribs_withdrawals); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index c60f572002..bef9fe6acd 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -6660,7 +6660,8 @@ impl ApiTester { } let expected_withdrawals = get_expected_withdrawals(&state, &self.chain.spec) .unwrap() - .0; + .withdrawals() + .to_vec(); // fetch expected withdrawals from the client let result = self.client.get_expected_withdrawals(&state_id).await; @@ -6668,7 +6669,7 @@ impl ApiTester { Ok(withdrawal_response) => { assert_eq!(withdrawal_response.execution_optimistic, Some(false)); assert_eq!(withdrawal_response.finalized, Some(false)); - assert_eq!(withdrawal_response.data, expected_withdrawals.to_vec()); + assert_eq!(withdrawal_response.data, expected_withdrawals); } Err(_) => { panic!("query failed incorrectly"); diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index f8d86cd06d..1de5083f6f 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -1,7 +1,7 @@ use crate::consensus_context::ConsensusContext; use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid}; use rayon::prelude::*; -use safe_arith::{ArithError, SafeArith, SafeArithIter}; +use safe_arith::{ArithError, SafeArith}; use signature_sets::{block_proposal_signature_set, get_pubkey_from_state, randao_signature_set}; use std::borrow::Cow; use tree_hash::TreeHash; @@ -24,9 +24,11 @@ pub use verify_deposit::{ get_existing_validator_index, is_valid_deposit_signature, verify_deposit_merkle_proof, }; pub use verify_exit::verify_exit; +pub use withdrawals::get_expected_withdrawals; pub mod altair; pub mod block_signature_verifier; +pub mod builder; pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; @@ -39,8 +41,8 @@ mod verify_bls_to_execution_change; mod verify_deposit; mod verify_exit; mod verify_proposer_slashing; +pub mod withdrawals; -use crate::common::decrease_balance; use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_metrics, }; @@ -172,14 +174,21 @@ 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() { + withdrawals::gloas::process_withdrawals::(state, spec)?; + // TODO(EIP-7732): process execution payload bid + } else { + if state.fork_name_unchecked().capella_enabled() { + withdrawals::capella_electra::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)?; @@ -513,190 +522,3 @@ pub fn compute_timestamp_at_slot( .safe_mul(spec.get_slot_duration().as_secs()) .and_then(|since_genesis| state.genesis_time().safe_add(since_genesis)) } - -/// 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 -pub fn get_expected_withdrawals( - state: &BeaconState, - spec: &ChainSpec, -) -> Result<(Withdrawals, 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 Electra:EIP7251] - // Consume pending partial withdrawals - let processed_partial_withdrawals_count = - if let Ok(pending_partial_withdrawals) = state.pending_partial_withdrawals() { - let mut processed_partial_withdrawals_count = 0; - for withdrawal in pending_partial_withdrawals { - if withdrawal.withdrawable_epoch > epoch - || withdrawals.len() == spec.max_pending_partials_per_withdrawals_sweep as usize - { - break; - } - - let validator = state.get_validator(withdrawal.validator_index as usize)?; - - let has_sufficient_effective_balance = - validator.effective_balance >= spec.min_activation_balance; - let total_withdrawn = withdrawals - .iter() - .filter_map(|w| { - (w.validator_index == withdrawal.validator_index).then_some(w.amount) - }) - .safe_sum()?; - let balance = state - .get_balance(withdrawal.validator_index as usize)? - .safe_sub(total_withdrawn)?; - let has_excess_balance = balance > spec.min_activation_balance; - - if validator.exit_epoch == spec.far_future_epoch - && has_sufficient_effective_balance - && has_excess_balance - { - let withdrawable_balance = std::cmp::min( - balance.safe_sub(spec.min_activation_balance)?, - withdrawal.amount, - ); - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index: withdrawal.validator_index, - address: validator - .get_execution_withdrawal_address(spec) - .ok_or(BeaconStateError::NonExecutionAddressWithdrawalCredential)?, - amount: withdrawable_balance, - }); - withdrawal_index.safe_add_assign(1)?; - } - processed_partial_withdrawals_count.safe_add_assign(1)?; - } - Some(processed_partial_withdrawals_count) - } else { - None - }; - - let bound = std::cmp::min( - state.validators().len() as u64, - spec.max_validators_per_withdrawals_sweep, - ); - for _ in 0..bound { - let validator = state.get_validator(validator_index as usize)?; - let partially_withdrawn_balance = withdrawals - .iter() - .filter_map(|withdrawal| { - (withdrawal.validator_index == validator_index).then_some(withdrawal.amount) - }) - .safe_sum()?; - let balance = state - .balances() - .get(validator_index as usize) - .ok_or(BeaconStateError::BalancesOutOfBounds( - validator_index as usize, - ))? - .safe_sub(partially_withdrawn_balance)?; - if validator.is_fully_withdrawable_validator(balance, epoch, spec, fork_name) { - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index, - address: validator - .get_execution_withdrawal_address(spec) - .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, - amount: balance, - }); - withdrawal_index.safe_add_assign(1)?; - } else if validator.is_partially_withdrawable_validator(balance, spec, fork_name) { - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index, - address: validator - .get_execution_withdrawal_address(spec) - .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, - amount: balance.safe_sub(validator.get_max_effective_balance(spec, fork_name))?, - }); - withdrawal_index.safe_add_assign(1)?; - } - if withdrawals.len() == E::max_withdrawals_per_payload() { - break; - } - validator_index = validator_index - .safe_add(1)? - .safe_rem(state.validators().len() as u64)?; - } - - Ok(( - withdrawals - .try_into() - .map_err(BlockProcessingError::SszTypesError)?, - 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(()) - } -} diff --git a/consensus/state_processing/src/per_block_processing/builder.rs b/consensus/state_processing/src/per_block_processing/builder.rs new file mode 100644 index 0000000000..cbaac92c64 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/builder.rs @@ -0,0 +1,13 @@ +use types::{builder::BuilderIndex, consts::gloas::BUILDER_INDEX_FLAG}; + +pub fn is_builder_index(validator_index: u64) -> bool { + validator_index & BUILDER_INDEX_FLAG != 0 +} + +pub fn convert_builder_index_to_validator_index(builder_index: BuilderIndex) -> u64 { + builder_index | BUILDER_INDEX_FLAG +} + +pub fn convert_validator_index_to_builder_index(validator_index: u64) -> BuilderIndex { + validator_index & !BUILDER_INDEX_FLAG +} diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index ff7c0204e2..d0cf7b46d9 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -90,6 +90,14 @@ pub enum BlockProcessingError { found: Hash256, }, WithdrawalCredentialsInvalid, + /// This should be unreachable unless there's a logical flaw in the spec for withdrawals. + WithdrawalsLimitExceeded { + limit: usize, + prior_withdrawals: usize, + }, + /// Unreachable unless there's a logic error in LH. + IncorrectExpectedWithdrawalsVariant, + MissingLastWithdrawal, PendingAttestationInElectra, } diff --git a/consensus/state_processing/src/per_block_processing/withdrawals.rs b/consensus/state_processing/src/per_block_processing/withdrawals.rs new file mode 100644 index 0000000000..39ad4efc5c --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/withdrawals.rs @@ -0,0 +1,543 @@ +use crate::common::decrease_balance; +use crate::per_block_processing::builder::{ + convert_builder_index_to_validator_index, convert_validator_index_to_builder_index, + is_builder_index, +}; +use crate::per_block_processing::errors::BlockProcessingError; +use milhouse::List; +use safe_arith::{SafeArith, SafeArithIter}; +use tree_hash::TreeHash; +use types::{ + AbstractExecPayload, BeaconState, BeaconStateError, ChainSpec, EthSpec, ExecPayload, + ExpectedWithdrawals, ExpectedWithdrawalsCapella, ExpectedWithdrawalsElectra, + ExpectedWithdrawalsGloas, Validator, Withdrawal, Withdrawals, +}; + +/// Compute the next batch of withdrawals which should be included in a block. +/// +/// 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, BlockProcessingError> { + let mut withdrawal_index = state.next_withdrawal_index()?; + let mut withdrawals = Vec::::with_capacity(E::max_withdrawals_per_payload()); + + // [New in Gloas:EIP7732] + // Get builder withdrawals + let processed_builder_withdrawals_count = + get_builder_withdrawals(state, &mut withdrawal_index, &mut withdrawals)?; + + // [New in Electra:EIP7251] + // Get partial withdrawals. + let processed_partial_withdrawals_count = + get_pending_partial_withdrawals(state, &mut withdrawal_index, &mut withdrawals, spec)?; + + // [New in Gloas:EIP7732] + // Get builders sweep withdrawals + let processed_builders_sweep_count = + get_builders_sweep_withdrawals(state, &mut withdrawal_index, &mut withdrawals)?; + + // Get validators sweep withdrawals + let processed_sweep_withdrawals_count = + get_validators_sweep_withdrawals(state, &mut withdrawal_index, &mut withdrawals, spec)?; + + let withdrawals = withdrawals + .try_into() + .map_err(BlockProcessingError::SszTypesError)?; + + let fork_name = state.fork_name_unchecked(); + if fork_name.gloas_enabled() { + Ok(ExpectedWithdrawals::Gloas(ExpectedWithdrawalsGloas { + withdrawals, + processed_builder_withdrawals_count: processed_builder_withdrawals_count + .ok_or(BlockProcessingError::IncorrectExpectedWithdrawalsVariant)?, + processed_partial_withdrawals_count: processed_partial_withdrawals_count + .ok_or(BlockProcessingError::IncorrectExpectedWithdrawalsVariant)?, + processed_builders_sweep_count: processed_builders_sweep_count + .ok_or(BlockProcessingError::IncorrectExpectedWithdrawalsVariant)?, + processed_sweep_withdrawals_count, + })) + } else if fork_name.electra_enabled() { + Ok(ExpectedWithdrawals::Electra(ExpectedWithdrawalsElectra { + withdrawals, + processed_partial_withdrawals_count: processed_partial_withdrawals_count + .ok_or(BlockProcessingError::IncorrectExpectedWithdrawalsVariant)?, + processed_sweep_withdrawals_count, + })) + } else { + Ok(ExpectedWithdrawals::Capella(ExpectedWithdrawalsCapella { + withdrawals, + processed_sweep_withdrawals_count, + })) + } +} + +pub fn get_builder_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, +) -> Result, BlockProcessingError> { + let Ok(builder_pending_withdrawals) = state.builder_pending_withdrawals() else { + // Pre-Gloas, nothing to do. + return Ok(None); + }; + + // TODO(gloas): this has already changed on `master`, we need to update at next spec release + let withdrawals_limit = E::max_withdrawals_per_payload(); + + // TODO(gloas): this assert is from `master`, remove this comment once it is part of the tested + // spec version. + block_verify!( + withdrawals.len() <= withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count = 0; + for withdrawal in builder_pending_withdrawals { + let has_reached_limit = withdrawals.len() == withdrawals_limit; + + if has_reached_limit { + break; + } + + let builder_index = withdrawal.builder_index; + + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index: convert_builder_index_to_validator_index(builder_index), + address: withdrawal.fee_recipient, + amount: withdrawal.amount, + }); + withdrawal_index.safe_add_assign(1)?; + processed_count.safe_add_assign(1)?; + } + Ok(Some(processed_count)) +} + +pub fn get_pending_partial_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, + spec: &ChainSpec, +) -> Result, BlockProcessingError> { + let Ok(pending_partial_withdrawals) = state.pending_partial_withdrawals() else { + // Pre-Electra nothing to do. + return Ok(None); + }; + let epoch = state.current_epoch(); + + let withdrawals_limit = std::cmp::min( + withdrawals + .len() + .safe_add(spec.max_pending_partials_per_withdrawals_sweep as usize)?, + E::max_withdrawals_per_payload().safe_sub(1)?, + ); + + // TODO(gloas): this assert is from `master`, remove this comment once it is part of the tested + // spec version. + block_verify!( + withdrawals.len() <= withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count = 0; + for withdrawal in pending_partial_withdrawals { + let is_withdrawable = withdrawal.withdrawable_epoch <= epoch; + let has_reached_limit = withdrawals.len() >= withdrawals_limit; + + if !is_withdrawable || has_reached_limit { + break; + } + + let validator_index = withdrawal.validator_index; + let validator = state.get_validator(validator_index as usize)?; + let balance = get_balance_after_withdrawals(state, validator_index, withdrawals)?; + + if is_eligible_for_partial_withdrawals(validator, balance, spec) { + let withdrawal_amount = std::cmp::min( + balance.safe_sub(spec.min_activation_balance)?, + withdrawal.amount, + ); + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index, + address: validator + .get_execution_withdrawal_address(spec) + .ok_or(BeaconStateError::NonExecutionAddressWithdrawalCredential)?, + amount: withdrawal_amount, + }); + withdrawal_index.safe_add_assign(1)?; + } + processed_count.safe_add_assign(1)?; + } + + Ok(Some(processed_count)) +} + +/// Get withdrawals from the builders sweep. +/// +/// This function iterates through builders starting from `next_withdrawal_builder_index` +/// and adds withdrawals for builders whose withdrawable_epoch has been reached and have balance. +/// +/// https://ethereum.github.io/consensus-specs/specs/gloas/beacon-chain/#new-get_builders_sweep_withdrawals +pub fn get_builders_sweep_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, +) -> Result, BlockProcessingError> { + let Ok(builders) = state.builders() else { + // Pre-Gloas, nothing to do. + return Ok(None); + }; + + if builders.is_empty() { + return Ok(Some(0)); + } + + let epoch = state.current_epoch(); + let builders_limit = std::cmp::min(builders.len(), E::max_builders_per_withdrawals_sweep()); + + // TODO(gloas): this has already changed on `master`, we should update at the next spec release + let withdrawals_limit = E::max_withdrawals_per_payload(); + + // TODO(gloas): this assert is from `master`, remove this comment once it is part of the tested + // spec version. + block_verify!( + withdrawals.len() <= withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count: u64 = 0; + let mut builder_index = state.next_withdrawal_builder_index()?; + + for _ in 0..builders_limit { + if withdrawals.len() >= withdrawals_limit { + break; + } + + let builder = builders + .get(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))?; + + if builder.withdrawable_epoch <= epoch && builder.balance > 0 { + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index: convert_builder_index_to_validator_index(builder_index), + address: builder.execution_address, + amount: builder.balance, + }); + withdrawal_index.safe_add_assign(1)?; + } + + builder_index = builder_index.safe_add(1)?.safe_rem(builders.len() as u64)?; + processed_count.safe_add_assign(1)?; + } + + Ok(Some(processed_count)) +} + +/// Get withdrawals from the validator sweep. +/// +/// This function iterates through validators starting from `next_withdrawal_validator_index` +/// and adds full or partial withdrawals for eligible validators. +/// +/// https://ethereum.github.io/consensus-specs/specs/capella/beacon-chain/#new-get_validators_sweep_withdrawals +pub fn get_validators_sweep_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, + spec: &ChainSpec, +) -> Result { + let epoch = state.current_epoch(); + let fork_name = state.fork_name_unchecked(); + let mut validator_index = state.next_withdrawal_validator_index()?; + let validators_limit = std::cmp::min( + state.validators().len() as u64, + spec.max_validators_per_withdrawals_sweep, + ); + let withdrawals_limit = E::max_withdrawals_per_payload(); + + // There must be at least one space reserved for validator sweep withdrawals + block_verify!( + withdrawals.len() < withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count: u64 = 0; + + for _ in 0..validators_limit { + if withdrawals.len() >= withdrawals_limit { + break; + } + + let validator = state.get_validator(validator_index as usize)?; + let balance = get_balance_after_withdrawals(state, validator_index, withdrawals)?; + + if validator.is_fully_withdrawable_validator(balance, epoch, spec, fork_name) { + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index, + address: validator + .get_execution_withdrawal_address(spec) + .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, + amount: balance, + }); + withdrawal_index.safe_add_assign(1)?; + } else if validator.is_partially_withdrawable_validator(balance, spec, fork_name) { + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index, + address: validator + .get_execution_withdrawal_address(spec) + .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, + amount: balance.safe_sub(validator.get_max_effective_balance(spec, fork_name))?, + }); + withdrawal_index.safe_add_assign(1)?; + } + + validator_index = validator_index + .safe_add(1)? + .safe_rem(state.validators().len() as u64)?; + processed_count.safe_add_assign(1)?; + } + + Ok(processed_count) +} + +pub fn get_balance_after_withdrawals( + state: &BeaconState, + validator_index: u64, + withdrawals: &[Withdrawal], +) -> Result { + let withdrawn = withdrawals + .iter() + .filter(|withdrawal| withdrawal.validator_index == validator_index) + .map(|withdrawal| withdrawal.amount) + .safe_sum()?; + state + .get_balance(validator_index as usize)? + .safe_sub(withdrawn) + .map_err(Into::into) +} + +fn is_eligible_for_partial_withdrawals( + validator: &Validator, + balance: u64, + spec: &ChainSpec, +) -> bool { + let has_sufficient_effective_balance = + validator.effective_balance >= spec.min_activation_balance; + let has_excess_balance = balance > spec.min_activation_balance; + validator.exit_epoch == spec.far_future_epoch + && has_sufficient_effective_balance + && has_excess_balance +} + +fn update_next_withdrawal_index( + state: &mut BeaconState, + withdrawals: &Withdrawals, +) -> Result<(), BlockProcessingError> { + // Update the next withdrawal index if this block contained withdrawals + if let Some(latest_withdrawal) = withdrawals.last() { + *state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?; + } + Ok(()) +} + +fn update_payload_expected_withdrawals( + state: &mut BeaconState, + withdrawals: &Withdrawals, +) -> Result<(), BlockProcessingError> { + *state.payload_expected_withdrawals_mut()? = List::new(withdrawals.to_vec())?; + Ok(()) +} + +fn update_builder_pending_withdrawals( + state: &mut BeaconState, + processed_builder_withdrawals_count: u64, +) -> Result<(), BlockProcessingError> { + state + .builder_pending_withdrawals_mut()? + .pop_front(processed_builder_withdrawals_count as usize)?; + Ok(()) +} + +fn update_pending_partial_withdrawals( + state: &mut BeaconState, + processed_partial_withdrawals_count: u64, +) -> Result<(), BlockProcessingError> { + state + .pending_partial_withdrawals_mut()? + .pop_front(processed_partial_withdrawals_count as usize)?; + Ok(()) +} + +fn update_next_withdrawal_builder_index( + state: &mut BeaconState, + processed_builders_sweep_count: u64, +) -> Result<(), BlockProcessingError> { + if !state.builders()?.is_empty() { + // Update the next builder index to start the next withdrawal sweep + let next_index = state + .next_withdrawal_builder_index()? + .safe_add(processed_builders_sweep_count)?; + let next_builder_index = next_index.safe_rem(state.builders()?.len() as u64)?; + *state.next_withdrawal_builder_index_mut()? = next_builder_index; + } + Ok(()) +} + +fn update_next_withdrawal_validator_index( + state: &mut BeaconState, + withdrawals: &Withdrawals, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + // Update the next validator index to start the next withdrawal sweep + if withdrawals.len() == E::max_withdrawals_per_payload() { + // Next sweep starts after the latest withdrawal's validator index + let latest_withdrawal = withdrawals + .last() + .ok_or(BlockProcessingError::MissingLastWithdrawal)?; + 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; + } else { + // Advance sweep by the max length of the sweep if there was not a full set of withdrawals + 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(()) +} + +pub fn apply_withdrawals( + state: &mut BeaconState, + withdrawals: &Withdrawals, +) -> Result<(), BlockProcessingError> { + for withdrawal in withdrawals { + if state.fork_name_unchecked().gloas_enabled() + && is_builder_index(withdrawal.validator_index) + { + let builder_index = + convert_validator_index_to_builder_index(withdrawal.validator_index); + let builder = state + .builders_mut()? + .get_mut(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))?; + builder.balance = builder.balance.saturating_sub(withdrawal.amount); + } else { + decrease_balance( + state, + withdrawal.validator_index as usize, + withdrawal.amount, + )?; + } + } + Ok(()) +} + +pub mod capella_electra { + use super::*; + + /// Apply withdrawals to the state. + pub fn process_withdrawals>( + state: &mut BeaconState, + payload: Payload::Ref<'_>, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + let expected_withdrawals = get_expected_withdrawals(state, spec)?; + + let expected_root = expected_withdrawals.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, + }); + } + + // Apply expected withdrawals. + apply_withdrawals(state, expected_withdrawals.withdrawals())?; + + // [Common] Update withdrawals fields in the state + update_next_withdrawal_index(state, expected_withdrawals.withdrawals())?; + + // [New in Electra:EIP7251] + if let Ok(processed_partial_withdrawals_count) = + expected_withdrawals.processed_partial_withdrawals_count() + { + update_pending_partial_withdrawals(state, processed_partial_withdrawals_count)?; + } + + // [Common from Capella] + update_next_withdrawal_validator_index(state, expected_withdrawals.withdrawals(), spec)?; + + Ok(()) + } +} + +pub mod gloas { + use super::*; + + /// 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 ExpectedWithdrawals::Gloas(ExpectedWithdrawalsGloas { + withdrawals, + processed_builder_withdrawals_count, + processed_partial_withdrawals_count, + processed_builders_sweep_count, + processed_sweep_withdrawals_count: _, + }) = get_expected_withdrawals(state, spec)? + else { + return Err(BlockProcessingError::IncorrectExpectedWithdrawalsVariant); + }; + + // Apply expected withdrawals. + apply_withdrawals(state, &withdrawals)?; + + // [Common] Update withdrawals fields in the state + update_next_withdrawal_index(state, &withdrawals)?; + + // [New in Gloas:EIP7732] + update_payload_expected_withdrawals(state, &withdrawals)?; + + // [New in Gloas:EIP7732] + update_builder_pending_withdrawals(state, processed_builder_withdrawals_count)?; + + // [Common from Electra] + update_pending_partial_withdrawals(state, processed_partial_withdrawals_count)?; + + // [New in Gloas:EIP7732] + update_next_withdrawal_builder_index(state, processed_builders_sweep_count)?; + + // [Common from Capella] + update_next_withdrawal_validator_index(state, &withdrawals, spec)?; + + Ok(()) + } +} diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 3f8fa4cfff..04d9a1aea8 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -23,13 +23,13 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - Builder, BuilderIndex, BuilderPendingPayment, BuilderPendingWithdrawal, ExecutionBlockHash, - ExecutionPayloadBid, Withdrawal, + ExecutionBlockHash, ExecutionPayloadBid, Withdrawal, attestation::{ AttestationData, AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, PTC, ParticipationFlags, PendingAttestation, }, block::{BeaconBlock, BeaconBlockHeader, SignedBeaconBlockHash}, + builder::{Builder, BuilderIndex, BuilderPendingPayment, BuilderPendingWithdrawal}, consolidation::PendingConsolidation, core::{ChainSpec, Domain, Epoch, EthSpec, Hash256, RelativeEpoch, RelativeEpochError, Slot}, deposit::PendingDeposit, @@ -68,6 +68,7 @@ pub enum BeaconStateError { EpochOutOfBounds, SlotOutOfBounds, UnknownValidator(usize), + UnknownBuilder(BuilderIndex), UnableToDetermineProducer, InvalidBitfield, EmptyCommittee, diff --git a/consensus/types/src/withdrawal/expected_withdrawals.rs b/consensus/types/src/withdrawal/expected_withdrawals.rs new file mode 100644 index 0000000000..f9809e6e73 --- /dev/null +++ b/consensus/types/src/withdrawal/expected_withdrawals.rs @@ -0,0 +1,29 @@ +use crate::{EthSpec, Withdrawals}; +use superstruct::superstruct; + +#[superstruct( + variants(Capella, Electra, Gloas), + variant_attributes(derive(Debug, PartialEq, Clone)) +)] +#[derive(Debug, PartialEq, Clone)] +pub struct ExpectedWithdrawals { + pub withdrawals: Withdrawals, + #[superstruct(only(Gloas), partial_getter(copy))] + pub processed_builder_withdrawals_count: u64, + #[superstruct(only(Electra, Gloas), partial_getter(copy))] + pub processed_partial_withdrawals_count: u64, + #[superstruct(only(Gloas), partial_getter(copy))] + pub processed_builders_sweep_count: u64, + #[superstruct(getter(copy))] + pub processed_sweep_withdrawals_count: u64, +} + +impl From> for Withdrawals { + fn from(expected_withdrawals: ExpectedWithdrawals) -> Withdrawals { + match expected_withdrawals { + ExpectedWithdrawals::Capella(ew) => ew.withdrawals, + ExpectedWithdrawals::Electra(ew) => ew.withdrawals, + ExpectedWithdrawals::Gloas(ew) => ew.withdrawals, + } + } +} diff --git a/consensus/types/src/withdrawal/mod.rs b/consensus/types/src/withdrawal/mod.rs index bac80d00be..fbe7351754 100644 --- a/consensus/types/src/withdrawal/mod.rs +++ b/consensus/types/src/withdrawal/mod.rs @@ -1,8 +1,13 @@ +mod expected_withdrawals; mod pending_partial_withdrawal; mod withdrawal; mod withdrawal_credentials; mod withdrawal_request; +pub use expected_withdrawals::{ + ExpectedWithdrawals, ExpectedWithdrawalsCapella, ExpectedWithdrawalsElectra, + ExpectedWithdrawalsGloas, +}; pub use pending_partial_withdrawal::PendingPartialWithdrawal; pub use withdrawal::{Withdrawal, Withdrawals}; pub use withdrawal_credentials::WithdrawalCredentials; diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index a53bce927c..2c7a385bd5 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -19,7 +19,7 @@ use state_processing::{ altair_deneb, base, process_attester_slashings, process_bls_to_execution_changes, process_deposits, process_exits, process_proposer_slashings, }, - process_sync_aggregate, process_withdrawals, + process_sync_aggregate, withdrawals, }, }; use std::fmt::Debug; @@ -45,7 +45,7 @@ struct ExecutionMetadata { /// Newtype for testing withdrawals. #[derive(Debug, Clone, Deserialize)] pub struct WithdrawalsPayload { - payload: FullPayload, + payload: ExecutionPayload, } #[derive(Debug, Clone)] @@ -408,9 +408,7 @@ impl Operation for WithdrawalsPayload { ssz_decode_file_with(path, |bytes| { ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) }) - .map(|payload| WithdrawalsPayload { - payload: payload.into(), - }) + .map(|payload| WithdrawalsPayload { payload }) } fn apply_to( @@ -419,8 +417,16 @@ 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() { + withdrawals::gloas::process_withdrawals(state, spec) + } else { + let full_payload = FullPayload::from(self.payload.clone()); + withdrawals::capella_electra::process_withdrawals::<_, FullPayload<_>>( + state, + full_payload.to_ref(), + spec, + ) + } } } diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index afa6304eae..86d317b564 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1117,6 +1117,17 @@ impl> Handler for OperationsHandler fn handler_name(&self) -> String { O::handler_name() } + + fn disabled_forks(&self) -> Vec { + // TODO(gloas): Can be removed once we enable Gloas on all tests + vec![] + } + + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { + // TODO(gloas): So far only withdrawals tests are enabled for Gloas. + Self::Case::is_enabled_for_fork(fork_name) + && (!fork_name.gloas_enabled() || self.handler_name() == "withdrawals") + } } #[derive(Educe)]