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,