mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-01-29 17:18:08 -05:00
feat: add erigons debugTraceCallMany (#3878)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
@@ -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<BlockId>,
|
||||
opts: Option<GethDebugTracingCallOptions>,
|
||||
) -> RpcResult<GethTrace>;
|
||||
|
||||
/// 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<Bundle>,
|
||||
state_context: Option<StateContext>,
|
||||
opts: Option<GethDebugTracingOptions>,
|
||||
state_override: Option<StateOverride>,
|
||||
) -> RpcResult<Vec<GethTrace>>;
|
||||
}
|
||||
|
||||
@@ -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<CallRequest>,
|
||||
/// Block overides
|
||||
pub block_override: Option<BlockOverrides>,
|
||||
}
|
||||
|
||||
/// 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<BlockId>,
|
||||
/// Inclusive number of tx to replay in block. -1 means replay all
|
||||
pub transaction_index: Option<TransactionIndex>,
|
||||
}
|
||||
|
||||
/// 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<usize> {
|
||||
match self {
|
||||
TransactionIndex::All => None,
|
||||
TransactionIndex::Index(idx) => Some(*idx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for TransactionIndex {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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::<TransactionIndex>(s).unwrap();
|
||||
assert_eq!(idx, TransactionIndex::All);
|
||||
|
||||
let s = "5";
|
||||
let idx = serde_json::from_str::<TransactionIndex>(s).unwrap();
|
||||
assert_eq!(idx, TransactionIndex::Index(5));
|
||||
|
||||
let s = "-2";
|
||||
let res = serde_json::from_str::<TransactionIndex>(s);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_call_request() {
|
||||
let s = r#"{"accessList":[],"data":"0x0902f1ac","to":"0xa478c2975ab1ea89e8196811f51a7b7ade33eb11","type":"0x02"}"#;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Bundle>,
|
||||
state_context: Option<StateContext>,
|
||||
opts: Option<GethDebugTracingOptions>,
|
||||
state_override: Option<StateOverride>,
|
||||
) -> EthResult<Vec<GethTrace>> {
|
||||
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<Bundle>,
|
||||
state_context: Option<StateContext>,
|
||||
opts: Option<GethDebugTracingOptions>,
|
||||
state_override: Option<StateOverride>,
|
||||
) -> RpcResult<Vec<GethTrace>> {
|
||||
let _permit = self.acquire_trace_permit().await;
|
||||
Ok(DebugApi::debug_trace_call_many(self, bundles, state_context, opts, state_override)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Provider, Eth> std::fmt::Debug for DebugApi<Provider, Eth> {
|
||||
|
||||
Reference in New Issue
Block a user