From 716976f0d1a72a61206d05421d6a5bd8ffea46c1 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Thu, 30 May 2024 16:30:59 +0100 Subject: [PATCH] feat: implement EIP-7002 (#8485) --- Cargo.lock | 3 + crates/ethereum/evm/Cargo.toml | 3 +- crates/ethereum/evm/src/execute.rs | 136 +++++++++++++++++++++++-- crates/evm/execution-errors/src/lib.rs | 6 ++ crates/payload/basic/src/lib.rs | 35 ++++++- crates/payload/ethereum/src/lib.rs | 64 +++++++----- crates/primitives/src/revm/env.rs | 55 ++++++++-- crates/revm/Cargo.toml | 1 + crates/revm/src/batch.rs | 5 + crates/revm/src/state_change.rs | 103 ++++++++++++++++++- 10 files changed, 360 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 395b5d87c..3693e33fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7036,7 +7036,9 @@ dependencies = [ "reth-evm", "reth-primitives", "reth-revm", + "reth-testing-utils", "revm-primitives", + "secp256k1 0.28.2", ] [[package]] @@ -7714,6 +7716,7 @@ name = "reth-revm" version = "0.2.0-beta.7" dependencies = [ "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7320d4c)", + "alloy-rlp", "reth-consensus-common", "reth-execution-errors", "reth-primitives", diff --git a/crates/ethereum/evm/Cargo.toml b/crates/ethereum/evm/Cargo.toml index 88e5967e5..1b562254b 100644 --- a/crates/ethereum/evm/Cargo.toml +++ b/crates/ethereum/evm/Cargo.toml @@ -21,6 +21,7 @@ reth-ethereum-consensus.workspace = true revm-primitives.workspace = true [dev-dependencies] +reth-testing-utils.workspace = true reth-revm = { workspace = true, features = ["test-utils"] } alloy-eips.workspace = true - +secp256k1.workspace = true diff --git a/crates/ethereum/evm/src/execute.rs b/crates/ethereum/evm/src/execute.rs index 471affddd..1d876d39d 100644 --- a/crates/ethereum/evm/src/execute.rs +++ b/crates/ethereum/evm/src/execute.rs @@ -20,7 +20,8 @@ use reth_revm::{ batch::{BlockBatchRecord, BlockExecutorStats}, db::states::bundle_state::BundleRetention, state_change::{ - apply_beacon_root_contract_call, apply_blockhashes_update, post_block_balance_increments, + apply_beacon_root_contract_call, apply_blockhashes_update, + apply_withdrawal_requests_contract_call, post_block_balance_increments, }, Evm, State, }; @@ -121,13 +122,16 @@ impl EthEvmExecutor where EvmConfig: ConfigureEvm, { - /// Executes the transactions in the block and returns the receipts. + /// Executes the transactions in the block and returns the receipts of the transactions in the + /// block, the total gas used and the list of EIP-7685 [requests](Request). /// - /// This applies the pre-execution changes, and executes the transactions. + /// This applies the pre-execution and post-execution changes that require an [EVM](Evm), and + /// executes the transactions. /// /// # Note /// - /// It does __not__ apply post-execution changes. + /// It does __not__ apply post-execution changes that do not require an [EVM](Evm), for that see + /// [EthBlockExecutor::post_execution]. fn execute_state_transitions( &self, block: &BlockWithSenders, @@ -197,9 +201,15 @@ where }, ); } - drop(evm); - Ok(EthExecuteOutput { receipts, requests: vec![], gas_used: cumulative_gas_used }) + let requests = if self.chain_spec.is_prague_active_at_timestamp(block.timestamp) { + // Collect all EIP-7685 requests + apply_withdrawal_requests_contract_call(&mut evm)? + } else { + vec![] + }; + + Ok(EthExecuteOutput { receipts, requests, gas_used: cumulative_gas_used }) } } @@ -260,7 +270,8 @@ where /// Execute a single block and apply the state changes to the internal state. /// - /// Returns the receipts of the transactions in the block and the total gas used. + /// Returns the receipts of the transactions in the block, the total gas used and the list of + /// EIP-7685 [requests](Request). /// /// Returns an error if execution fails. fn execute_without_verification( @@ -273,7 +284,6 @@ where // 2. configure the evm and execute let env = self.evm_env_for_block(&block.header, total_difficulty); - let output = { let evm = self.executor.evm_config.evm_with_env(&mut self.state, env); self.executor.execute_state_transitions(block, evm) @@ -292,8 +302,8 @@ where self.state.set_state_clear_flag(state_clear_flag); } - /// Apply post execution state changes, including block rewards, withdrawals, and irregular DAO - /// hardfork state change. + /// Apply post execution state changes that do not require an [EVM](Evm), such as: block + /// rewards, withdrawals, and irregular DAO hardfork state change pub fn post_execution( &mut self, block: &BlockWithSenders, @@ -441,14 +451,19 @@ mod tests { use alloy_eips::{ eip2935::HISTORY_STORAGE_ADDRESS, eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE, SYSTEM_ADDRESS}, + eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE}, }; use reth_primitives::{ - keccak256, trie::EMPTY_ROOT_HASH, Account, Block, ChainSpecBuilder, ForkCondition, B256, + constants::ETH_TO_WEI, keccak256, public_key_to_address, trie::EMPTY_ROOT_HASH, Account, + Block, ChainSpecBuilder, ForkCondition, Transaction, TxKind, TxLegacy, B256, }; use reth_revm::{ database::StateProviderDatabase, state_change::HISTORY_SERVE_WINDOW, test_utils::StateProviderTest, TransitionState, }; + use reth_testing_utils::generators::{self, sign_tx_with_key_pair}; + use revm_primitives::{b256, fixed_bytes, Bytes}; + use secp256k1::{Keypair, Secp256k1}; use std::collections::HashMap; fn create_state_provider_with_beacon_root_contract() -> StateProviderTest { @@ -470,6 +485,25 @@ mod tests { db } + fn create_state_provider_with_withdrawal_requests_contract() -> StateProviderTest { + let mut db = StateProviderTest::default(); + + let withdrawal_requests_contract_account = Account { + nonce: 1, + balance: U256::ZERO, + bytecode_hash: Some(keccak256(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone())), + }; + + db.insert_account( + WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + withdrawal_requests_contract_account, + Some(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone()), + HashMap::new(), + ); + + db + } + fn executor_provider(chain_spec: Arc) -> EthExecutorProvider { EthExecutorProvider { chain_spec, evm_config: Default::default() } } @@ -1192,4 +1226,84 @@ mod tests { .unwrap() .is_zero()); } + + #[test] + fn eip_7002() { + let chain_spec = Arc::new( + ChainSpecBuilder::from(&*MAINNET) + .shanghai_activated() + .with_fork(Hardfork::Prague, ForkCondition::Timestamp(0)) + .build(), + ); + + let mut db = create_state_provider_with_withdrawal_requests_contract(); + + let secp = Secp256k1::new(); + let sender_key_pair = Keypair::new(&secp, &mut generators::rng()); + let sender_address = public_key_to_address(sender_key_pair.public_key()); + + db.insert_account( + sender_address, + Account { nonce: 1, balance: U256::from(ETH_TO_WEI), bytecode_hash: None }, + None, + HashMap::new(), + ); + + // https://github.com/lightclient/7002asm/blob/e0d68e04d15f25057af7b6d180423d94b6b3bdb3/test/Contract.t.sol.in#L49-L64 + let validator_public_key = fixed_bytes!("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"); + let withdrawal_amount = fixed_bytes!("2222222222222222"); + let input: Bytes = [&validator_public_key[..], &withdrawal_amount[..]].concat().into(); + assert_eq!(input.len(), 56); + + let mut header = chain_spec.genesis_header(); + header.gas_limit = 1_500_000; + header.gas_used = 134_807; + header.receipts_root = + b256!("b31a3e47b902e9211c4d349af4e4c5604ce388471e79ca008907ae4616bb0ed3"); + + let tx = sign_tx_with_key_pair( + sender_key_pair, + Transaction::Legacy(TxLegacy { + chain_id: Some(chain_spec.chain.id()), + nonce: 1, + gas_price: header.base_fee_per_gas.unwrap().into(), + gas_limit: 134_807, + to: TxKind::Call(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS), + // `MIN_WITHDRAWAL_REQUEST_FEE` + value: U256::from(1), + input, + }), + ); + + let provider = executor_provider(chain_spec); + + let executor = provider.executor(StateProviderDatabase::new(&db)); + + let BlockExecutionOutput { receipts, requests, .. } = executor + .execute( + ( + &Block { + header, + body: vec![tx], + ommers: vec![], + withdrawals: None, + requests: None, + } + .with_recovered_senders() + .unwrap(), + U256::ZERO, + ) + .into(), + ) + .unwrap(); + + let receipt = receipts.first().unwrap(); + assert!(receipt.success); + + let request = requests.first().unwrap(); + let withdrawal_request = request.as_withdrawal_request().unwrap(); + assert_eq!(withdrawal_request.source_address, sender_address); + assert_eq!(withdrawal_request.validator_public_key, validator_public_key); + assert_eq!(withdrawal_request.amount, u64::from_be_bytes(withdrawal_amount.into())); + } } diff --git a/crates/evm/execution-errors/src/lib.rs b/crates/evm/execution-errors/src/lib.rs index a92f6b0d4..c2b5d2f0d 100644 --- a/crates/evm/execution-errors/src/lib.rs +++ b/crates/evm/execution-errors/src/lib.rs @@ -80,6 +80,12 @@ pub enum BlockValidationError { /// Provider error during the [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) block hash account loading. #[error(transparent)] BlockHashAccountLoadingFailed(#[from] ProviderError), + /// EVM error during withdrawal requests contract call + #[error("failed to apply withdrawal requests contract call: {message}")] + WithdrawalRequestsContractCall { + /// The error message. + message: String, + }, } /// BlockExecutor Errors diff --git a/crates/payload/basic/src/lib.rs b/crates/payload/basic/src/lib.rs index 759f59929..a96d7dedc 100644 --- a/crates/payload/basic/src/lib.rs +++ b/crates/payload/basic/src/lib.rs @@ -18,13 +18,14 @@ use reth_payload_builder::{ }; use reth_primitives::{ constants::{EMPTY_WITHDRAWALS, RETH_CLIENT_VERSION, SLOT_DURATION}, - proofs, BlockNumberOrTag, Bytes, ChainSpec, SealedBlock, Withdrawals, B256, U256, + proofs, BlockNumberOrTag, Bytes, ChainSpec, Request, SealedBlock, Withdrawals, B256, U256, }; use reth_provider::{ BlockReaderIdExt, BlockSource, CanonStateNotification, ProviderError, StateProviderFactory, }; use reth_revm::state_change::{ - apply_beacon_root_contract_call, post_block_withdrawals_balance_increments, + apply_beacon_root_contract_call, apply_withdrawal_requests_contract_call, + post_block_withdrawals_balance_increments, }; use reth_tasks::TaskSpawner; use reth_transaction_pool::TransactionPool; @@ -888,6 +889,36 @@ where .map_err(|err| PayloadBuilderError::Internal(err.into())) } +/// Apply the [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) post block contract call. +/// +/// This constructs a new [Evm] with the given DB, and environment +/// ([CfgEnvWithHandlerCfg] and [BlockEnv]) to execute the post block contract call. +/// +/// This uses [apply_withdrawal_requests_contract_call] to ultimately calculate the +/// [requests](Request). +pub fn post_block_withdrawal_requests_contract_call( + db: &mut DB, + initialized_cfg: &CfgEnvWithHandlerCfg, + initialized_block_env: &BlockEnv, +) -> Result, PayloadBuilderError> +where + DB::Error: std::fmt::Display, +{ + // apply post-block EIP-7002 contract call + let mut evm_post_block = Evm::builder() + .with_db(db) + .with_env_with_handler_cfg(EnvWithHandlerCfg::new_with_cfg_env( + initialized_cfg.clone(), + initialized_block_env.clone(), + Default::default(), + )) + .build(); + + // initialize a block from the env, because the post block call needs the block itself + apply_withdrawal_requests_contract_call(&mut evm_post_block) + .map_err(|err| PayloadBuilderError::Internal(err.into())) +} + /// Checks if the new payload is better than the current best. /// /// This compares the total fees of the blocks, higher is better. diff --git a/crates/payload/ethereum/src/lib.rs b/crates/payload/ethereum/src/lib.rs index c5edb8119..86ad733f9 100644 --- a/crates/payload/ethereum/src/lib.rs +++ b/crates/payload/ethereum/src/lib.rs @@ -10,8 +10,9 @@ #![allow(clippy::useless_let_if_seq)] use reth_basic_payload_builder::{ - commit_withdrawals, is_better_payload, pre_block_beacon_root_contract_call, BuildArguments, - BuildOutcome, PayloadBuilder, PayloadConfig, WithdrawalsOutcome, + commit_withdrawals, is_better_payload, post_block_withdrawal_requests_contract_call, + pre_block_beacon_root_contract_call, BuildArguments, BuildOutcome, PayloadBuilder, + PayloadConfig, WithdrawalsOutcome, }; use reth_evm::ConfigureEvm; use reth_evm_ethereum::EthEvmConfig; @@ -20,14 +21,12 @@ use reth_payload_builder::{ }; use reth_primitives::{ constants::{ - eip4844::MAX_DATA_GAS_PER_BLOCK, BEACON_NONCE, EMPTY_RECEIPTS, EMPTY_ROOT_HASH, - EMPTY_TRANSACTIONS, + eip4844::MAX_DATA_GAS_PER_BLOCK, BEACON_NONCE, EMPTY_RECEIPTS, EMPTY_TRANSACTIONS, }, eip4844::calculate_excess_blob_gas, - proofs, + proofs::{self, calculate_requests_root}, revm::env::tx_env_with_recovered, - Block, Header, IntoRecoveredTransaction, Receipt, Receipts, Requests, EMPTY_OMMER_ROOT_HASH, - U256, + Block, Header, IntoRecoveredTransaction, Receipt, Receipts, EMPTY_OMMER_ROOT_HASH, U256, }; use reth_provider::{BundleStateWithReceipts, StateProviderFactory}; use reth_revm::{database::StateProviderDatabase, state_change::apply_blockhashes_update}; @@ -154,14 +153,6 @@ where err })?; - // Calculate the requests and the requests root. - let (requests, requests_root) = - if chain_spec.is_prague_active_at_timestamp(attributes.timestamp) { - (Some(Requests::default()), Some(EMPTY_ROOT_HASH)) - } else { - (None, None) - }; - // merge all transitions into bundle state, this would apply the withdrawal balance // changes and 4788 contract call db.merge_transitions(BundleRetention::PlainState); @@ -194,6 +185,24 @@ where blob_gas_used = Some(0); } + // Calculate the requests and the requests root. + let (requests, requests_root) = + if chain_spec.is_prague_active_at_timestamp(attributes.timestamp) { + // We do not calculate the EIP-6110 deposit requests because there are no + // transactions in an empty payload. + let withdrawal_requests = post_block_withdrawal_requests_contract_call( + &mut db, + &initialized_cfg, + &initialized_block_env, + )?; + + let requests = withdrawal_requests; + let requests_root = calculate_requests_root(&requests); + (Some(requests.into()), Some(requests_root)) + } else { + (None, None) + }; + let header = Header { parent_hash: parent_block.hash(), ommers_hash: EMPTY_OMMER_ROOT_HASH, @@ -407,6 +416,23 @@ where return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads }) } + // calculate the requests and the requests root + let (requests, requests_root) = + if chain_spec.is_prague_active_at_timestamp(attributes.timestamp) { + let withdrawal_requests = post_block_withdrawal_requests_contract_call( + &mut db, + &initialized_cfg, + &initialized_block_env, + )?; + + // TODO: add deposit requests + let requests = withdrawal_requests; + let requests_root = calculate_requests_root(&requests); + (Some(requests.into()), Some(requests_root)) + } else { + (None, None) + }; + let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals(&mut db, &chain_spec, attributes.timestamp, attributes.withdrawals)?; @@ -456,14 +482,6 @@ where blob_gas_used = Some(sum_blob_gas_used); } - // todo: compute requests and requests root - let (requests, requests_root) = - if chain_spec.is_prague_active_at_timestamp(attributes.timestamp) { - (Some(Requests::default()), Some(EMPTY_ROOT_HASH)) - } else { - (None, None) - }; - let header = Header { parent_hash: parent_block.hash(), ommers_hash: EMPTY_OMMER_ROOT_HASH, diff --git a/crates/primitives/src/revm/env.rs b/crates/primitives/src/revm/env.rs index 1691e09e7..2fad8fb3f 100644 --- a/crates/primitives/src/revm/env.rs +++ b/crates/primitives/src/revm/env.rs @@ -5,7 +5,7 @@ use crate::{ B256, U256, }; -use alloy_eips::eip4788::BEACON_ROOTS_ADDRESS; +use alloy_eips::{eip4788::BEACON_ROOTS_ADDRESS, eip7002::WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS}; #[cfg(feature = "optimism")] use revm_primitives::OptimismFields; @@ -132,24 +132,59 @@ pub fn tx_env_with_recovered(transaction: &TransactionSignedEcRecovered) -> TxEn /// [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788) are: /// /// At the start of processing any execution block where `block.timestamp >= FORK_TIMESTAMP` (i.e. -/// before processing any transactions), call `BEACON_ROOTS_ADDRESS` as `SYSTEM_ADDRESS` with the -/// 32-byte input of `header.parent_beacon_block_root`, a gas limit of `30_000_000`, and `0` value. -/// This will trigger the `set()` routine of the beacon roots contract. This is a system operation -/// and therefore: +/// before processing any transactions), call [BEACON_ROOTS_ADDRESS] as +/// [SYSTEM_ADDRESS](alloy_eips::eip4788::SYSTEM_ADDRESS) with the 32-byte input of +/// `header.parent_beacon_block_root`. This will trigger the `set()` routine of the beacon roots +/// contract. +pub fn fill_tx_env_with_beacon_root_contract_call(env: &mut Env, parent_beacon_block_root: B256) { + fill_tx_env_with_system_contract_call( + env, + alloy_eips::eip4788::SYSTEM_ADDRESS, + BEACON_ROOTS_ADDRESS, + parent_beacon_block_root.0.into(), + ); +} + +/// Fill transaction environment with the EIP-7002 withdrawal requests contract message data. +// +/// This requirement for the withdrawal requests contract call defined by +/// [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) is: +// +/// At the end of processing any execution block where `block.timestamp >= FORK_TIMESTAMP` (i.e. +/// after processing all transactions and after performing the block body withdrawal requests +/// validations), call the contract as `SYSTEM_ADDRESS`. +pub fn fill_tx_env_with_withdrawal_requests_contract_call(env: &mut Env) { + fill_tx_env_with_system_contract_call( + env, + alloy_eips::eip7002::SYSTEM_ADDRESS, + WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + Bytes::new(), + ); +} + +/// Fill transaction environment with the system caller and the system contract address and message +/// data. +/// +/// This is a system operation and therefore: /// * the call must execute to completion /// * the call does not count against the block’s gas limit /// * the call does not follow the EIP-1559 burn semantics - no value should be transferred as part /// of the call -/// * if no code exists at `BEACON_ROOTS_ADDRESS`, the call must fail silently -pub fn fill_tx_env_with_beacon_root_contract_call(env: &mut Env, parent_beacon_block_root: B256) { +/// * if no code exists at the provided address, the call will fail silently +fn fill_tx_env_with_system_contract_call( + env: &mut Env, + caller: Address, + contract: Address, + data: Bytes, +) { env.tx = TxEnv { - caller: alloy_eips::eip4788::SYSTEM_ADDRESS, - transact_to: TransactTo::Call(BEACON_ROOTS_ADDRESS), + caller, + transact_to: TransactTo::Call(contract), // Explicitly set nonce to None so revm does not do any nonce checks nonce: None, gas_limit: 30_000_000, value: U256::ZERO, - data: parent_beacon_block_root.0.into(), + data, // Setting the gas price to zero enforces that no value is transferred as part of the call, // and that the call will not count against the block's gas limit gas_price: U256::ZERO, diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index 715e306f3..cba27331f 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -25,6 +25,7 @@ revm.workspace = true # alloy alloy-eips.workspace = true +alloy-rlp.workspace = true # common tracing.workspace = true diff --git a/crates/revm/src/batch.rs b/crates/revm/src/batch.rs index 0ca61c990..bf0661ca3 100644 --- a/crates/revm/src/batch.rs +++ b/crates/revm/src/batch.rs @@ -83,6 +83,11 @@ impl BlockBatchRecord { std::mem::take(&mut self.receipts) } + /// Returns the recorded requests. + pub fn requests(&self) -> &[Requests] { + &self.requests + } + /// Returns all recorded requests. pub fn take_requests(&mut self) -> Vec { std::mem::take(&mut self.requests) diff --git a/crates/revm/src/state_change.rs b/crates/revm/src/state_change.rs index c8b2ac3e1..e61a12b24 100644 --- a/crates/revm/src/state_change.rs +++ b/crates/revm/src/state_change.rs @@ -1,14 +1,23 @@ -use alloy_eips::eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE}; +use alloy_eips::{ + eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE}, + eip7002::WithdrawalRequest, +}; +use alloy_rlp::Buf; use reth_consensus_common::calc; use reth_execution_errors::{BlockExecutionError, BlockValidationError}; use reth_primitives::{ - revm::env::fill_tx_env_with_beacon_root_contract_call, Address, ChainSpec, Header, Withdrawal, - B256, U256, + revm::env::{ + fill_tx_env_with_beacon_root_contract_call, + fill_tx_env_with_withdrawal_requests_contract_call, + }, + Address, ChainSpec, Header, Request, Withdrawal, B256, U256, }; use reth_storage_errors::provider::ProviderError; use revm::{ interpreter::Host, - primitives::{Account, AccountInfo, Bytecode, EvmStorageSlot}, + primitives::{ + Account, AccountInfo, Bytecode, EvmStorageSlot, ExecutionResult, FixedBytes, ResultAndState, + }, Database, DatabaseCommit, Evm, }; use std::collections::HashMap; @@ -238,3 +247,89 @@ pub fn insert_post_block_withdrawals_balance_increments( } } } + +/// Applies the post-block call to the EIP-7002 withdrawal requests contract. +/// +/// If Prague is not active at the given timestamp, then this is a no-op, and an empty vector is +/// returned. Otherwise, the withdrawal requests are returned. +#[inline] +pub fn apply_withdrawal_requests_contract_call( + evm: &mut Evm<'_, EXT, DB>, +) -> Result, BlockExecutionError> +where + DB::Error: std::fmt::Display, +{ + // get previous env + let previous_env = Box::new(evm.context.env().clone()); + + // modify env for pre block call + fill_tx_env_with_withdrawal_requests_contract_call(&mut evm.context.evm.env); + + let ResultAndState { result, mut state } = match evm.transact() { + Ok(res) => res, + Err(e) => { + evm.context.evm.env = previous_env; + return Err(BlockValidationError::WithdrawalRequestsContractCall { + message: format!("execution failed: {e}"), + } + .into()) + } + }; + + // cleanup the state + state.remove(&alloy_eips::eip7002::SYSTEM_ADDRESS); + state.remove(&evm.block().coinbase); + evm.context.evm.db.commit(state); + + // re-set the previous env + evm.context.evm.env = previous_env; + + let mut data = match result { + ExecutionResult::Success { output, .. } => Ok(output.into_data()), + ExecutionResult::Revert { output, .. } => { + Err(BlockValidationError::WithdrawalRequestsContractCall { + message: format!("execution reverted: {output}"), + }) + } + ExecutionResult::Halt { reason, .. } => { + Err(BlockValidationError::WithdrawalRequestsContractCall { + message: format!("execution halted: {reason:?}"), + }) + } + }?; + + // Withdrawals are encoded as a series of withdrawal requests, each with the following + // format: + // + // +------+--------+--------+ + // | addr | pubkey | amount | + // +------+--------+--------+ + // 20 48 8 + + const WITHDRAWAL_REQUEST_SIZE: usize = 20 + 48 + 8; + let mut withdrawal_requests = Vec::with_capacity(data.len() / WITHDRAWAL_REQUEST_SIZE); + while data.has_remaining() { + if data.remaining() < WITHDRAWAL_REQUEST_SIZE { + return Err(BlockValidationError::WithdrawalRequestsContractCall { + message: "invalid withdrawal request length".to_string(), + } + .into()) + } + + let mut source_address = Address::ZERO; + data.copy_to_slice(source_address.as_mut_slice()); + + let mut validator_public_key = FixedBytes::<48>::ZERO; + data.copy_to_slice(validator_public_key.as_mut_slice()); + + let amount = data.get_u64(); + + withdrawal_requests.push(Request::WithdrawalRequest(WithdrawalRequest { + source_address, + validator_public_key, + amount, + })); + } + + Ok(withdrawal_requests) +}