diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 1185faf82..9a0f049f5 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -31,7 +31,7 @@ lint: - taplo@0.8.1 - terrascan@1.19.1 - trivy@0.51.1 - - trufflehog@3.75.1 + - trufflehog@3.76.0 - yamllint@1.35.1 ignore: - linters: [ALL] diff --git a/src/eth_provider/error.rs b/src/eth_provider/error.rs index 0ff0b24e0..0d0545e6a 100644 --- a/src/eth_provider/error.rs +++ b/src/eth_provider/error.rs @@ -27,7 +27,9 @@ pub enum EthRpcErrorCode { impl From for EthRpcErrorCode { fn from(error: EthApiError) -> Self { match error { - EthApiError::UnknownBlock | EthApiError::UnknownBlockNumber => EthRpcErrorCode::ResourceNotFound, + EthApiError::UnknownBlock | EthApiError::UnknownBlockNumber | EthApiError::TransactionNotFound => { + EthRpcErrorCode::ResourceNotFound + } EthApiError::InvalidBlockRange | EthApiError::Signature(_) | EthApiError::EthereumDataFormat(_) @@ -48,6 +50,9 @@ pub enum EthApiError { /// When an unknown block number is encountered #[error("unknown block number")] UnknownBlockNumber, + /// When a transaction is not found + #[error("transaction not found")] + TransactionNotFound, /// When an invalid block range is provided #[error("invalid block range")] InvalidBlockRange, diff --git a/src/eth_rpc/api/debug_api.rs b/src/eth_rpc/api/debug_api.rs index a4f4626a1..02d8950b8 100644 --- a/src/eth_rpc/api/debug_api.rs +++ b/src/eth_rpc/api/debug_api.rs @@ -2,7 +2,7 @@ use jsonrpsee::core::RpcResult as Result; use jsonrpsee::proc_macros::rpc; use reth_primitives::{Bytes, B256}; use reth_rpc_types::{ - trace::geth::{GethDebugTracingOptions, TraceResult}, + trace::geth::{GethDebugTracingOptions, GethTrace, TraceResult}, BlockId, BlockNumberOrTag, }; @@ -40,7 +40,7 @@ pub trait DebugApi { &self, block_number: BlockNumberOrTag, opts: Option, - ) -> Result>>; + ) -> Result>; /// Returns the Geth debug trace for the given block hash. #[method(name = "traceBlockByHash")] @@ -48,5 +48,13 @@ pub trait DebugApi { &self, block_hash: B256, opts: Option, - ) -> Result>>; + ) -> Result>; + + /// Returns the Geth debug trace for the given transaction hash. + #[method(name = "traceTransaction")] + async fn trace_transaction( + &self, + transaction_hash: B256, + opts: Option, + ) -> Result; } diff --git a/src/eth_rpc/servers/debug_rpc.rs b/src/eth_rpc/servers/debug_rpc.rs index 63d88e7e4..7c1d5f1f6 100644 --- a/src/eth_rpc/servers/debug_rpc.rs +++ b/src/eth_rpc/servers/debug_rpc.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use alloy_rlp::Encodable; use jsonrpsee::core::{async_trait, RpcResult as Result}; use reth_primitives::{Block, Bytes, Header, Log, Receipt, ReceiptWithBloom, TransactionSigned, B256}; -use reth_rpc_types::trace::geth::{GethDebugTracingOptions, TraceResult}; +use reth_rpc_types::trace::geth::{GethDebugTracingOptions, GethTrace, TraceResult}; use reth_rpc_types::{BlockId, BlockNumberOrTag}; use crate::eth_provider::error::{EthApiError, EthereumDataFormatError, SignatureError}; @@ -153,14 +153,10 @@ impl DebugApiServer for DebugRpc

&self, block_number: BlockNumberOrTag, opts: Option, - ) -> Result>> { + ) -> Result> { let provider = Arc::new(&self.eth_provider); - let maybe_tracer = - TracerBuilder::new(provider).await?.with_block_id(BlockId::Number(block_number)).await?.build()?; - if maybe_tracer.is_none() { - return Ok(None); - } - let tracer = maybe_tracer.unwrap(); + let tracer = TracerBuilder::new(provider).await?.with_block_id(BlockId::Number(block_number)).await?.build()?; + let traces = tracer.debug_block(opts.unwrap_or_default())?; Ok(traces) } @@ -170,15 +166,25 @@ impl DebugApiServer for DebugRpc

&self, block_hash: B256, opts: Option, - ) -> Result>> { + ) -> Result> { let provider = Arc::new(&self.eth_provider); - let maybe_tracer = + let tracer = TracerBuilder::new(provider).await?.with_block_id(BlockId::Hash(block_hash.into())).await?.build()?; - if maybe_tracer.is_none() { - return Ok(None); - } - let tracer = maybe_tracer.unwrap(); + let traces = tracer.debug_block(opts.unwrap_or_default())?; Ok(traces) } + + /// Returns the Geth debug trace for the given transaction hash. + async fn trace_transaction( + &self, + transaction_hash: B256, + opts: Option, + ) -> Result { + let provider = Arc::new(&self.eth_provider); + let tracer = TracerBuilder::new(provider).await?.with_transaction_hash(transaction_hash).await?.build()?; + + let trace = tracer.debug_transaction(transaction_hash, opts.unwrap_or_default())?; + Ok(trace) + } } diff --git a/src/eth_rpc/servers/trace_rpc.rs b/src/eth_rpc/servers/trace_rpc.rs index 5e69308e3..e09ccba5f 100644 --- a/src/eth_rpc/servers/trace_rpc.rs +++ b/src/eth_rpc/servers/trace_rpc.rs @@ -25,11 +25,8 @@ impl TraceApiServer for TraceRpc

/// Returns the parity traces for the given block. async fn trace_block(&self, block_id: BlockId) -> Result>> { let provider = Arc::new(&self.eth_provider); - let maybe_tracer = TracerBuilder::new(provider).await?.with_block_id(block_id).await?.build()?; - if maybe_tracer.is_none() { - return Ok(None); - } - let tracer = maybe_tracer.unwrap(); + let tracer = TracerBuilder::new(provider).await?.with_block_id(block_id).await?.build()?; + let traces = tracer.trace_block(TracingInspectorConfig::default_parity())?; Ok(traces) } diff --git a/src/tracing/builder.rs b/src/tracing/builder.rs index 0045e97b8..b59e6ad76 100644 --- a/src/tracing/builder.rs +++ b/src/tracing/builder.rs @@ -7,7 +7,7 @@ use crate::eth_provider::{ provider::EthereumProvider, }; -use super::{config::KakarotEvmConfig, database::EthDatabaseSnapshot, Tracer, TracerResult}; +use super::{database::EthDatabaseSnapshot, Tracer, TracerResult}; #[derive(Debug)] pub struct Floating; @@ -18,7 +18,7 @@ pub struct Pinned; pub struct TracerBuilder { eth_provider: P, env: Env, - block: Option, + block: reth_rpc_types::Block, _phantom: std::marker::PhantomData, } @@ -29,66 +29,68 @@ impl TracerBuilder { let env = Env { cfg, ..Default::default() }; - Ok(Self { eth_provider, env, block: None, _phantom: std::marker::PhantomData }) + Ok(Self { eth_provider, env, block: Default::default(), _phantom: std::marker::PhantomData }) } /// Sets the block to trace pub async fn with_block_id(self, block_id: BlockId) -> TracerResult> { - let maybe_block = self.block(block_id).await?; + let block = self.block(block_id).await?; Ok(TracerBuilder { eth_provider: self.eth_provider.clone(), env: self.env.clone(), - block: maybe_block, + block, _phantom: std::marker::PhantomData, }) } + /// Sets the block to trace given the transaction hash + pub async fn with_transaction_hash(self, transaction_hash: B256) -> TracerResult> { + let transaction = + self.eth_provider.transaction_by_hash(transaction_hash).await?.ok_or(EthApiError::TransactionNotFound)?; + + // we can't trace a pending transaction + if transaction.block_number.is_none() { + return Err(EthApiError::UnknownBlock); + } + + self.with_block_id(BlockId::Number(transaction.block_number.unwrap().into())).await + } + /// Fetches a block from the Ethereum provider given a block id /// /// # Returns /// /// Returns the block if it exists, otherwise returns None - async fn block(&self, block_id: BlockId) -> TracerResult> { - let maybe_block = match block_id { + async fn block(&self, block_id: BlockId) -> TracerResult { + let block = match block_id { BlockId::Hash(hash) => self.eth_provider.block_by_hash(hash.block_hash, true).await?, BlockId::Number(number) => self.eth_provider.block_by_number(number, true).await?, - }; - - if maybe_block.is_none() { - return Ok(None); } + .ok_or(EthApiError::UnknownBlock)?; // we can't trace a pending block - let block = maybe_block.unwrap(); if block.header.hash.unwrap_or_default().is_zero() { return Err(EthApiError::UnknownBlock); } - Ok(Some(block.inner)) + Ok(block.inner) } } impl TracerBuilder { - /// Builds the tracer. Returns None if the block was not found during the call to - /// `with_block_id`. - pub fn build(self) -> TracerResult>> { - // If block is None, it means that the block was not found - if self.block.is_none() { - return Ok(None); - } - let block = self.block.as_ref().unwrap(); - - let transactions = match &block.transactions { + /// Builds the tracer. + pub fn build(self) -> TracerResult> { + let transactions = match &self.block.transactions { BlockTransactions::Full(transactions) => transactions.clone(), _ => return Err(TransactionError::ExpectedFullTransactions.into()), }; let env = self.init_env_with_handler_config(); // DB should use the state of the parent block - let db = EthDatabaseSnapshot::new(self.eth_provider, BlockId::Hash(block.header.parent_hash.into())); + let db = EthDatabaseSnapshot::new(self.eth_provider, BlockId::Hash(self.block.header.parent_hash.into())); - Ok(Some(Tracer { env, transactions, cfg: KakarotEvmConfig, db })) + Ok(Tracer { env, transactions, db }) } /// Init an EnvWithHandlerCfg. @@ -102,7 +104,7 @@ impl TracerBuilder { let mut env = self.env.clone(); let Header { number, timestamp, gas_limit, miner, base_fee_per_gas, difficulty, .. } = - self.block.clone().expect("Block not set").header; + self.block.header.clone(); let block_env = BlockEnv { number: U256::from(number.unwrap_or_default()), timestamp: U256::from(timestamp), diff --git a/src/tracing/config.rs b/src/tracing/config.rs index 6ab63a764..77fe5ab83 100644 --- a/src/tracing/config.rs +++ b/src/tracing/config.rs @@ -1,46 +1,61 @@ use std::sync::Arc; use alphanet_instructions::{context::InstructionsContext, eip3074}; -use reth_revm::{inspector_handle_register, primitives::EnvWithHandlerCfg, Database, Evm, EvmBuilder}; +use reth_revm::{ + handler::register::HandleRegisterBox, inspector_handle_register, primitives::EnvWithHandlerCfg, Database, Evm, +}; #[derive(Debug, Clone)] -pub(super) struct KakarotEvmConfig; +pub(super) struct EvmBuilder; -impl KakarotEvmConfig { - /// Returns new EVM with the given database and env. Similar to the implementation of [reth_evm::ConfigureEvmEnv] +impl EvmBuilder { + /// Returns new EVM with the given database, env and inspector. Similar to the implementation of [reth_evm::ConfigureEvmEnv] /// but only keeping the necessary API. pub(super) fn evm_with_env_and_inspector<'a, DB: Database + 'a, I: reth_revm::Inspector>( - &self, db: DB, env: EnvWithHandlerCfg, inspector: I, ) -> Evm<'a, I, DB> { - let instructions_context = InstructionsContext::default(); - let to_capture_instructions = instructions_context.clone(); - - let mut evm = EvmBuilder::default() + let mut evm = reth_revm::EvmBuilder::default() .with_db(db) .with_external_context(inspector) - .append_handler_register_box(Box::new(move |handler| { - if let Some(ref mut table) = handler.instruction_table { - for boxed_instruction_with_opcode in eip3074::boxed_instructions(to_capture_instructions.clone()) { - table.insert_boxed( - boxed_instruction_with_opcode.opcode, - boxed_instruction_with_opcode.boxed_instruction, - ); - } - } - let post_execution_context = instructions_context.clone(); - handler.post_execution.end = Arc::new(move |_, outcome: _| { - // at the end of the transaction execution we clear the instructions - post_execution_context.clear(); - outcome - }); - })) + .append_handler_register_box(eip3074_handle_register()) .append_handler_register(inspector_handle_register) .build(); evm.modify_spec_id(env.spec_id()); evm.context.evm.env = env.env; evm } + + /// Returns new EVM with the given database and env. Similar to the implementation of [reth_evm::ConfigureEvmEnv] + /// but only keeping the necessary API. + pub(super) fn evm_with_env<'a, DB: Database + 'a>(db: DB, env: EnvWithHandlerCfg) -> Evm<'a, (), DB> { + let mut evm = + reth_revm::EvmBuilder::default().with_db(db).append_handler_register_box(eip3074_handle_register()).build(); + evm.modify_spec_id(env.spec_id()); + evm.context.evm.env = env.env; + evm + } +} + +fn eip3074_handle_register() -> HandleRegisterBox { + let instructions_context = InstructionsContext::default(); + let to_capture_instructions = instructions_context.clone(); + + Box::new(move |handler| { + if let Some(ref mut table) = handler.instruction_table { + for boxed_instruction_with_opcode in eip3074::boxed_instructions(to_capture_instructions.clone()) { + table.insert_boxed( + boxed_instruction_with_opcode.opcode, + boxed_instruction_with_opcode.boxed_instruction, + ); + } + } + let post_execution_context = instructions_context.clone(); + handler.post_execution.end = Arc::new(move |_, outcome: _| { + // at the end of the transaction execution we clear the instructions + post_execution_context.clear(); + outcome + }); + }) } diff --git a/src/tracing/mod.rs b/src/tracing/mod.rs index bc4432958..4ed931f66 100644 --- a/src/tracing/mod.rs +++ b/src/tracing/mod.rs @@ -5,9 +5,10 @@ mod database; use eyre::eyre; use reth_primitives::revm::env::tx_env_with_recovered; use reth_primitives::ruint::FromUintError; -use reth_revm::primitives::{Env, EnvWithHandlerCfg}; -use reth_revm::DatabaseCommit; -use reth_rpc_types::trace::geth::TraceResult; +use reth_primitives::B256; +use reth_revm::primitives::{Env, EnvWithHandlerCfg, ExecutionResult, ResultAndState}; +use reth_revm::{Database, DatabaseCommit}; +use reth_rpc_types::trace::geth::{GethTrace, TraceResult}; use reth_rpc_types::{ trace::{ geth::{GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingOptions}, @@ -17,7 +18,7 @@ use reth_rpc_types::{ }; use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; -use self::config::KakarotEvmConfig; +use self::config::EvmBuilder; use self::database::EthDatabaseSnapshot; use crate::eth_provider::{ error::{EthApiError, EthereumDataFormatError, TransactionError}, @@ -29,7 +30,6 @@ pub type TracerResult = Result; #[derive(Debug)] pub struct Tracer { transactions: Vec, - cfg: KakarotEvmConfig, env: EnvWithHandlerCfg, db: EthDatabaseSnapshot

, } @@ -41,8 +41,7 @@ impl Tracer

{ tracing_config: TracingInspectorConfig, ) -> TracerResult>> { let transact_to_parity_trace = - |cfg: KakarotEvmConfig, - env: EnvWithHandlerCfg, + |env: EnvWithHandlerCfg, db: &mut EthDatabaseSnapshot

, tx: &reth_rpc_types::Transaction| -> TracerResult<(Vec, reth_revm::primitives::State)> { @@ -55,10 +54,8 @@ impl Tracer

{ // Set up the inspector and transact the transaction let mut inspector = TracingInspector::new(tracing_config); - let mut evm = cfg.evm_with_env_and_inspector(db, env, &mut inspector); - let res = evm.transact().map_err(|err| TransactionError::Tracing(err.into()))?; - // we drop the evm to avoid cloning the inspector - drop(evm); + let evm = EvmBuilder::evm_with_env_and_inspector(db, env, &mut inspector); + let res = transact_in_place(evm)?; let parity_builder = inspector.into_parity_builder(); @@ -68,108 +65,168 @@ impl Tracer

{ Ok((parity_builder.into_localized_transaction_traces(transaction_info), res.state)) }; - let traces = self.trace_block_in_place(transact_to_parity_trace)?; + let txs = self.transactions.clone(); + let traces = self.trace_transactions(transact_to_parity_trace, txs)?; Ok(Some(traces)) } /// Returns the debug trace in the Geth. /// Currently only supports the call tracer or the default tracer. - pub fn debug_block(self, opts: GethDebugTracingOptions) -> TracerResult>> { - let transact_to_geth_trace = |cfg: KakarotEvmConfig, - env: EnvWithHandlerCfg, - db: &mut EthDatabaseSnapshot

, - tx: &reth_rpc_types::Transaction| - -> TracerResult<(Vec, reth_revm::primitives::State)> { - let GethDebugTracingOptions { tracer_config, config, tracer, .. } = opts.clone(); - - if let Some(tracer) = tracer { - return match tracer { - GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::CallTracer) => { - let call_config = tracer_config - .clone() - .into_call_config() - .map_err(|err| EthApiError::Transaction(TransactionError::Tracing(err.into())))?; - let mut inspector = - TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config)); - let mut evm = cfg.evm_with_env_and_inspector(db, env, &mut inspector); - - let res = evm.transact().map_err(|err| TransactionError::Tracing(err.into()))?; - // we drop the evm to avoid cloning the inspector - drop(evm); - let call_frame = inspector.into_geth_builder().geth_call_traces( - tracer_config.into_call_config().map_err(|err| TransactionError::Tracing(err.into()))?, - res.result.gas_used(), - ); - Ok(( - vec![TraceResult::Success { result: call_frame.into(), tx_hash: Some(tx.hash) }], - res.state, - )) - } - _ => Err(EthApiError::Transaction(TransactionError::Tracing( - eyre!("only call tracer is currently supported").into(), - ))), - }; - } + pub fn debug_block(self, opts: GethDebugTracingOptions) -> TracerResult> { + let transact_to_geth_trace = transact_and_get_traces_geth(opts); + let txs = self.transactions.clone(); + let traces = self.trace_transactions(transact_to_geth_trace, txs)?; - // default tracer - let mut inspector = TracingInspector::new(TracingInspectorConfig::from_geth_config(&config)); - let mut evm = cfg.evm_with_env_and_inspector(db, env, &mut inspector); + Ok(traces) + } - let res = evm.transact().map_err(|err| TransactionError::Tracing(err.into()))?; - // we drop the evm to avoid cloning the inspector - drop(evm); - let gas_used = res.result.gas_used(); - let return_value = res.result.into_output().unwrap_or_default(); - let frame = inspector.into_geth_builder().geth_traces(gas_used, return_value, config); - Ok((vec![TraceResult::Success { result: frame.into(), tx_hash: Some(tx.hash) }], res.state)) - }; + pub fn debug_transaction( + mut self, + transaction_hash: B256, + opts: GethDebugTracingOptions, + ) -> TracerResult { + for tx in self.transactions.clone() { + if tx.hash == transaction_hash { + let transact_and_get_traces = transact_and_get_traces_geth::

(opts); + // We only want to trace the transaction with the given hash. + let trace = self + .trace_transactions(transact_and_get_traces, vec![tx])? + .first() + .cloned() + .ok_or(TransactionError::Tracing(eyre!("No trace found").into()))?; + return match trace { + TraceResult::Success { result, .. } => Ok(result), + TraceResult::Error { error, .. } => Err(TransactionError::Tracing(error.into()).into()), + }; + } - let traces = self.trace_block_in_place(transact_to_geth_trace)?; + let env = env_with_tx(self.env.clone(), tx.clone())?; + let evm = EvmBuilder::evm_with_env(&mut self.db, env); + transact_commit_in_place(evm)?; + } - Ok(Some(traces)) + Err(EthApiError::TransactionNotFound) } - /// Traces a block using tokio::task::block_in_place. This is needed in order to enter a blocking context - /// which is then converted to a async context in the implementation of [Database] using - /// `Handle::current().block_on(async { ... })` - /// The function `transact_and_get_traces` closure uses the `cfg`, `env` and `db` to create an evm + /// Traces the provided transactions using the given closure. + /// The function `transact_and_get_traces` closure uses the `env` and `db` to create an evm /// which is then used to transact and trace the transaction. - fn trace_block_in_place(self, transact_and_get_traces: F) -> TracerResult> + fn trace_transactions( + self, + transact_and_get_traces: F, + transactions: Vec, + ) -> TracerResult> where F: Fn( - KakarotEvmConfig, EnvWithHandlerCfg, &mut EthDatabaseSnapshot

, &reth_rpc_types::Transaction, ) -> TracerResult<(Vec, reth_revm::primitives::State)>, { - tokio::task::block_in_place(move || { - let mut traces = Vec::with_capacity(self.transactions.len()); - let mut transactions = self.transactions.iter().peekable(); - let mut db = self.db; - - while let Some(tx) = transactions.next() { - // Convert the transaction to an ec recovered transaction and update the env with it. - let tx_ec_recovered = - tx.clone().try_into().map_err(|_| EthereumDataFormatError::TransactionConversionError)?; - - let tx_env = tx_env_with_recovered(&tx_ec_recovered); - let env = EnvWithHandlerCfg { - env: Env::boxed(self.env.env.cfg.clone(), self.env.env.block.clone(), tx_env), - handler_cfg: self.env.handler_cfg, - }; + let mut traces = Vec::with_capacity(self.transactions.len()); + let mut transactions = transactions.iter().peekable(); + let mut db = self.db; - let (res, state_changes) = transact_and_get_traces(self.cfg.clone(), env, &mut db, tx)?; - traces.extend(res); + while let Some(tx) = transactions.next() { + let env = env_with_tx(self.env.clone(), tx.clone())?; - // Only commit to the database if there are more transactions to process. - if transactions.peek().is_some() { - db.commit(state_changes); - } + let (res, state_changes) = transact_and_get_traces(env, &mut db, tx)?; + traces.extend(res); + + // Only commit to the database if there are more transactions to process. + if transactions.peek().is_some() { + db.commit(state_changes); } + } + + TracerResult::Ok(traces) + } +} + +/// Returns a closure that transacts and gets the geth traces for the given transaction. Captures the +/// `opts` in order to use it in the closure. +fn transact_and_get_traces_geth( + opts: GethDebugTracingOptions, +) -> impl Fn( + EnvWithHandlerCfg, + &mut EthDatabaseSnapshot

, + &reth_rpc_types::Transaction, +) -> TracerResult<(Vec, reth_revm::primitives::State)> { + move |env: EnvWithHandlerCfg, + db: &mut EthDatabaseSnapshot

, + tx: &reth_rpc_types::Transaction| + -> TracerResult<(Vec, reth_revm::primitives::State)> { + let GethDebugTracingOptions { tracer_config, config, tracer, .. } = opts.clone(); + + if let Some(tracer) = tracer { + return match tracer { + GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::CallTracer) => { + let call_config = tracer_config + .clone() + .into_call_config() + .map_err(|err| EthApiError::Transaction(TransactionError::Tracing(err.into())))?; + let mut inspector = + TracingInspector::new(TracingInspectorConfig::from_geth_call_config(&call_config)); + let evm = EvmBuilder::evm_with_env_and_inspector(db, env, &mut inspector); + + let res = transact_in_place(evm)?; + let call_frame = inspector.into_geth_builder().geth_call_traces( + tracer_config.into_call_config().map_err(|err| TransactionError::Tracing(err.into()))?, + res.result.gas_used(), + ); + Ok((vec![TraceResult::Success { result: call_frame.into(), tx_hash: Some(tx.hash) }], res.state)) + } + _ => Err(EthApiError::Transaction(TransactionError::Tracing( + eyre!("only call tracer is currently supported").into(), + ))), + }; + } + + // default tracer + let mut inspector = TracingInspector::new(TracingInspectorConfig::from_geth_config(&config)); + let evm = EvmBuilder::evm_with_env_and_inspector(db, env, &mut inspector); - TracerResult::Ok(traces) - }) + let res = transact_in_place(evm)?; + let gas_used = res.result.gas_used(); + let return_value = res.result.into_output().unwrap_or_default(); + let frame = inspector.into_geth_builder().geth_traces(gas_used, return_value, config); + Ok((vec![TraceResult::Success { result: frame.into(), tx_hash: Some(tx.hash) }], res.state)) } } + +/// Returns the environment with the transaction env updated to the given transaction. +fn env_with_tx(env: EnvWithHandlerCfg, tx: reth_rpc_types::Transaction) -> TracerResult { + // Convert the transaction to an ec recovered transaction and update the env with it. + let tx_ec_recovered = tx.try_into().map_err(|_| EthereumDataFormatError::TransactionConversionError)?; + + let tx_env = tx_env_with_recovered(&tx_ec_recovered); + Ok(EnvWithHandlerCfg { + env: Env::boxed(env.env.cfg.clone(), env.env.block.clone(), tx_env), + handler_cfg: env.handler_cfg, + }) +} + +/// Runs the evm.transact_commit() in a blocking context using tokio::task::block_in_place. +/// This is needed in order to enter a blocking context which is then converted to a async +/// context in the implementation of [Database] using `Handle::current().block_on(async { ... })` +/// ⚠️ evm.transact() should NOT be used as is and we should always make use of the `transact_in_place` function +fn transact_in_place(mut evm: reth_revm::Evm<'_, I, DB>) -> TracerResult +where + ::Error: std::error::Error + Sync + Send + 'static, +{ + tokio::task::block_in_place(|| evm.transact().map_err(|err| TransactionError::Tracing(err.into()).into())) +} + +/// Runs the evm.transact_commit() in a blocking context using tokio::task::block_in_place. +/// This is needed in order to enter a blocking context which is then converted to a async +/// context in the implementation of [Database] using `Handle::current().block_on(async { ... })` +/// ⚠️ evm.transact_commit() should NOT be used as is and we should always make use of the `transaction_commit_in_place` function +fn transact_commit_in_place( + mut evm: reth_revm::Evm<'_, I, DB>, +) -> TracerResult +where + ::Error: std::error::Error + Sync + Send + 'static, +{ + tokio::task::block_in_place(|| evm.transact_commit().map_err(|err| TransactionError::Tracing(err.into()).into())) +} diff --git a/tests/tests/trace_api.rs b/tests/tests/trace_api.rs index faa56443e..1a2cfdf70 100644 --- a/tests/tests/trace_api.rs +++ b/tests/tests/trace_api.rs @@ -10,7 +10,7 @@ use kakarot_rpc::test_utils::katana::Katana; use kakarot_rpc::test_utils::rpc::start_kakarot_rpc_server; use kakarot_rpc::test_utils::rpc::RawRpcParamsBuilder; use reth_primitives::{Address, B256, U256}; -use reth_rpc_types::trace::geth::TraceResult; +use reth_rpc_types::trace::geth::{GethTrace, TraceResult}; use reth_rpc_types::trace::parity::LocalizedTransactionTrace; use rstest::*; use serde_json::{json, Value}; @@ -142,22 +142,10 @@ async fn test_trace_block(#[future] plain_opcodes: (Katana, KakarotEvmContract), drop(server_handle); } -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_debug_trace_block_by_number(#[future] plain_opcodes: (Katana, KakarotEvmContract), _setup: ()) { - // Setup the Kakarot RPC server. - let katana = plain_opcodes.0; - let plain_opcodes = plain_opcodes.1; - tracing(&katana, &plain_opcodes, "createCounterAndInvoke", Box::new(|_| ())).await; - - let (server_addr, server_handle) = - start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server"); - - // Send the trace_block RPC request. +async fn trace_block_by_number(port: u16) -> Vec { let reqwest_client = reqwest::Client::new(); let res = reqwest_client - .post(format!("http://localhost:{}", server_addr.port())) + .post(format!("http://localhost:{}", port)) .header("Content-Type", "application/json") .body( RawRpcParamsBuilder::new("debug_traceBlockByNumber") @@ -176,12 +164,27 @@ async fn test_debug_trace_block_by_number(#[future] plain_opcodes: (Katana, Kaka .expect("Failed to call Debug RPC"); let response = res.text().await.expect("Failed to get response body"); let raw: Value = serde_json::from_str(&response).expect("Failed to deserialize response body"); - let traces: Option> = - serde_json::from_value(raw["result"].clone()).expect("Failed to deserialize result"); - assert!(traces.is_some()); + serde_json::from_value(raw["result"].clone()).expect("Failed to deserialize result") +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_debug_trace_block_by_number(#[future] plain_opcodes: (Katana, KakarotEvmContract), _setup: ()) { + // Setup the Kakarot RPC server. + let katana = plain_opcodes.0; + let plain_opcodes = plain_opcodes.1; + tracing(&katana, &plain_opcodes, "createCounterAndInvoke", Box::new(|_| ())).await; + + let (server_addr, server_handle) = + start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server"); + + // Send the trace_block RPC request. + let traces = trace_block_by_number(server_addr.port()).await; + // We expect 1 trace per transaction given the formatting of the debug_traceBlockByNumber response. - assert!(traces.unwrap().len() == TRACING_TRANSACTIONS_COUNT); + assert!(traces.len() == TRACING_TRANSACTIONS_COUNT); drop(server_handle); } @@ -261,3 +264,64 @@ async fn test_trace_eip3074(#[future] eip_3074_invoker: (Katana, KakarotEvmContr assert!(traces.unwrap().len() == 2 * TRACING_TRANSACTIONS_COUNT); drop(server_handle); } + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_debug_trace_transaction(#[future] plain_opcodes: (Katana, KakarotEvmContract), _setup: ()) { + // Setup the Kakarot RPC server. + let katana = plain_opcodes.0; + let plain_opcodes = plain_opcodes.1; + tracing(&katana, &plain_opcodes, "createCounterAndInvoke", Box::new(|_| ())).await; + + let (server_addr, server_handle) = + start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server"); + + // Get the block in order to trace a transaction. + let block = katana + .eth_provider() + .block_by_number(TRACING_BLOCK_NUMBER.into(), false) + .await + .expect("Failed to get block") + .unwrap(); + let index = TRACING_TRANSACTIONS_COUNT - 2; + let tx_hash = block.transactions.as_hashes().unwrap().get(index).unwrap(); + + // Send the trace_block RPC request. + let reqwest_client = reqwest::Client::new(); + let res = reqwest_client + .post(format!("http://localhost:{}", server_addr.port())) + .header("Content-Type", "application/json") + .body( + RawRpcParamsBuilder::new("debug_traceTransaction") + .add_param(format!("0x{:016x}", tx_hash)) + .add_param(json!({ + "tracer": "callTracer", + "tracerConfig": { + "onlyTopCall": false + }, + "timeout": "300s" + })) + .build(), + ) + .send() + .await + .expect("Failed to call Debug RPC"); + let response = res.text().await.expect("Failed to get response body"); + let raw: Value = serde_json::from_str(&response).expect("Failed to deserialize response body"); + let trace: GethTrace = serde_json::from_value(raw["result"].clone()).expect("Failed to deserialize result"); + + // Get the traces for the block + let traces = trace_block_by_number(server_addr.port()).await; + let expected_trace = + if let reth_rpc_types::trace::geth::TraceResult::Success { result, .. } = traces.get(index).cloned().unwrap() { + result + } else { + panic!("Failed to get expected trace") + }; + + // Compare traces + assert_eq!(expected_trace, trace); + + drop(server_handle); +}