diff --git a/crates/rpc/rpc-api/src/debug.rs b/crates/rpc/rpc-api/src/debug.rs index 0d29042b5e..6fba56c83d 100644 --- a/crates/rpc/rpc-api/src/debug.rs +++ b/crates/rpc/rpc-api/src/debug.rs @@ -1,11 +1,12 @@ use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use reth_primitives::{BlockId, BlockNumberOrTag, Bytes, H256}; use reth_rpc_types::{ + state::StateOverride, trace::geth::{ BlockTraceResult, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, TraceResult, }, - CallRequest, RichBlock, + Bundle, CallRequest, RichBlock, StateContext, }; /// Debug rpc interface. @@ -102,4 +103,24 @@ pub trait DebugApi { block_number: Option, opts: Option, ) -> RpcResult; + + /// The `debug_traceCallMany` method lets you run an `eth_callmany` within the context of the + /// given block execution using the final state of parent block as the base followed by n + /// transactions + /// + /// The first argument is a list of bundles. Each bundle can overwrite the block headers. This + /// will affect all transaction in that bundle. + /// BlockNumber and transaction_index are optinal. Transaction_index + /// specifys the number of tx in the block to replay and -1 means all transactions should be + /// replayed. + /// The trace can be configured similar to `debug_traceTransaction`. + /// State override apply to all bundles. + #[method(name = "traceCallMany")] + async fn debug_trace_call_many( + &self, + bundles: Vec, + state_context: Option, + opts: Option, + state_override: Option, + ) -> RpcResult>; } diff --git a/crates/rpc/rpc-types/src/eth/call.rs b/crates/rpc/rpc-types/src/eth/call.rs index 9aec714c12..231eb3473c 100644 --- a/crates/rpc/rpc-types/src/eth/call.rs +++ b/crates/rpc/rpc-types/src/eth/call.rs @@ -1,5 +1,80 @@ -use reth_primitives::{AccessList, Address, Bytes, U256, U64, U8}; -use serde::{Deserialize, Serialize}; +use reth_primitives::{AccessList, Address, BlockId, Bytes, U256, U64, U8}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::BlockOverrides; + +/// Bundle of transactions +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct Bundle { + /// Transactions + pub transactions: Vec, + /// Block overides + pub block_override: Option, +} + +/// State context for callMany +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct StateContext { + /// Block Number + pub block_number: Option, + /// Inclusive number of tx to replay in block. -1 means replay all + pub transaction_index: Option, +} + +/// Represents a transaction index where -1 means all transactions +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub enum TransactionIndex { + /// -1 means all transactions + #[default] + All, + /// Transaction index + Index(usize), +} + +impl TransactionIndex { + /// Returns true if this is the all variant + pub fn is_all(&self) -> bool { + matches!(self, TransactionIndex::All) + } + + /// Returns the index if this is the index variant + pub fn index(&self) -> Option { + match self { + TransactionIndex::All => None, + TransactionIndex::Index(idx) => Some(*idx), + } + } +} + +impl Serialize for TransactionIndex { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + TransactionIndex::All => serializer.serialize_i8(-1), + TransactionIndex::Index(idx) => idx.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for TransactionIndex { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + match isize::deserialize(deserializer)? { + -1 => Ok(TransactionIndex::All), + idx if idx < -1 => Err(serde::de::Error::custom(format!( + "Invalid transaction index, expected -1 or positive integer, got {}", + idx + ))), + idx => Ok(TransactionIndex::Index(idx as usize)), + } + } +} /// Call request #[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -111,6 +186,21 @@ pub struct CallInputError; mod tests { use super::*; + #[test] + fn transaction_index() { + let s = "-1"; + let idx = serde_json::from_str::(s).unwrap(); + assert_eq!(idx, TransactionIndex::All); + + let s = "5"; + let idx = serde_json::from_str::(s).unwrap(); + assert_eq!(idx, TransactionIndex::Index(5)); + + let s = "-2"; + let res = serde_json::from_str::(s); + assert!(res.is_err()); + } + #[test] fn serde_call_request() { let s = r#"{"accessList":[],"data":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#; diff --git a/crates/rpc/rpc-types/src/eth/mod.rs b/crates/rpc/rpc-types/src/eth/mod.rs index 446e4192f6..528e4cffc7 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::{CallInput, CallInputError, CallRequest}; +pub use call::{Bundle, CallInput, CallInputError, CallRequest, StateContext}; pub use fee::{FeeHistory, TxGasAndReward}; pub use filter::*; pub use index::Index; diff --git a/crates/rpc/rpc/src/debug.rs b/crates/rpc/rpc/src/debug.rs index 3fd5bdef06..cf7a98b745 100644 --- a/crates/rpc/rpc/src/debug.rs +++ b/crates/rpc/rpc/src/debug.rs @@ -2,8 +2,8 @@ use crate::{ eth::{ error::{EthApiError, EthResult}, revm_utils::{ - clone_into_empty_db, inspect, inspect_and_return_db, replay_transactions_until, - result_output, EvmOverrides, + clone_into_empty_db, inspect, inspect_and_return_db, prepare_call_env, + replay_transactions_until, result_output, transact, EvmOverrides, }, EthTransactions, TransactionSource, }, @@ -25,11 +25,12 @@ use reth_revm::{ use reth_rlp::{Decodable, Encodable}; use reth_rpc_api::DebugApiServer; use reth_rpc_types::{ + state::StateOverride, trace::geth::{ BlockTraceResult, FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, NoopFrame, TraceResult, }, - BlockError, CallRequest, RichBlock, + BlockError, Bundle, CallRequest, RichBlock, StateContext, }; use reth_tasks::TaskSpawner; use revm::{ @@ -333,6 +334,103 @@ where Ok(frame.into()) } + /// The debug_traceCallMany method lets you run an `eth_callMany` within the context of the + /// given block execution using the first n transactions in the given block as base + pub async fn debug_trace_call_many( + &self, + bundles: Vec, + state_context: Option, + opts: Option, + state_override: Option, + ) -> EthResult> { + if bundles.is_empty() { + return Err(EthApiError::InvalidParams(String::from("bundles 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.inner.eth_api.evm_env_at(target_block), + self.inner.eth_api.block_by_id(target_block), + )?; + + let block = block.ok_or_else(|| EthApiError::UnknownBlockNumber)?; + let tracing_options = opts.unwrap_or_default(); + let gas_limit = self.inner.eth_api.call_gas_limit(); + + // 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; + } + + let this = self.clone(); + self.inner + .eth_api + .spawn_with_state_at_block(at.into(), move |state| { + let mut results = Vec::with_capacity(bundles.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); + } + } + + // Trace all bundles + let mut bundles = bundles.into_iter().peekable(); + while let Some(bundle) = bundles.next() { + //let mut result = Vec::with_capacity(bundle.len()); + let Bundle { transactions, block_override } = bundle; + 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 (trace, state) = this.trace_transaction( + tracing_options.clone(), + env, + target_block, + &mut db, + )?; + + if bundles.peek().is_none() && transactions.peek().is_none() { + db.commit(state); + } + results.push(trace); + } + } + Ok(results) + }) + .await + } + /// Executes the configured transaction with the environment on the given database. /// /// Returns the trace frame and the state that got updated after executing the transaction. @@ -651,6 +749,18 @@ where Ok(DebugApi::debug_trace_call(self, request, block_number, opts.unwrap_or_default()) .await?) } + + async fn debug_trace_call_many( + &self, + bundles: Vec, + state_context: Option, + opts: Option, + state_override: Option, + ) -> RpcResult> { + let _permit = self.acquire_trace_permit().await; + Ok(DebugApi::debug_trace_call_many(self, bundles, state_context, opts, state_override) + .await?) + } } impl std::fmt::Debug for DebugApi {