diff --git a/crates/rpc/rpc-api/src/eth.rs b/crates/rpc/rpc-api/src/eth.rs index 6ca403dd7d11..385442f5f3f1 100644 --- a/crates/rpc/rpc-api/src/eth.rs +++ b/crates/rpc/rpc-api/src/eth.rs @@ -4,8 +4,9 @@ use reth_primitives::{ AccessListWithGasUsed, Address, BlockId, BlockNumberOrTag, Bytes, H256, H64, U256, U64, }; use reth_rpc_types::{ - state::StateOverride, BlockOverrides, CallRequest, EIP1186AccountProofResponse, FeeHistory, - Index, RichBlock, SyncStatus, Transaction, TransactionReceipt, TransactionRequest, Work, + state::StateOverride, BlockOverrides, Bundle, CallRequest, EIP1186AccountProofResponse, + EthCallResponse, FeeHistory, Index, RichBlock, StateContext, SyncStatus, Transaction, + TransactionReceipt, TransactionRequest, Work, }; /// Eth rpc interface: @@ -153,6 +154,16 @@ pub trait EthApi { block_overrides: Option>, ) -> RpcResult; + /// Simulate arbitrary number of transactions at an arbitrary blockchain index, with the + /// optionality of state overrides + #[method(name = "callMany")] + async fn call_many( + &self, + bundle: Bundle, + state_context: Option, + state_override: Option, + ) -> RpcResult>; + /// Generates an access list for a transaction. /// /// This method creates an [EIP2930](https://eips.ethereum.org/EIPS/eip-2930) type accessList based on a given Transaction. diff --git a/crates/rpc/rpc-types/src/eth/call.rs b/crates/rpc/rpc-types/src/eth/call.rs index 231eb3473c83..19db85b77e75 100644 --- a/crates/rpc/rpc-types/src/eth/call.rs +++ b/crates/rpc/rpc-types/src/eth/call.rs @@ -23,6 +23,18 @@ pub struct StateContext { pub transaction_index: Option, } +/// CallResponse for eth_callMany +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct EthCallResponse { + #[serde(skip_serializing_if = "Option::is_none")] + /// eth_call output (if no error) + pub output: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// eth_call output (if error) + pub error: Option, +} + /// Represents a transaction index where -1 means all transactions #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] pub enum TransactionIndex { diff --git a/crates/rpc/rpc-types/src/eth/mod.rs b/crates/rpc/rpc-types/src/eth/mod.rs index 528e4cffc769..ae249e197280 100644 --- a/crates/rpc/rpc-types/src/eth/mod.rs +++ b/crates/rpc/rpc-types/src/eth/mod.rs @@ -19,7 +19,7 @@ mod work; pub use account::*; pub use block::*; -pub use call::{Bundle, CallInput, CallInputError, CallRequest, StateContext}; +pub use call::{Bundle, CallInput, CallInputError, CallRequest, EthCallResponse, StateContext}; pub use fee::{FeeHistory, TxGasAndReward}; pub use filter::*; pub use index::Index; diff --git a/crates/rpc/rpc/src/eth/api/call.rs b/crates/rpc/rpc/src/eth/api/call.rs index 29cbdcfc4460..d7c0a065325c 100644 --- a/crates/rpc/rpc/src/eth/api/call.rs +++ b/crates/rpc/rpc/src/eth/api/call.rs @@ -5,7 +5,7 @@ use crate::{ error::{ensure_success, EthApiError, EthResult, RevertError, RpcInvalidTransactionError}, revm_utils::{ build_call_evm_env, caller_gas_allowance, cap_tx_gas_limit_with_caller_allowance, - get_precompiles, inspect, transact, EvmOverrides, + get_precompiles, inspect, prepare_call_env, transact, EvmOverrides, }, EthTransactions, }, @@ -18,12 +18,16 @@ use reth_provider::{BlockReaderIdExt, EvmEnvProvider, StateProvider, StateProvid use reth_revm::{ access_list::AccessListInspector, database::{State, SubState}, + env::tx_env_with_recovered, +}; +use reth_rpc_types::{ + state::StateOverride, BlockError, Bundle, CallRequest, EthCallResponse, StateContext, }; -use reth_rpc_types::CallRequest; use reth_transaction_pool::TransactionPool; use revm::{ db::{CacheDB, DatabaseRef}, primitives::{BlockEnv, CfgEnv, Env, ExecutionResult, Halt, TransactTo}, + DatabaseCommit, }; use tracing::trace; @@ -62,6 +66,95 @@ where ensure_success(res.result) } + /// Simulate arbitrary number of transactions at an arbitrary blockchain index, with the + /// optionality of state overrides + pub async fn call_many( + &self, + bundle: Bundle, + state_context: Option, + state_override: Option, + ) -> EthResult> { + let Bundle { transactions, block_override } = bundle; + if transactions.is_empty() { + return Err(EthApiError::InvalidParams(String::from("transactions are empty."))) + } + + let StateContext { transaction_index, block_number } = state_context.unwrap_or_default(); + let transaction_index = transaction_index.unwrap_or_default(); + + let target_block = block_number.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)); + let ((cfg, block_env, _), block) = + futures::try_join!(self.evm_env_at(target_block), self.block_by_id(target_block))?; + + let block = block.ok_or_else(|| EthApiError::UnknownBlockNumber)?; + let gas_limit = self.inner.gas_cap; + + // we're essentially replaying the transactions in the block here, hence we need the state + // that points to the beginning of the block, which is the state at the parent block + let mut at = block.parent_hash; + let mut replay_block_txs = true; + + // but if all transactions are to be replayed, we can use the state at the block itself + let num_txs = transaction_index.index().unwrap_or(block.body.len()); + if num_txs == block.body.len() { + at = block.hash; + replay_block_txs = false; + } + + self.spawn_with_state_at_block(at.into(), move |state| { + let mut results = Vec::with_capacity(transactions.len()); + let mut db = SubState::new(State::new(state)); + + if replay_block_txs { + // only need to replay the transactions in the block if not all transactions are + // to be replayed + let transactions = block.body.into_iter().take(num_txs); + + // Execute all transactions until index + for tx in transactions { + let tx = tx.into_ecrecovered().ok_or(BlockError::InvalidSignature)?; + let tx = tx_env_with_recovered(&tx); + let env = Env { cfg: cfg.clone(), block: block_env.clone(), tx }; + let (res, _) = transact(&mut db, env)?; + db.commit(res.state); + } + } + + let overrides = EvmOverrides::new(state_override.clone(), block_override.map(Box::new)); + + let mut transactions = transactions.into_iter().peekable(); + while let Some(tx) = transactions.next() { + let env = prepare_call_env( + cfg.clone(), + block_env.clone(), + tx, + gas_limit, + &mut db, + overrides.clone(), + )?; + let (res, _) = transact(&mut db, env)?; + + match ensure_success(res.result) { + Ok(output) => { + results.push(EthCallResponse { output: Some(output), error: None }); + } + Err(err) => { + results + .push(EthCallResponse { output: None, error: Some(err.to_string()) }); + } + } + + if transactions.peek().is_some() { + // need to apply the state changes of this call before executing the next call + db.commit(res.state); + } + } + + Ok(results) + }) + .await + } + /// Estimates the gas usage of the `request` with the state. /// /// This will execute the [CallRequest] and find the best gas limit via binary search diff --git a/crates/rpc/rpc/src/eth/api/server.rs b/crates/rpc/rpc/src/eth/api/server.rs index 663308cd895c..32c985cf9831 100644 --- a/crates/rpc/rpc/src/eth/api/server.rs +++ b/crates/rpc/rpc/src/eth/api/server.rs @@ -21,8 +21,9 @@ use reth_provider::{ }; use reth_rpc_api::EthApiServer; use reth_rpc_types::{ - state::StateOverride, BlockOverrides, CallRequest, EIP1186AccountProofResponse, FeeHistory, - Index, RichBlock, SyncStatus, TransactionReceipt, TransactionRequest, Work, + state::StateOverride, BlockOverrides, Bundle, CallRequest, EIP1186AccountProofResponse, + EthCallResponse, FeeHistory, Index, RichBlock, StateContext, SyncStatus, TransactionReceipt, + TransactionRequest, Work, }; use reth_transaction_pool::TransactionPool; use serde_json::Value; @@ -245,6 +246,17 @@ where .await?) } + /// Handler for: `eth_callMany` + async fn call_many( + &self, + bundle: Bundle, + state_context: Option, + state_override: Option, + ) -> Result> { + trace!(target: "rpc::eth", ?bundle, ?state_context, ?state_override, "Serving eth_callMany"); + Ok(EthApi::call_many(self, bundle, state_context, state_override).await?) + } + /// Handler for: `eth_createAccessList` async fn create_access_list( &self,