Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4830,7 +4830,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
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);
}

Expand All @@ -4848,7 +4848,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
&self.spec,
)?;
get_expected_withdrawals(&advanced_state, &self.spec)
.map(|(withdrawals, _)| withdrawals)
.map(Into::into)
.map_err(Error::PrepareProposerFailed)
}

Expand Down
2 changes: 1 addition & 1 deletion beacon_node/beacon_chain/src/execution_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ pub fn get_execution_payload<T: BeaconChainTypes>(
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::<T::EthSpec>::from(get_expected_withdrawals(state, spec)?).into())
} else {
None
};
Expand Down
2 changes: 1 addition & 1 deletion beacon_node/http_api/src/builder_states.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub fn get_next_withdrawals<T: BeaconChainTypes>(
}

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
Expand Down
4 changes: 2 additions & 2 deletions beacon_node/http_api/tests/interactive_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions beacon_node/http_api/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6655,15 +6655,16 @@ 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;
match result {
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");
Expand Down
212 changes: 17 additions & 195 deletions consensus/state_processing/src/per_block_processing.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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,
};
Expand Down Expand Up @@ -172,14 +174,21 @@ pub fn per_block_processing<E: EthSpec, Payload: AbstractExecPayload<E>>(
// 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::<E, Payload>(state, body.execution_payload()?, spec)?;
process_execution_payload::<E, Payload>(state, body, spec)?;
if state.fork_name_unchecked().gloas_enabled() {
withdrawals::gloas::process_withdrawals::<E>(state, spec)?;
// TODO(EIP-7732): process execution payload bid
} else {
if state.fork_name_unchecked().capella_enabled() {
withdrawals::capella_electra::process_withdrawals::<E, Payload>(
state,
body.execution_payload()?,
spec,
)?;
}
process_execution_payload::<E, 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)?;
Expand Down Expand Up @@ -513,190 +522,3 @@ pub fn compute_timestamp_at_slot<E: EthSpec>(
.safe_mul(spec.seconds_per_slot)
.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<E: EthSpec>(
state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<(Withdrawals<E>, Option<usize>), 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::<Withdrawal>::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<E: EthSpec, Payload: AbstractExecPayload<E>>(
state: &mut BeaconState<E>,
payload: Payload::Ref<'_>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
if state.fork_name_unchecked().capella_enabled() {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved this check up to process_block/per_block_processing so that this function doesn't just no-op in case Capella is disabled. We should not be calling process_withdrawals prior to Capella, that is a serious error.

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(())
}
}
13 changes: 13 additions & 0 deletions consensus/state_processing/src/per_block_processing/builder.rs
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions consensus/state_processing/src/per_block_processing/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
Loading