diff --git a/ethereum-consensus/src/capella/block_processing.rs b/ethereum-consensus/src/capella/block_processing.rs index 0f6fb304a..31d7a0b87 100644 --- a/ethereum-consensus/src/capella/block_processing.rs +++ b/ethereum-consensus/src/capella/block_processing.rs @@ -1,15 +1,17 @@ use crate::{ capella::{ - compute_timestamp_at_slot, get_current_epoch, get_randao_mix, process_attestation, + compute_timestamp_at_slot, decrease_balance, get_current_epoch, get_randao_mix, + is_fully_withdrawable_validator, is_partially_withdrawable_validator, process_attestation, process_attester_slashing, process_block_header, process_deposit, process_eth1_data, process_proposer_slashing, process_randao, process_sync_aggregate, process_voluntary_exit, - BeaconBlock, BeaconBlockBody, BeaconState, ExecutionEngine, ExecutionPayload, - ExecutionPayloadHeader, NewPayloadRequest, SignedBlsToExecutionChange, + BeaconBlock, BeaconBlockBody, BeaconState, ExecutionAddress, ExecutionEngine, + ExecutionPayload, ExecutionPayloadHeader, NewPayloadRequest, SignedBlsToExecutionChange, + Withdrawal, }, ssz::prelude::*, state_transition::{ invalid_operation_error, Context, InvalidDeposit, InvalidExecutionPayload, - InvalidOperation, Result, + InvalidOperation, InvalidWithdrawals, Result, }, }; @@ -240,7 +242,7 @@ pub fn process_withdrawals< const MAX_TRANSACTIONS_PER_PAYLOAD: usize, const MAX_WITHDRAWALS_PER_PAYLOAD: usize, >( - _state: &mut BeaconState< + state: &mut BeaconState< SLOTS_PER_HISTORICAL_ROOT, HISTORICAL_ROOTS_LIMIT, ETH1_DATA_VOTES_BOUND, @@ -252,16 +254,113 @@ pub fn process_withdrawals< BYTES_PER_LOGS_BLOOM, MAX_EXTRA_DATA_BYTES, >, - _execution_payload: &ExecutionPayload< + execution_payload: &ExecutionPayload< BYTES_PER_LOGS_BLOOM, MAX_EXTRA_DATA_BYTES, MAX_BYTES_PER_TRANSACTION, MAX_TRANSACTIONS_PER_PAYLOAD, MAX_WITHDRAWALS_PER_PAYLOAD, >, - _context: &Context, + context: &Context, ) -> Result<()> { - unimplemented!() + let expected_withdrawals = get_expected_withdrawals(state, context); + + if execution_payload.withdrawals.as_ref() != expected_withdrawals { + return Err(invalid_operation_error(InvalidOperation::Withdrawal( + InvalidWithdrawals::IncorrectWithdrawals { + provided: execution_payload.withdrawals.to_vec(), + expected: expected_withdrawals, + }, + ))) + } + + for withdrawal in &expected_withdrawals { + decrease_balance(state, withdrawal.validator_index, withdrawal.amount); + } + + // Update the next withdrawal index if this block contained withdrawals + if let Some(latest_withdrawal) = expected_withdrawals.last() { + state.next_withdrawal_index = latest_withdrawal.index + 1; + } + + // Update the next validator index to start the next withdrawal sweep + if expected_withdrawals.len() == context.max_withdrawals_per_payload { + // Next sweep starts after the latest withdrawal's validator index + let latest_withdrawal = expected_withdrawals.last().expect("empty withdrawals"); + let next_validator_index = (latest_withdrawal.validator_index + 1) % state.validators.len(); + state.next_withdrawal_validator_index = next_validator_index; + } else { + // Advance sweep by the max length of the sweep if there was not a full set of withdrawals + let next_index = + state.next_withdrawal_validator_index + context.max_validators_per_withdrawals_sweep; + state.next_withdrawal_validator_index = next_index % state.validators.len(); + } + Ok(()) +} + +pub fn get_expected_withdrawals< + const SLOTS_PER_HISTORICAL_ROOT: usize, + const HISTORICAL_ROOTS_LIMIT: usize, + const ETH1_DATA_VOTES_BOUND: usize, + const VALIDATOR_REGISTRY_LIMIT: usize, + const EPOCHS_PER_HISTORICAL_VECTOR: usize, + const EPOCHS_PER_SLASHINGS_VECTOR: usize, + const MAX_VALIDATORS_PER_COMMITTEE: usize, + const SYNC_COMMITTEE_SIZE: usize, + const BYTES_PER_LOGS_BLOOM: usize, + const MAX_EXTRA_DATA_BYTES: usize, +>( + state: &BeaconState< + SLOTS_PER_HISTORICAL_ROOT, + HISTORICAL_ROOTS_LIMIT, + ETH1_DATA_VOTES_BOUND, + VALIDATOR_REGISTRY_LIMIT, + EPOCHS_PER_HISTORICAL_VECTOR, + EPOCHS_PER_SLASHINGS_VECTOR, + MAX_VALIDATORS_PER_COMMITTEE, + SYNC_COMMITTEE_SIZE, + BYTES_PER_LOGS_BLOOM, + MAX_EXTRA_DATA_BYTES, + >, + context: &Context, +) -> Vec { + let epoch = get_current_epoch(state, context); + let mut withdrawal_index = state.next_withdrawal_index; + let mut validator_index = state.next_withdrawal_validator_index; + let mut withdrawals = vec![]; + let bound = state.validators.len().min(context.max_validators_per_withdrawals_sweep); + for _ in 0..bound { + let validator = &state.validators[validator_index]; + let balance = state.balances[validator_index]; + if is_fully_withdrawable_validator(validator, balance, epoch) { + let address = + ExecutionAddress::try_from(&validator.withdrawal_credentials.as_slice()[12..]) + .expect("providing the correct amount of input to type"); + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index, + address, + amount: balance, + }); + withdrawal_index += 1; + } else if is_partially_withdrawable_validator(validator, balance, context) { + let address = + ExecutionAddress::try_from(&validator.withdrawal_credentials.as_slice()[12..]) + .expect("providing the correct amount of input to type"); + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index, + address, + amount: balance - context.max_effective_balance, + }); + withdrawal_index += 1; + } + if withdrawals.len() == context.max_withdrawals_per_payload { + break + } + validator_index = (validator_index + 1) % state.validators.len(); + } + withdrawals } pub fn process_block< diff --git a/ethereum-consensus/src/state_transition/context.rs b/ethereum-consensus/src/state_transition/context.rs index 53756eaa9..f0ce24620 100644 --- a/ethereum-consensus/src/state_transition/context.rs +++ b/ethereum-consensus/src/state_transition/context.rs @@ -1,5 +1,5 @@ use crate::{ - altair, bellatrix, + altair, bellatrix, capella, clock::{self, Clock, SystemTimeProvider}, configs::{self, Config}, deneb, @@ -73,6 +73,11 @@ pub struct Context { pub bytes_per_logs_bloom: usize, pub max_extra_data_bytes: usize, + // capella preset + pub max_bls_to_execution_changes: usize, + pub max_withdrawals_per_payload: usize, + pub max_validators_per_withdrawals_sweep: usize, + // deneb preset pub field_elements_per_blob: usize, pub max_blob_commitments_per_block: usize, @@ -129,15 +134,31 @@ impl Context { let phase0_preset = &phase0::mainnet::PRESET; let altair_preset = &altair::mainnet::PRESET; let bellatrix_preset = &bellatrix::mainnet::PRESET; + let capella_preset = &capella::mainnet::PRESET; let deneb_preset = &deneb::mainnet::PRESET; - Self::from(phase0_preset, altair_preset, bellatrix_preset, deneb_preset, &config) + Self::from( + phase0_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + &config, + ) } "minimal" => { let phase0_preset = &phase0::minimal::PRESET; let altair_preset = &altair::minimal::PRESET; let bellatrix_preset = &bellatrix::minimal::PRESET; + let capella_preset = &capella::minimal::PRESET; let deneb_preset = &deneb::minimal::PRESET; - Self::from(phase0_preset, altair_preset, bellatrix_preset, deneb_preset, &config) + Self::from( + phase0_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + &config, + ) } other => return Err(Error::UnknownPreset(other.to_string())), }; @@ -148,6 +169,7 @@ impl Context { phase0_preset: &phase0::Preset, altair_preset: &altair::Preset, bellatrix_preset: &bellatrix::Preset, + capella_preset: &capella::Preset, deneb_preset: &deneb::Preset, config: &Config, ) -> Self { @@ -206,6 +228,11 @@ impl Context { max_transactions_per_payload: bellatrix_preset.max_transactions_per_payload, bytes_per_logs_bloom: bellatrix_preset.bytes_per_logs_bloom, max_extra_data_bytes: bellatrix_preset.max_extra_data_bytes, + // capella + max_bls_to_execution_changes: capella_preset.max_bls_to_execution_changes, + max_withdrawals_per_payload: capella_preset.max_withdrawals_per_payload, + max_validators_per_withdrawals_sweep: capella_preset + .max_validators_per_withdrawals_sweep, // deneb field_elements_per_blob: deneb_preset.field_elements_per_blob, max_blob_commitments_per_block: deneb_preset.max_blob_commitments_per_block, @@ -250,8 +277,16 @@ impl Context { let phase0_preset = &phase0::mainnet::PRESET; let altair_preset = &altair::mainnet::PRESET; let bellatrix_preset = &bellatrix::mainnet::PRESET; + let capella_preset = &capella::mainnet::PRESET; let deneb_preset = &deneb::mainnet::PRESET; - Self::from(phase0_preset, altair_preset, bellatrix_preset, deneb_preset, config) + Self::from( + phase0_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + config, + ) } pub fn for_minimal() -> Self { @@ -259,8 +294,16 @@ impl Context { let phase0_preset = &phase0::minimal::PRESET; let altair_preset = &altair::minimal::PRESET; let bellatrix_preset = &bellatrix::minimal::PRESET; + let capella_preset = &capella::minimal::PRESET; let deneb_preset = &deneb::minimal::PRESET; - Self::from(phase0_preset, altair_preset, bellatrix_preset, deneb_preset, config) + Self::from( + phase0_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + config, + ) } pub fn for_goerli() -> Self { @@ -268,8 +311,16 @@ impl Context { let phase0_preset = &phase0::mainnet::PRESET; let altair_preset = &altair::mainnet::PRESET; let bellatrix_preset = &bellatrix::mainnet::PRESET; + let capella_preset = &capella::mainnet::PRESET; let deneb_preset = &deneb::mainnet::PRESET; - Self::from(phase0_preset, altair_preset, bellatrix_preset, deneb_preset, config) + Self::from( + phase0_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + config, + ) } pub fn for_sepolia() -> Self { @@ -277,8 +328,16 @@ impl Context { let phase0_preset = &phase0::mainnet::PRESET; let altair_preset = &altair::mainnet::PRESET; let bellatrix_preset = &bellatrix::mainnet::PRESET; + let capella_preset = &capella::mainnet::PRESET; let deneb_preset = &deneb::mainnet::PRESET; - Self::from(phase0_preset, altair_preset, bellatrix_preset, deneb_preset, config) + Self::from( + phase0_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + config, + ) } pub fn for_holesky() -> Self { @@ -286,8 +345,16 @@ impl Context { let phase0_preset = &phase0::mainnet::PRESET; let altair_preset = &altair::mainnet::PRESET; let bellatrix_preset = &bellatrix::mainnet::PRESET; + let capella_preset = &capella::mainnet::PRESET; let deneb_preset = &deneb::mainnet::PRESET; - Self::from(phase0_preset, altair_preset, bellatrix_preset, deneb_preset, config) + Self::from( + phase0_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + config, + ) } pub fn fork_for(&self, slot: Slot) -> Forks { diff --git a/ethereum-consensus/src/state_transition/error.rs b/ethereum-consensus/src/state_transition/error.rs index a5166aefa..bacdb6759 100644 --- a/ethereum-consensus/src/state_transition/error.rs +++ b/ethereum-consensus/src/state_transition/error.rs @@ -1,4 +1,5 @@ use crate::{ + capella::Withdrawal, crypto::Error as CryptoError, phase0::{AttestationData, BeaconBlockHeader, Checkpoint}, primitives::{BlsSignature, Bytes32, Epoch, Hash32, Root, Slot, ValidatorIndex}, @@ -84,6 +85,8 @@ pub enum InvalidOperation { SyncAggregate(#[from] InvalidSyncAggregate), #[error("invalid execution payload: {0}")] ExecutionPayload(#[from] InvalidExecutionPayload), + #[error("invalid withdrawals: {0}")] + Withdrawal(#[from] InvalidWithdrawals), } #[derive(Debug, Error)] @@ -180,6 +183,12 @@ pub enum InvalidVoluntaryExit { InvalidSignature(BlsSignature), } +#[derive(Debug, Error)] +pub enum InvalidWithdrawals { + #[error("expected withdrawals {expected:#?} do not match provided withdrawals {provided:#?}")] + IncorrectWithdrawals { provided: Vec, expected: Vec }, +} + #[derive(Debug, Error)] pub enum InvalidSyncAggregate { #[error("invalid sync committee aggregate signature {signature} signing over previous slot block root {root}")]