From faf609bcea260c4c85b4e4f1e0cff1f118d39125 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Mon, 13 May 2024 12:46:44 +0300 Subject: [PATCH] feat(evm): EIP-7002 withdrawal requests (#8212) --- Cargo.lock | 5 ++ bin/reth/src/commands/debug_cmd/merkle.rs | 3 +- crates/config/src/config.rs | 2 +- crates/e2e-test-utils/src/payload.rs | 4 +- crates/ethereum/evm/Cargo.toml | 3 + crates/ethereum/evm/src/execute.rs | 65 ++++++++++------ .../ethereum/evm/src/instructions/eip3074.rs | 10 +-- crates/evm/Cargo.toml | 5 +- crates/evm/src/execute.rs | 23 +++++- crates/evm/src/test_utils.rs | 1 + crates/interfaces/src/executor.rs | 8 +- crates/net/discv4/src/lib.rs | 2 +- crates/net/downloaders/src/bodies/bodies.rs | 22 +++--- crates/net/eth-wire/src/multiplex.rs | 4 +- crates/node-core/src/args/pruning.rs | 2 +- crates/node-core/src/init.rs | 2 +- crates/node-core/src/node_config.rs | 4 +- crates/optimism/evm/src/execute.rs | 16 ++-- crates/payload/validator/src/lib.rs | 2 +- crates/primitives/Cargo.toml | 1 + crates/primitives/src/compression/mod.rs | 2 +- crates/primitives/src/lib.rs | 2 + crates/primitives/src/receipt.rs | 8 +- crates/primitives/src/request.rs | 25 +++++++ crates/primitives/src/revm/env.rs | 55 +++++++++++--- crates/primitives/src/transaction/mod.rs | 2 +- crates/revm/Cargo.toml | 2 + crates/revm/src/batch.rs | 20 ++++- crates/revm/src/state_change.rs | 75 ++++++++++++++++++- crates/rpc/ipc/src/server/ipc.rs | 2 +- crates/rpc/ipc/src/server/mod.rs | 2 +- crates/rpc/rpc-types/src/mev.rs | 2 +- crates/rpc/rpc/src/otterscan.rs | 2 +- crates/stages/src/stages/execution.rs | 3 +- crates/storage/nippy-jar/src/lib.rs | 2 +- .../bundle_state_with_receipts.rs | 10 ++- .../src/providers/static_file/writer.rs | 2 +- 37 files changed, 310 insertions(+), 90 deletions(-) create mode 100644 crates/primitives/src/request.rs diff --git a/Cargo.lock b/Cargo.lock index 0ea83fd51d9..487aaf8ed29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6978,6 +6978,7 @@ dependencies = [ name = "reth-evm" version = "0.2.0-beta.7" dependencies = [ + "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=dd7a999)", "futures-util", "parking_lot 0.12.2", "reth-interfaces", @@ -6990,6 +6991,7 @@ dependencies = [ name = "reth-evm-ethereum" version = "0.2.0-beta.7" dependencies = [ + "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=dd7a999)", "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=dd7a999)", "reth-evm", "reth-interfaces", @@ -7544,6 +7546,7 @@ name = "reth-primitives" version = "0.2.0-beta.7" dependencies = [ "alloy-chains", + "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=dd7a999)", "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=dd7a999)", "alloy-genesis 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=dd7a999)", "alloy-primitives", @@ -7647,7 +7650,9 @@ dependencies = [ name = "reth-revm" version = "0.2.0-beta.7" dependencies = [ + "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=dd7a999)", "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=dd7a999)", + "alloy-rlp", "reth-consensus-common", "reth-interfaces", "reth-primitives", diff --git a/bin/reth/src/commands/debug_cmd/merkle.rs b/bin/reth/src/commands/debug_cmd/merkle.rs index f452e2e5220..45a40b5ff88 100644 --- a/bin/reth/src/commands/debug_cmd/merkle.rs +++ b/bin/reth/src/commands/debug_cmd/merkle.rs @@ -197,7 +197,8 @@ impl Command { PruneModes::none(), ); executor.execute_one((&sealed_block.clone().unseal(), td).into())?; - let BatchBlockExecutionOutput { bundle, receipts, first_block } = executor.finalize(); + let BatchBlockExecutionOutput { bundle, receipts, requests: _, first_block } = + executor.finalize(); BundleStateWithReceipts::new(bundle, receipts, first_block).write_to_storage( provider_rw.tx_ref(), None, diff --git a/crates/config/src/config.rs b/crates/config/src/config.rs index aa8b7ee09ab..4215f89a438 100644 --- a/crates/config/src/config.rs +++ b/crates/config/src/config.rs @@ -57,7 +57,7 @@ impl Config { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!("reth config file extension must be '{EXTENSION}'"), - )); + )) } confy::store_path(path, self).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) } diff --git a/crates/e2e-test-utils/src/payload.rs b/crates/e2e-test-utils/src/payload.rs index 47f4134d7fe..828bc5f32c4 100644 --- a/crates/e2e-test-utils/src/payload.rs +++ b/crates/e2e-test-utils/src/payload.rs @@ -50,9 +50,9 @@ impl PayloadTestContext { let payload = self.payload_builder.best_payload(payload_id).await.unwrap().unwrap(); if payload.block().body.is_empty() { tokio::time::sleep(std::time::Duration::from_millis(20)).await; - continue; + continue } - break; + break } } diff --git a/crates/ethereum/evm/Cargo.toml b/crates/ethereum/evm/Cargo.toml index ceb2b1eee2a..fc754b5027a 100644 --- a/crates/ethereum/evm/Cargo.toml +++ b/crates/ethereum/evm/Cargo.toml @@ -23,6 +23,9 @@ revm-interpreter.workspace = true revm-precompile = { version = "7.0.0", features = ["std"], default-features = false } +# Alloy +alloy-consensus.workspace = true + # misc tracing.workspace = true diff --git a/crates/ethereum/evm/src/execute.rs b/crates/ethereum/evm/src/execute.rs index 0decdf493e4..f9f53313a52 100644 --- a/crates/ethereum/evm/src/execute.rs +++ b/crates/ethereum/evm/src/execute.rs @@ -5,6 +5,7 @@ use crate::{ verify::verify_receipts, EthEvmConfig, }; +use alloy_consensus::Request; use reth_evm::{ execute::{ BatchBlockExecutionOutput, BatchExecutor, BlockExecutionInput, BlockExecutionOutput, @@ -25,6 +26,7 @@ use reth_revm::{ db::states::bundle_state::BundleRetention, state_change::{ apply_beacon_root_contract_call, apply_blockhashes_update, post_block_balance_increments, + post_block_withdrawal_requests, }, Evm, State, }; @@ -105,6 +107,14 @@ where } } +/// Helper type for the output of executing a block. +#[derive(Debug, Clone)] +struct EthExecuteOutput { + receipts: Vec, + requests: Vec, + gas_used: u64, +} + /// Helper container type for EVM with chain spec. #[derive(Debug, Clone)] struct EthEvmExecutor { @@ -118,18 +128,21 @@ 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. - fn execute_pre_and_transactions( + /// 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, mut evm: Evm<'_, Ext, &mut State>, - ) -> Result<(Vec, u64), BlockExecutionError> + ) -> Result where DB: Database, { @@ -155,7 +168,7 @@ where transaction_gas_limit: transaction.gas_limit(), block_available_gas, } - .into()); + .into()) } EvmConfig::fill_tx_env(evm.tx_mut(), transaction, *sender); @@ -188,7 +201,6 @@ where }, ); } - drop(evm); // Check if gas used matches the value set in header. if block.gas_used != cumulative_gas_used { @@ -197,10 +209,15 @@ where gas: GotExpected { got: cumulative_gas_used, expected: block.gas_used }, gas_spent_by_tx: receipts.gas_spent_by_tx()?, } - .into()); + .into()) } - Ok((receipts, cumulative_gas_used)) + // Collect all EIP-7685 requests + let withdrawal_requests = + post_block_withdrawal_requests(&self.chain_spec, block.timestamp, &mut evm)?; + let requests = withdrawal_requests; + + Ok(EthExecuteOutput { receipts, requests, gas_used: cumulative_gas_used }) } } @@ -261,23 +278,23 @@ 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 or receipt verification fails. fn execute_and_verify( &mut self, block: &BlockWithSenders, total_difficulty: U256, - ) -> Result<(Vec, u64), BlockExecutionError> { + ) -> Result { // 1. prepare state on new block self.on_new_block(&block.header); // 2. configure the evm and execute let env = self.evm_env_for_block(&block.header, total_difficulty); - - let (receipts, gas_used) = { + let EthExecuteOutput { receipts, requests, gas_used } = { let evm = self.executor.evm_config.evm_with_env(&mut self.state, env); - self.executor.execute_pre_and_transactions(block, evm) + self.executor.execute_state_transitions(block, evm) }?; // 3. apply post execution changes @@ -294,11 +311,11 @@ where receipts.iter(), ) { debug!(target: "evm", %error, ?receipts, "receipts verification failed"); - return Err(error); + return Err(error) }; } - Ok((receipts, gas_used)) + Ok(EthExecuteOutput { receipts, requests, gas_used }) } /// Apply settings before a new block is executed. @@ -308,8 +325,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, @@ -366,12 +383,13 @@ where /// State changes are committed to the database. fn execute(mut self, input: Self::Input<'_>) -> Result { let BlockExecutionInput { block, total_difficulty } = input; - let (receipts, gas_used) = self.execute_and_verify(block, total_difficulty)?; + let EthExecuteOutput { receipts, requests, gas_used } = + self.execute_and_verify(block, total_difficulty)?; // NOTE: we need to merge keep the reverts for the bundle retention self.state.merge_transitions(BundleRetention::Reverts); - Ok(BlockExecutionOutput { state: self.state.take_bundle(), receipts, gas_used }) + Ok(BlockExecutionOutput { state: self.state.take_bundle(), receipts, requests, gas_used }) } } @@ -408,7 +426,8 @@ where fn execute_one(&mut self, input: Self::Input<'_>) -> Result<(), Self::Error> { let BlockExecutionInput { block, total_difficulty } = input; - let (receipts, _gas_used) = self.executor.execute_and_verify(block, total_difficulty)?; + let EthExecuteOutput { receipts, requests, gas_used: _ } = + self.executor.execute_and_verify(block, total_difficulty)?; // prepare the state according to the prune mode let retention = self.batch_record.bundle_retention(block.number); @@ -417,6 +436,9 @@ where // store receipts in the set self.batch_record.save_receipts(receipts)?; + // store requests in the set + self.batch_record.save_requests(requests); + if self.batch_record.first_block().is_none() { self.batch_record.set_first_block(block.number); } @@ -430,6 +452,7 @@ where BatchBlockExecutionOutput::new( self.executor.state.take_bundle(), self.batch_record.take_receipts(), + self.batch_record.take_requests(), self.batch_record.first_block().unwrap_or_default(), ) } diff --git a/crates/ethereum/evm/src/instructions/eip3074.rs b/crates/ethereum/evm/src/instructions/eip3074.rs index 6e5b93180e9..9180e13fcda 100644 --- a/crates/ethereum/evm/src/instructions/eip3074.rs +++ b/crates/ethereum/evm/src/instructions/eip3074.rs @@ -111,13 +111,13 @@ fn auth_instruction( ctx.remove(AUTHORIZED_VAR_NAME); push!(interp, B256::ZERO.into()); interp.instruction_result = InstructionResult::Continue; - return; + return } acc } Err(_) => { interp.instruction_result = InstructionResult::Revert; - return; + return } }; let nonce = authority_account.0.info.nonce; @@ -132,7 +132,7 @@ fn auth_instruction( Ok(signer) => signer, Err(_) => { interp.instruction_result = InstructionResult::Revert; - return; + return } }; @@ -164,7 +164,7 @@ fn authcall_instruction( Some(address) => Address::from_slice(&address), None => { interp.instruction_result = InstructionResult::Revert; - return; + return } }; @@ -181,7 +181,7 @@ fn authcall_instruction( pop!(interp, value); if interp.is_static && value != U256::ZERO { interp.instruction_result = InstructionResult::CallNotAllowedInsideStatic; - return; + return } let Some((input, return_memory_offset)) = get_memory_input_and_out_ranges(interp) else { diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 854dcd95a20..177b2e650bf 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -17,6 +17,9 @@ revm-primitives.workspace = true revm.workspace = true reth-interfaces.workspace = true +# alloy +alloy-consensus.workspace = true + futures-util.workspace = true parking_lot = { workspace = true, optional = true } @@ -24,4 +27,4 @@ parking_lot = { workspace = true, optional = true } parking_lot.workspace = true [features] -test-utils = ["dep:parking_lot"] \ No newline at end of file +test-utils = ["dep:parking_lot"] diff --git a/crates/evm/src/execute.rs b/crates/evm/src/execute.rs index e7ce09e7980..3c9750e125a 100644 --- a/crates/evm/src/execute.rs +++ b/crates/evm/src/execute.rs @@ -1,7 +1,10 @@ //! Traits for execution. +use alloy_consensus::Request; use reth_interfaces::{executor::BlockExecutionError, provider::ProviderError}; -use reth_primitives::{BlockNumber, BlockWithSenders, PruneModes, Receipt, Receipts, U256}; +use reth_primitives::{ + BlockNumber, BlockWithSenders, PruneModes, Receipt, Receipts, Requests, U256, +}; use revm::db::BundleState; use revm_primitives::db::Database; @@ -80,6 +83,8 @@ pub struct BlockExecutionOutput { pub state: BundleState, /// All the receipts of the transactions in the block. pub receipts: Vec, + /// All the EIP-7685 requests of the transactions in the block. + pub requests: Vec, /// The total gas used by the block. pub gas_used: u64, } @@ -95,14 +100,26 @@ pub struct BatchBlockExecutionOutput { /// /// If receipt is None it means it is pruned. pub receipts: Receipts, + /// The collection of EIP-7685 requests. + /// Outer vector stores requests for each block sequentially. + /// The inner vector stores requests ordered by transaction number. + /// + /// A transaction may have zero or more requests, so the length of the inner vector is not + /// guaranteed to be the same as the number of transactions. + pub requests: Requests, /// First block of bundle state. pub first_block: BlockNumber, } impl BatchBlockExecutionOutput { /// Create Bundle State. - pub fn new(bundle: BundleState, receipts: Receipts, first_block: BlockNumber) -> Self { - Self { bundle, receipts, first_block } + pub fn new( + bundle: BundleState, + receipts: Receipts, + requests: Requests, + first_block: BlockNumber, + ) -> Self { + Self { bundle, receipts, requests, first_block } } } diff --git a/crates/evm/src/test_utils.rs b/crates/evm/src/test_utils.rs index e0ee4691704..5e17117f313 100644 --- a/crates/evm/src/test_utils.rs +++ b/crates/evm/src/test_utils.rs @@ -55,6 +55,7 @@ impl Executor for MockExecutorProvider { state: bundle, receipts: receipts.into_iter().flatten().flatten().collect(), gas_used: 0, + requests: vec![], }) } } diff --git a/crates/interfaces/src/executor.rs b/crates/interfaces/src/executor.rs index bbaa547ad14..84ab6760509 100644 --- a/crates/interfaces/src/executor.rs +++ b/crates/interfaces/src/executor.rs @@ -83,11 +83,17 @@ pub enum BlockValidationError { /// /// [EIP-2935]: https://eips.ethereum.org/EIPS/eip-2935 #[error("failed to apply EIP-2935 pre-block state transition: {message}")] - // todo: better variant name + // TODO: better variant name Eip2935StateTransition { /// The error message. message: String, }, + /// 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/net/discv4/src/lib.rs b/crates/net/discv4/src/lib.rs index 77cc309ebf9..2019f58ee16 100644 --- a/crates/net/discv4/src/lib.rs +++ b/crates/net/discv4/src/lib.rs @@ -2266,7 +2266,7 @@ mod tests { assert!(service.pending_pings.contains_key(&node.id)); assert_eq!(service.pending_pings.len(), num_inserted); if num_inserted == MAX_NODES_PING { - break; + break } } } diff --git a/crates/net/downloaders/src/bodies/bodies.rs b/crates/net/downloaders/src/bodies/bodies.rs index 8f97e09c7dd..a806f2fa62e 100644 --- a/crates/net/downloaders/src/bodies/bodies.rs +++ b/crates/net/downloaders/src/bodies/bodies.rs @@ -95,7 +95,7 @@ where max_non_empty: u64, ) -> DownloadResult>> { if range.is_empty() || max_non_empty == 0 { - return Ok(None); + return Ok(None) } // Collect headers while @@ -144,7 +144,7 @@ where // if we're only connected to a few peers, we keep it low if num_peers < *self.concurrent_requests_range.start() { - return max_requests; + return max_requests } max_requests.min(*self.concurrent_requests_range.end()) @@ -238,7 +238,7 @@ where .skip_while(|b| b.block_number() < expected) .take_while(|b| self.download_range.contains(&b.block_number())) .collect() - }); + }) } // Drop buffered response since we passed that range @@ -257,7 +257,7 @@ where self.queued_bodies.shrink_to_fit(); self.metrics.total_flushed.increment(next_batch.len() as u64); self.metrics.queued_blocks.set(self.queued_bodies.len() as f64); - return Some(next_batch); + return Some(next_batch) } None } @@ -354,13 +354,13 @@ where fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); if this.is_terminated() { - return Poll::Ready(None); + return Poll::Ready(None) } // Submit new requests and poll any in progress loop { // Yield next batch if ready if let Some(next_batch) = this.try_split_next_batch() { - return Poll::Ready(Some(Ok(next_batch))); + return Poll::Ready(Some(Ok(next_batch))) } // Poll requests @@ -373,7 +373,7 @@ where Err(error) => { tracing::debug!(target: "downloaders::bodies", %error, "Request failed"); this.clear(); - return Poll::Ready(Some(Err(error))); + return Poll::Ready(Some(Err(error))) } }; } @@ -396,7 +396,7 @@ where Err(error) => { tracing::error!(target: "downloaders::bodies", %error, "Failed to download from next request"); this.clear(); - return Poll::Ready(Some(Err(error))); + return Poll::Ready(Some(Err(error))) } }; } @@ -409,21 +409,21 @@ where this.buffered_responses.shrink_to_fit(); if !new_request_submitted { - break; + break } } // All requests are handled, stream is finished if this.in_progress_queue.is_empty() { if this.queued_bodies.is_empty() { - return Poll::Ready(None); + return Poll::Ready(None) } let batch_size = this.stream_batch_size.min(this.queued_bodies.len()); let next_batch = this.queued_bodies.drain(..batch_size).collect::>(); this.queued_bodies.shrink_to_fit(); this.metrics.total_flushed.increment(next_batch.len() as u64); this.metrics.queued_blocks.set(this.queued_bodies.len() as f64); - return Poll::Ready(Some(Ok(next_batch))); + return Poll::Ready(Some(Ok(next_batch))) } Poll::Pending diff --git a/crates/net/eth-wire/src/multiplex.rs b/crates/net/eth-wire/src/multiplex.rs index 04b7cda37e5..82172f8d5c7 100644 --- a/crates/net/eth-wire/src/multiplex.rs +++ b/crates/net/eth-wire/src/multiplex.rs @@ -479,9 +479,9 @@ where if let Err(disconnect_err) = this.inner.conn.start_disconnect(DisconnectReason::DisconnectRequested) { - return Poll::Ready(Some(Err(disconnect_err.into()))); + return Poll::Ready(Some(Err(disconnect_err.into()))) } - return Poll::Ready(Some(Err(err.into()))); + return Poll::Ready(Some(Err(err.into()))) } Poll::Pending => { conn_ready = false; diff --git a/crates/node-core/src/args/pruning.rs b/crates/node-core/src/args/pruning.rs index 4adc721586b..e585a216dc7 100644 --- a/crates/node-core/src/args/pruning.rs +++ b/crates/node-core/src/args/pruning.rs @@ -20,7 +20,7 @@ impl PruningArgs { /// Returns pruning configuration. pub fn prune_config(&self, chain_spec: &ChainSpec) -> Option { if !self.full { - return None; + return None } Some(PruneConfig { block_interval: 5, diff --git a/crates/node-core/src/init.rs b/crates/node-core/src/init.rs index 6d924b6b1a4..05435ce37e9 100644 --- a/crates/node-core/src/init.rs +++ b/crates/node-core/src/init.rs @@ -373,7 +373,7 @@ fn parse_accounts( while let Ok(n) = reader.read_line(&mut line) { if n == 0 { - break; + break } let GenesisAccountWithAddress { genesis_account, address } = serde_json::from_str(&line)?; diff --git a/crates/node-core/src/node_config.rs b/crates/node-core/src/node_config.rs index 5cb28c87307..ad73d095457 100644 --- a/crates/node-core/src/node_config.rs +++ b/crates/node-core/src/node_config.rs @@ -411,7 +411,7 @@ impl NodeConfig { // try to look up the header in the database if let Some(header) = header { info!(target: "reth::cli", ?tip, "Successfully looked up tip block in the database"); - return Ok(header.number); + return Ok(header.number) } Ok(self.fetch_tip_from_network(client, tip.into()).await?.number) @@ -434,7 +434,7 @@ impl NodeConfig { match get_single_header(&client, tip).await { Ok(tip_header) => { info!(target: "reth::cli", ?tip, "Successfully fetched tip"); - return Ok(tip_header); + return Ok(tip_header) } Err(error) => { fetch_failures += 1; diff --git a/crates/optimism/evm/src/execute.rs b/crates/optimism/evm/src/execute.rs index f729ceda1c7..3b3b5bf2305 100644 --- a/crates/optimism/evm/src/execute.rs +++ b/crates/optimism/evm/src/execute.rs @@ -157,12 +157,12 @@ where transaction_gas_limit: transaction.gas_limit(), block_available_gas, } - .into()); + .into()) } // An optimism block should never contain blob transactions. if matches!(transaction.tx_type(), TxType::Eip4844) { - return Err(OptimismBlockExecutionError::BlobTransactionRejected.into()); + return Err(OptimismBlockExecutionError::BlobTransactionRejected.into()) } // Cache the depositor account prior to the state transition for the deposit nonce. @@ -228,7 +228,7 @@ where gas: GotExpected { got: cumulative_gas_used, expected: block.gas_used }, gas_spent_by_tx: receipts.gas_spent_by_tx()?, } - .into()); + .into()) } Ok((receipts, cumulative_gas_used)) @@ -325,7 +325,7 @@ where block.timestamp, ) { debug!(target: "evm", %error, ?receipts, "receipts verification failed"); - return Err(error); + return Err(error) }; } @@ -388,7 +388,12 @@ where // NOTE: we need to merge keep the reverts for the bundle retention self.state.merge_transitions(BundleRetention::Reverts); - Ok(BlockExecutionOutput { state: self.state.take_bundle(), receipts, gas_used }) + Ok(BlockExecutionOutput { + state: self.state.take_bundle(), + receipts, + gas_used, + requests: Default::default(), + }) } } @@ -450,6 +455,7 @@ where BatchBlockExecutionOutput::new( self.executor.state.take_bundle(), self.batch_record.take_receipts(), + self.batch_record.take_requests(), self.batch_record.first_block().unwrap_or_default(), ) } diff --git a/crates/payload/validator/src/lib.rs b/crates/payload/validator/src/lib.rs index 6b95b042576..7f85a0177ae 100644 --- a/crates/payload/validator/src/lib.rs +++ b/crates/payload/validator/src/lib.rs @@ -155,7 +155,7 @@ impl ExecutionPayloadValidator { let shanghai_active = self.is_shanghai_active_at_timestamp(sealed_block.timestamp); if !shanghai_active && sealed_block.withdrawals.is_some() { // shanghai not active but withdrawals present - return Err(PayloadError::PreShanghaiBlockWithWitdrawals); + return Err(PayloadError::PreShanghaiBlockWithWitdrawals) } // EIP-4844 checks diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index d9d6c592e79..d5c339c1251 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -21,6 +21,7 @@ revm-primitives = { workspace = true, features = ["serde"] } # ethereum alloy-chains = { workspace = true, features = ["serde", "rlp"] } +alloy-consensus.workspace = true alloy-primitives = { workspace = true, features = ["rand", "rlp"] } alloy-rlp = { workspace = true, features = ["arrayvec"] } alloy-trie = { workspace = true, features = ["serde"] } diff --git a/crates/primitives/src/compression/mod.rs b/crates/primitives/src/compression/mod.rs index 200b6bc4360..b0a3fd2fe50 100644 --- a/crates/primitives/src/compression/mod.rs +++ b/crates/primitives/src/compression/mod.rs @@ -69,7 +69,7 @@ impl ReusableDecompressor { reserved_upper_bound = true; if let Some(upper_bound) = Decompressor::upper_bound(src) { if let Some(additional) = upper_bound.checked_sub(self.buf.capacity()) { - break 'b additional; + break 'b additional } } } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index d7317951e18..178236d023b 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -39,6 +39,7 @@ mod net; pub mod proofs; mod prune; mod receipt; +mod request; /// Helpers for working with revm pub mod revm; pub mod stage; @@ -83,6 +84,7 @@ pub use prune::{ MINIMUM_PRUNING_DISTANCE, }; pub use receipt::{Receipt, ReceiptWithBloom, ReceiptWithBloomRef, Receipts}; +pub use request::Requests; pub use static_file::StaticFileSegment; pub use storage::StorageEntry; diff --git a/crates/primitives/src/receipt.rs b/crates/primitives/src/receipt.rs index 63955a1d13b..c6478ba928b 100644 --- a/crates/primitives/src/receipt.rs +++ b/crates/primitives/src/receipt.rs @@ -128,7 +128,7 @@ impl Receipts { if let Some(receipt) = tx_r.as_ref() { out.push((id as u64, receipt.cumulative_gas_used)); } else { - return Err(PruneSegmentError::ReceiptsPruned); + return Err(PruneSegmentError::ReceiptsPruned) } } Ok(out) @@ -312,7 +312,7 @@ impl ReceiptWithBloom { let b = &mut &**buf; let rlp_head = alloy_rlp::Header::decode(b)?; if !rlp_head.list { - return Err(alloy_rlp::Error::UnexpectedString); + return Err(alloy_rlp::Error::UnexpectedString) } let started_len = b.len(); @@ -357,7 +357,7 @@ impl ReceiptWithBloom { return Err(alloy_rlp::Error::ListLengthMismatch { expected: rlp_head.payload_length, got: consumed, - }); + }) } *buf = *b; Ok(this) @@ -510,7 +510,7 @@ impl<'a> ReceiptWithBloomEncoder<'a> { fn encode_inner(&self, out: &mut dyn BufMut, with_header: bool) { if matches!(self.receipt.tx_type, TxType::Legacy) { self.encode_fields(out); - return; + return } let mut payload = Vec::new(); diff --git a/crates/primitives/src/request.rs b/crates/primitives/src/request.rs new file mode 100644 index 00000000000..1eb6d94accd --- /dev/null +++ b/crates/primitives/src/request.rs @@ -0,0 +1,25 @@ +use alloy_consensus::Request; + +/// A collection of requests organized as a two-dimensional vector. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct Requests { + /// A two-dimensional vector of [Request] instances. + pub request_vec: Vec>, +} + +impl Requests { + /// Create a new [Requests] instance with an empty vector. + pub fn new() -> Self { + Self { request_vec: vec![] } + } + + /// Create a new [Requests] instance from an existing vector. + pub fn from_vec(vec: Vec>) -> Self { + Self { request_vec: vec } + } + + /// Push a new vector of [requests](Request) into the [Requests] collection. + pub fn push(&mut self, requests: Vec) { + self.request_vec.push(requests); + } +} diff --git a/crates/primitives/src/revm/env.rs b/crates/primitives/src/revm/env.rs index f519abc0582..2858c54a50c 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/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 3117615e842..af80c8271bb 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -1410,7 +1410,7 @@ impl Decodable for TransactionSigned { /// header if the first byte is less than `0xf7`. fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { if buf.is_empty() { - return Err(RlpError::InputTooShort); + return Err(RlpError::InputTooShort) } // decode header diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index 87d30ca6f29..f33e91f63ed 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -23,7 +23,9 @@ reth-trie = { workspace = true, optional = true } revm.workspace = true # alloy +alloy-consensus.workspace = true 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 544a74a5c09..f30fcd8d6cd 100644 --- a/crates/revm/src/batch.rs +++ b/crates/revm/src/batch.rs @@ -1,9 +1,10 @@ //! Helper for handling execution of multiple blocks. use crate::{precompile::Address, primitives::alloy_primitives::BlockNumber}; +use alloy_consensus::Request; use reth_interfaces::executor::BlockExecutionError; use reth_primitives::{ - PruneMode, PruneModes, PruneSegmentError, Receipt, Receipts, MINIMUM_PRUNING_DISTANCE, + PruneMode, PruneModes, PruneSegmentError, Receipt, Receipts, Requests, MINIMUM_PRUNING_DISTANCE, }; use revm::db::states::bundle_state::BundleRetention; use std::time::Duration; @@ -23,6 +24,13 @@ pub struct BlockBatchRecord { /// /// If receipt is None it means it is pruned. receipts: Receipts, + /// The collection of EIP-7685 requests. + /// Outer vector stores requests for each block sequentially. + /// The inner vector stores requests ordered by transaction number. + /// + /// A transaction may have zero or more requests, so the length of the inner vector is not + /// guaranteed to be the same as the number of transactions. + requests: Requests, /// Memoized address pruning filter. /// Empty implies that there is going to be addresses to include in the filter in a future /// block. None means there isn't any kind of configuration. @@ -75,6 +83,11 @@ impl BlockBatchRecord { std::mem::take(&mut self.receipts) } + /// Returns all recorded requests. + pub fn take_requests(&mut self) -> Requests { + std::mem::take(&mut self.requests) + } + /// Returns the [BundleRetention] for the given block based on the configured prune modes. pub fn bundle_retention(&self, block_number: BlockNumber) -> BundleRetention { if self.tip.map_or(true, |tip| { @@ -155,6 +168,11 @@ impl BlockBatchRecord { Ok(()) } + + /// Save EIP-7685 requests to the executor. + pub fn save_requests(&mut self, requests: Vec) { + self.requests.push(requests); + } } /// Block execution statistics. Contains duration of each step of block execution. diff --git a/crates/revm/src/state_change.rs b/crates/revm/src/state_change.rs index fa2b3818060..8531ba4a2b2 100644 --- a/crates/revm/src/state_change.rs +++ b/crates/revm/src/state_change.rs @@ -1,12 +1,19 @@ +use alloy_consensus::Request; +use alloy_rlp::Decodable; use reth_consensus_common::calc; use reth_interfaces::executor::{BlockExecutionError, BlockValidationError}; use reth_primitives::{ - address, revm::env::fill_tx_env_with_beacon_root_contract_call, Address, ChainSpec, Header, - Withdrawal, B256, U256, + address, + revm::env::{ + fill_tx_env_with_beacon_root_contract_call, + fill_tx_env_with_withdrawal_requests_contract_call, + }, + Address, ChainSpec, Header, Withdrawal, B256, U256, }; use revm::{ + self, interpreter::Host, - primitives::{Account, AccountInfo, StorageSlot}, + primitives::{Account, AccountInfo, ExecutionResult, ResultAndState, StorageSlot}, Database, DatabaseCommit, Evm, }; use std::{collections::HashMap, ops::Rem}; @@ -282,3 +289,65 @@ 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 post_block_withdrawal_requests( + chain_spec: &ChainSpec, + block_timestamp: u64, + evm: &mut Evm<'_, EXT, DB>, +) -> Result, BlockExecutionError> +where + DB::Error: std::fmt::Display, +{ + if !chain_spec.is_prague_active_at_timestamp(block_timestamp) { + return Ok(vec![]) + } + + // 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: e.to_string(), + } + .into()) + } + }; + + state.remove(&alloy_eips::eip7002::SYSTEM_ADDRESS); + state.remove(&evm.block().coinbase); + + let withdrawal_requests = match result { + ExecutionResult::Success { output, .. } => { + Vec::::decode(&mut output.into_data().as_ref()).map_err(|err| { + BlockValidationError::WithdrawalRequestsContractCall { message: err.to_string() } + }) + } + ExecutionResult::Revert { output, .. } => { + Err(BlockValidationError::WithdrawalRequestsContractCall { + message: format!("execution reverted: {output}"), + }) + } + ExecutionResult::Halt { reason, .. } => { + Err(BlockValidationError::WithdrawalRequestsContractCall { + message: format!("execution halted: {reason:?}"), + }) + } + }?; + evm.context.evm.db.commit(state); + + // re-set the previous env + evm.context.evm.env = previous_env; + + Ok(withdrawal_requests) +} diff --git a/crates/rpc/ipc/src/server/ipc.rs b/crates/rpc/ipc/src/server/ipc.rs index c73d9bb9367..33ed8d2d553 100644 --- a/crates/rpc/ipc/src/server/ipc.rs +++ b/crates/rpc/ipc/src/server/ipc.rs @@ -151,7 +151,7 @@ where return Some(batch_response_error( Id::Null, reject_too_big_request(max_request_body_size as u32), - )); + )) } // Single request or notification diff --git a/crates/rpc/ipc/src/server/mod.rs b/crates/rpc/ipc/src/server/mod.rs index 04608745484..2bec090cc44 100644 --- a/crates/rpc/ipc/src/server/mod.rs +++ b/crates/rpc/ipc/src/server/mod.rs @@ -871,7 +871,7 @@ mod tests { // and you might want to do something smarter if it's // critical that "the most recent item" must be sent when it is produced. if sink.send(notif).await.is_err() { - break Ok(()); + break Ok(()) } closed = c; diff --git a/crates/rpc/rpc-types/src/mev.rs b/crates/rpc/rpc-types/src/mev.rs index 9126c09635d..5da5a5667da 100644 --- a/crates/rpc/rpc-types/src/mev.rs +++ b/crates/rpc/rpc-types/src/mev.rs @@ -755,7 +755,7 @@ mod u256_numeric_string { match val { serde_json::Value::String(s) => { if let Ok(val) = s.parse::() { - return Ok(U256::from(val)); + return Ok(U256::from(val)) } U256::from_str(&s).map_err(de::Error::custom) } diff --git a/crates/rpc/rpc/src/otterscan.rs b/crates/rpc/rpc/src/otterscan.rs index 2f62e66a31d..1682f6f88d7 100644 --- a/crates/rpc/rpc/src/otterscan.rs +++ b/crates/rpc/rpc/src/otterscan.rs @@ -129,7 +129,7 @@ where if tx_len != receipts.len() { return Err(internal_rpc_err( "the number of transactions does not match the number of receipts", - )); + )) } // make sure the block is full diff --git a/crates/stages/src/stages/execution.rs b/crates/stages/src/stages/execution.rs index 0f933cea782..26d144bf757 100644 --- a/crates/stages/src/stages/execution.rs +++ b/crates/stages/src/stages/execution.rs @@ -272,7 +272,8 @@ where } } let time = Instant::now(); - let BatchBlockExecutionOutput { bundle, receipts, first_block } = executor.finalize(); + let BatchBlockExecutionOutput { bundle, receipts, requests: _, first_block } = + executor.finalize(); let state = BundleStateWithReceipts::new(bundle, receipts, first_block); let write_preparation_duration = time.elapsed(); diff --git a/crates/storage/nippy-jar/src/lib.rs b/crates/storage/nippy-jar/src/lib.rs index 1abbfba75cc..b7cf2c56eb0 100644 --- a/crates/storage/nippy-jar/src/lib.rs +++ b/crates/storage/nippy-jar/src/lib.rs @@ -530,7 +530,7 @@ impl DataReader { let offset_end = index + self.offset_size as usize; if offset_end > self.offset_mmap.len() { - return Err(NippyJarError::OffsetOutOfBounds { index }); + return Err(NippyJarError::OffsetOutOfBounds { index }) } buffer[..self.offset_size as usize].copy_from_slice(&self.offset_mmap[index..offset_end]); diff --git a/crates/storage/provider/src/bundle_state/bundle_state_with_receipts.rs b/crates/storage/provider/src/bundle_state/bundle_state_with_receipts.rs index 5f6d4af3f84..b7ce4d176c7 100644 --- a/crates/storage/provider/src/bundle_state/bundle_state_with_receipts.rs +++ b/crates/storage/provider/src/bundle_state/bundle_state_with_receipts.rs @@ -9,8 +9,8 @@ use reth_interfaces::provider::{ProviderError, ProviderResult}; use reth_primitives::{ logs_bloom, revm::compat::{into_reth_acc, into_revm_acc}, - Account, Address, BlockNumber, Bloom, Bytecode, Log, Receipt, Receipts, StaticFileSegment, - StorageEntry, B256, U256, + Account, Address, BlockNumber, Bloom, Bytecode, Log, Receipt, Receipts, Requests, + StaticFileSegment, StorageEntry, B256, U256, }; use reth_trie::HashedPostState; pub use revm::db::states::OriginalValuesKnown; @@ -38,16 +38,18 @@ pub struct BundleStateWithReceipts { // TODO(mattsse): unify the types, currently there's a cyclic dependency between impl From for BundleStateWithReceipts { fn from(value: BatchBlockExecutionOutput) -> Self { - let BatchBlockExecutionOutput { bundle, receipts, first_block } = value; + let BatchBlockExecutionOutput { bundle, receipts, requests: _, first_block } = value; Self { bundle, receipts, first_block } } } // TODO(mattsse): unify the types, currently there's a cyclic dependency between +#[cfg(any(test, feature = "test-utils"))] impl From for BatchBlockExecutionOutput { fn from(value: BundleStateWithReceipts) -> Self { let BundleStateWithReceipts { bundle, receipts, first_block } = value; - Self { bundle, receipts, first_block } + // TODO(alexey): add requests + Self { bundle, receipts, requests: Requests::default(), first_block } } } diff --git a/crates/storage/provider/src/providers/static_file/writer.rs b/crates/storage/provider/src/providers/static_file/writer.rs index 3a0f2d03174..a81ef5b005b 100644 --- a/crates/storage/provider/src/providers/static_file/writer.rs +++ b/crates/storage/provider/src/providers/static_file/writer.rs @@ -522,7 +522,7 @@ impl StaticFileProviderRW { if self.prune_on_commit.is_some() { return Err(ProviderError::NippyJar( "Pruning should be comitted before appending or pruning more data".to_string(), - )); + )) } Ok(()) }