From 5c8acea30563890e1420ea49e8f8b5c6f4f29fd6 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 20 Jan 2026 16:25:24 +1100 Subject: [PATCH] Update `get_expected_withdrawals` to return `processed_validators_sweep_count`. --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 +- beacon_node/http_api/src/builder_states.rs | 2 +- .../src/per_block_processing.rs | 128 +++++++++++------- .../process_withdrawals.rs | 4 +- testing/ef_tests/src/cases/operations.rs | 4 +- testing/ef_tests/tests/tests.rs | 6 +- 6 files changed, 87 insertions(+), 61 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 1ffde9c11e..3f0380e43f 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/http_api/src/builder_states.rs b/beacon_node/http_api/src/builder_states.rs index 74228961fb..a5a8c28fce 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 bd16d84f94..c9b193ff7a 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -659,6 +659,77 @@ pub fn get_pending_partial_withdrawals( 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, fork_name) + .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, fork_name) + .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) +} + /// 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 @@ -666,13 +737,9 @@ pub fn get_pending_partial_withdrawals( pub fn get_expected_withdrawals( state: &BeaconState, spec: &ChainSpec, -) -> Result<(Withdrawals, Option, Option), BlockProcessingError> { - let epoch = state.current_epoch(); +) -> Result<(Withdrawals, Option, Option, u64), BlockProcessingError> { let mut withdrawal_index = state.next_withdrawal_index()?; let mut withdrawals = Vec::::with_capacity(E::max_withdrawals_per_payload()); - let fork_name = state.fork_name_unchecked(); - - let mut validator_index = state.next_withdrawal_validator_index()?; // [New in Gloas:EIP7732] // Get builder withdrawals @@ -684,53 +751,9 @@ pub fn get_expected_withdrawals( let processed_partial_withdrawals_count = get_pending_partial_withdrawals(state, &mut withdrawal_index, &mut withdrawals, spec)?; - 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, state.fork_name_unchecked()) - .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, state.fork_name_unchecked()) - .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)?; - } + // Get validators sweep withdrawals + let processed_validators_sweep_count = + get_validators_sweep_withdrawals(state, &mut withdrawal_index, &mut withdrawals, spec)?; Ok(( withdrawals @@ -738,6 +761,7 @@ pub fn get_expected_withdrawals( .map_err(BlockProcessingError::SszTypesError)?, processed_builder_withdrawals_count, processed_partial_withdrawals_count, + processed_validators_sweep_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 index ed72dd9205..a4ca222186 100644 --- a/consensus/state_processing/src/per_block_processing/process_withdrawals.rs +++ b/consensus/state_processing/src/per_block_processing/process_withdrawals.rs @@ -82,7 +82,7 @@ pub mod capella { ) -> 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) = + let (expected_withdrawals, _, partial_withdrawals_count, _) = get_expected_withdrawals(state, spec)?; let expected_root = expected_withdrawals.tree_hash_root(); @@ -127,7 +127,7 @@ pub mod gloas { return Ok(()); } - let (expected_withdrawals, builder_withdrawals_count, partial_withdrawals_count) = + let (expected_withdrawals, builder_withdrawals_count, partial_withdrawals_count, _) = get_expected_withdrawals(state, spec)?; for withdrawal in expected_withdrawals.iter() { diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index b1c8a88c09..229350041f 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -28,8 +28,8 @@ use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconState, BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, - ForkVersionDecode, FullPayload, PayloadAttestation, ProposerSlashing, SignedBlsToExecutionChange, - SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, + ForkVersionDecode, FullPayload, PayloadAttestation, ProposerSlashing, + SignedBlsToExecutionChange, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 1d5de6981e..3f104dff8d 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -378,8 +378,10 @@ mod ssz_static { .run(); SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); - SszStaticHandler::, MinimalEthSpec>::gloas_only().run(); - SszStaticHandler::, MainnetEthSpec>::gloas_only().run(); + SszStaticHandler::, MinimalEthSpec>::gloas_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_only() + .run(); } // Altair and later