Skip to main content

reth_rpc_eth_api/helpers/
trace.rs

1//! Loads a pending block from database. Helper trait for `eth_` call and trace RPC methods.
2
3use super::{Call, LoadBlock, LoadState, LoadTransaction};
4use crate::{FromEthApiError, FromEvmError};
5use alloy_consensus::{transaction::TxHashRef, BlockHeader};
6use alloy_eip7928::{bal::DecodedBal, BlockAccessIndex};
7use alloy_primitives::B256;
8use alloy_rpc_types_eth::{BlockId, TransactionInfo};
9use evm2::evm::Bal as EvmBal;
10use evm2_inspectors::tracing::{TracingInspector, TracingInspectorConfig};
11use futures::Future;
12use reth_errors::RethError;
13use reth_evm::{
14    execute::{BlockExecutor, BlockTransactionResult},
15    ConfigureEvm, Database, Evm, EvmEnvFor, EvmTypesFor, TxEnvFor, TxResultWithStateFor,
16};
17use reth_primitives_traits::{BlockBody, BlockTy, RecoveredBlock};
18use reth_rpc_eth_types::{cache::db::attach_bal_before_tx, EthApiError, StateCacheDb};
19use reth_storage_api::ProviderBlock;
20use reth_tasks::cancel::is_cancelled;
21use std::sync::Arc;
22
23/// Context passed to per-transaction trace callbacks.
24pub struct TracingCtx<'a, Insp, EvmTypes: evm2::EvmTypes> {
25    /// Execution result and detached state changes for the transaction.
26    pub result: &'a evm2::TxResultWithState<EvmTypes>,
27    /// Database pointing to the transaction pre-state.
28    pub db: &'a mut dyn evm2::evm::DynDatabase,
29    /// Inspector used while executing the transaction.
30    pub inspector: Insp,
31}
32
33impl<Insp, EvmTypes: evm2::EvmTypes> core::fmt::Debug for TracingCtx<'_, Insp, EvmTypes> {
34    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35        f.debug_struct("TracingCtx").finish_non_exhaustive()
36    }
37}
38
39impl<Insp, EvmTypes: evm2::EvmTypes> TracingCtx<'_, Insp, EvmTypes> {
40    /// Consumes this context and returns the inspector.
41    pub fn take_inspector(self) -> Insp {
42        self.inspector
43    }
44}
45
46/// Executes CPU heavy trace tasks.
47pub trait Trace: LoadState<Error: FromEvmError<Self::Evm>> + Call {
48    /// Executes a transaction with the provided inspector without committing state changes.
49    fn inspect<DB, I>(
50        &self,
51        db: DB,
52        evm_env: EvmEnvFor<Self::Evm>,
53        tx_env: &TxEnvFor<Self::Evm>,
54        inspector: I,
55    ) -> Result<(I, TxResultWithStateFor<Self::Evm>), Self::Error>
56    where
57        DB: Database,
58        I: evm2::Inspector<EvmTypesFor<Self::Evm>> + 'static,
59    {
60        let mut evm = self.evm_config().evm_with_env(db, evm_env);
61        evm.transact_with_inspector(tx_env, inspector).map_err(Self::Error::from_evm_err)
62    }
63
64    /// Executes the transaction on top of the given [`BlockId`] with a tracer configured by the
65    /// config and provides the used database to the callback.
66    fn spawn_trace_at_with_state<F, R>(
67        &self,
68        evm_env: EvmEnvFor<Self::Evm>,
69        tx_env: TxEnvFor<Self::Evm>,
70        config: TracingInspectorConfig,
71        at: BlockId,
72        f: F,
73    ) -> impl Future<Output = Result<R, Self::Error>> + Send
74    where
75        F: FnOnce(
76                TracingInspector,
77                TxResultWithStateFor<Self::Evm>,
78                StateCacheDb,
79            ) -> Result<R, Self::Error>
80            + Send
81            + 'static,
82        R: Send + 'static,
83    {
84        self.spawn_with_state_at_block(at, move |this, mut db| {
85            let (inspector, result) =
86                this.inspect(&mut db, evm_env, &tx_env, TracingInspector::new(config))?;
87            f(inspector, result, db)
88        })
89    }
90
91    /// Retrieves the transaction if it exists and returns its trace.
92    ///
93    /// Before the transaction is traced, the state is positioned right before the transaction,
94    /// either by attaching the block's cached BAL or by executing all previous transactions in
95    /// the block.
96    /// The callback `f` is invoked with the [`TxResultWithStateFor`] after the transaction was
97    /// executed and the database that points to the beginning of the transaction. The database may
98    /// have the block's BAL attached and must only be used for reads, because an attached BAL takes
99    /// read precedence over state committed on top, see [`attach_bal_before_tx`].
100    ///
101    /// Note: Implementers should use a threadpool where blocking is allowed, such as
102    /// [`BlockingTaskPool`](reth_tasks::pool::BlockingTaskPool).
103    fn spawn_trace_transaction_in_block<F, R>(
104        &self,
105        hash: B256,
106        config: TracingInspectorConfig,
107        f: F,
108    ) -> impl Future<Output = Result<Option<R>, Self::Error>> + Send
109    where
110        Self: LoadTransaction,
111        F: FnOnce(
112                TransactionInfo,
113                TracingInspector,
114                TxResultWithStateFor<Self::Evm>,
115                StateCacheDb,
116            ) -> Result<R, Self::Error>
117            + Send
118            + 'static,
119        R: Send + 'static,
120    {
121        self.spawn_trace_transaction_in_block_with_inspector(hash, TracingInspector::new(config), f)
122    }
123
124    /// Retrieves the transaction if it exists and returns its trace.
125    ///
126    /// Before the transaction is traced, the state is positioned right before the transaction,
127    /// either by attaching the block's cached BAL or by executing all previous transactions in
128    /// the block.
129    /// The callback `f` is invoked with the [`TxResultWithStateFor`] after the transaction was
130    /// executed and the database that points to the beginning of the transaction. The database may
131    /// have the block's BAL attached and must only be used for reads, because an attached BAL takes
132    /// read precedence over state committed on top, see [`attach_bal_before_tx`].
133    ///
134    /// Note: Implementers should use a threadpool where blocking is allowed, such as
135    /// [`BlockingTaskPool`](reth_tasks::pool::BlockingTaskPool).
136    fn spawn_trace_transaction_in_block_with_inspector<Insp, F, R>(
137        &self,
138        hash: B256,
139        inspector: Insp,
140        f: F,
141    ) -> impl Future<Output = Result<Option<R>, Self::Error>> + Send
142    where
143        Self: LoadTransaction,
144        F: FnOnce(
145                TransactionInfo,
146                Insp,
147                TxResultWithStateFor<Self::Evm>,
148                StateCacheDb,
149            ) -> Result<R, Self::Error>
150            + Send
151            + 'static,
152        Insp: evm2::Inspector<EvmTypesFor<Self::Evm>> + Send + 'static,
153        R: Send + 'static,
154    {
155        async move {
156            let Some((transaction, block, bal)) =
157                self.transaction_and_block_and_maybe_bal(hash).await?
158            else {
159                return Ok(None);
160            };
161            let (_, tx_info) = transaction.split();
162
163            // we need to get the state of the parent block because we're essentially replaying the
164            // block the transaction is included in
165            self.spawn_with_state_at_block(block.parent_hash(), move |this, mut db| {
166                let (inspector, result, _) = this.inspect_transaction_in_block(
167                    &block,
168                    &mut db,
169                    inspector,
170                    // index should always be available because `transaction_and_block` only
171                    // returns transactions included in a block
172                    tx_info.index.expect("transaction_and_block only returns block transactions")
173                        as usize,
174                    bal.as_deref(),
175                )?;
176                f(tx_info, inspector, result, db)
177            })
178            .await
179            .map(Some)
180        }
181    }
182
183    /// Positions the state of `db` right before the transaction at the target index.
184    ///
185    /// If the block's cached BAL is given, it is attached to the database at the target index and
186    /// no transactions are executed, see [`attach_bal_before_tx`]. Otherwise all transactions
187    /// before the target transaction are executed through the block executor, see
188    /// [`Call::block_executor`], and their changes are written to the _runtime_ db
189    /// ([`StateCacheDb`]).
190    ///
191    /// If the target index is greater than or equal to the block's transaction count, all
192    /// transactions are replayed.
193    fn replay_block_until(
194        &self,
195        db: &mut StateCacheDb,
196        block: &RecoveredBlock<BlockTy<Self::Primitives>>,
197        target_tx_index: usize,
198        bal: Option<&DecodedBal<Arc<EvmBal>>>,
199    ) -> Result<(), Self::Error> {
200        if let Some(bal) = bal {
201            attach_bal_before_tx(db, bal, target_tx_index);
202            return Ok(())
203        }
204
205        self.apply_pre_execution_changes(block, db)?;
206
207        let evm_env = self.evm_env_for_header(block.sealed_block().sealed_header())?;
208        let mut executor = self.block_executor(block, db, evm_env)?;
209        self.replay_transactions_until_with_executor(
210            &mut executor,
211            block.transactions_recovered(),
212            target_tx_index,
213        )?;
214        let cache = executor.evm_mut().take_state_cache();
215        drop(executor);
216        db.cache.merge(cache);
217        Ok(())
218    }
219
220    /// Executes the block's transaction at the target index with the configured inspector on the
221    /// state right before the transaction.
222    ///
223    /// The transaction is executed through the block executor, see [`Call::block_executor`]. If
224    /// the block's cached BAL is given, the state is positioned by attaching the BAL at the target
225    /// index, see [`attach_bal_before_tx`], and the executor is told the transaction's position
226    /// with [`BlockExecutor::set_block_access_index`]. Otherwise all transactions before the
227    /// target transaction are replayed through the executor without inspection first.
228    ///
229    /// Returns [`EthApiError::InvalidParams`] if the block has no transaction at that index.
230    #[expect(clippy::type_complexity)]
231    fn inspect_transaction_in_block<I>(
232        &self,
233        block: &RecoveredBlock<BlockTy<Self::Primitives>>,
234        db: &mut StateCacheDb,
235        inspector: I,
236        target_tx_index: usize,
237        bal: Option<&DecodedBal<Arc<EvmBal>>>,
238    ) -> Result<(I, TxResultWithStateFor<Self::Evm>, EvmEnvFor<Self::Evm>), Self::Error>
239    where
240        I: evm2::Inspector<EvmTypesFor<Self::Evm>> + 'static,
241    {
242        let Some(target_tx) = block.transactions_recovered().nth(target_tx_index) else {
243            return Err(EthApiError::InvalidParams(format!(
244                "tx_index {target_tx_index} out of bounds for block with {} transactions",
245                block.transaction_count()
246            ))
247            .into())
248        };
249
250        if let Some(bal) = bal {
251            // the BAL also covers the block's pre-execution changes
252            attach_bal_before_tx(db, bal, target_tx_index);
253        } else {
254            self.apply_pre_execution_changes(block, db)?;
255        }
256
257        let evm_env = self.evm_env_for_header(block.sealed_block().sealed_header())?;
258        let mut executor = self.block_executor(block, db, evm_env.clone())?;
259
260        if bal.is_some() {
261            executor
262                .set_block_access_index(BlockAccessIndex::from_tx_index(target_tx_index as u64));
263        } else {
264            self.replay_transactions_until_with_executor(
265                &mut executor,
266                block.transactions_recovered(),
267                target_tx_index,
268            )?;
269        }
270        let (inspector, result) = executor
271            .inspect_transaction_without_commit(target_tx, inspector)
272            .map(|(inspector, output)| (inspector, output.into_result()))
273            .map_err(Self::Error::from_evm_err)?;
274        let cache = executor.evm_mut().take_state_cache();
275        drop(executor);
276        db.cache.merge(cache);
277        Ok((inspector, result, evm_env))
278    }
279
280    /// Executes all transactions of a block up to a given index.
281    ///
282    /// If a `highest_index` is given, this will only execute the first `highest_index`
283    /// transactions, in other words, it will stop executing transactions after the
284    /// `highest_index`th transaction. If `highest_index` is `None`, all transactions
285    /// are executed.
286    fn trace_block_until<F, R>(
287        &self,
288        block_id: BlockId,
289        block: Option<Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>>,
290        highest_index: Option<u64>,
291        config: TracingInspectorConfig,
292        f: F,
293    ) -> impl Future<Output = Result<Option<Vec<R>>, Self::Error>> + Send
294    where
295        Self: LoadBlock,
296        F: Fn(
297                TransactionInfo,
298                TracingCtx<'_, TracingInspector, EvmTypesFor<Self::Evm>>,
299            ) -> Result<R, Self::Error>
300            + Send
301            + 'static,
302        R: Send + 'static,
303    {
304        self.trace_block_until_with_inspector(
305            block_id,
306            block,
307            highest_index,
308            move || TracingInspector::new(config),
309            f,
310        )
311    }
312
313    /// Executes all transactions of a block.
314    ///
315    /// If a `highest_index` is given, this will only execute the first `highest_index`
316    /// transactions, in other words, it will stop executing transactions after the
317    /// `highest_index`th transaction.
318    ///
319    /// Note: This expect tx index to be 0-indexed, so the first transaction is at index 0.
320    ///
321    /// This accepts a `inspector_setup` closure that returns the inspector to be used for tracing
322    /// the transactions.
323    fn trace_block_until_with_inspector<Setup, Insp, F, R>(
324        &self,
325        block_id: BlockId,
326        block: Option<Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>>,
327        highest_index: Option<u64>,
328        mut inspector_setup: Setup,
329        f: F,
330    ) -> impl Future<Output = Result<Option<Vec<R>>, Self::Error>> + Send
331    where
332        Self: LoadBlock,
333        F: Fn(
334                TransactionInfo,
335                TracingCtx<'_, Insp, EvmTypesFor<Self::Evm>>,
336            ) -> Result<R, Self::Error>
337            + Send
338            + 'static,
339        Setup: FnMut() -> Insp + Send + 'static,
340        Insp: evm2::Inspector<EvmTypesFor<Self::Evm>> + 'static,
341        R: Send + 'static,
342    {
343        async move {
344            let block =
345                if block.is_some() { block } else { self.recovered_block(block_id).await? };
346
347            let Some(block) = block else { return Ok(None) };
348            let evm_env = self.evm_env_for_header(block.sealed_block().sealed_header())?;
349
350            if block.body().transactions().is_empty() {
351                // nothing to trace
352                return Ok(Some(Vec::new()))
353            }
354
355            // replay all transactions of the block
356            // we need to get the state of the parent block because we're replaying this block
357            // on top of its parent block's state
358            self.spawn_with_state_at_block(block.parent_hash(), move |this, mut db| {
359                let block_hash = block.hash();
360
361                let block_number = block.number();
362                let block_timestamp = block.timestamp();
363                let base_fee = block.base_fee_per_gas();
364
365                this.apply_pre_execution_changes(&block, &mut db)?;
366
367                // prepare transactions, we do everything upfront to reduce time spent with open
368                // state
369                let max_transactions = highest_index.map_or_else(
370                    || block.body().transaction_count(),
371                    |highest| {
372                        // we need + 1 because the index is 0-based
373                        highest as usize + 1
374                    },
375                );
376                let tx_count = max_transactions.min(block.body().transaction_count());
377
378                let mut results = Vec::new();
379                let mut executor = this.block_executor(&block, &mut db, evm_env)?;
380
381                for (idx, tx) in block.transactions_recovered().take(max_transactions).enumerate() {
382                    if is_cancelled() {
383                        return Err(EthApiError::InternalEthError.into())
384                    }
385                    let tx_hash = *tx.tx_hash();
386                    let (inspector, output) = executor
387                        .inspect_transaction_without_commit(tx, inspector_setup())
388                        .map_err(Self::Error::from_evm_err)?;
389                    let tx_info = TransactionInfo {
390                        hash: Some(tx_hash),
391                        index: Some(idx as u64),
392                        block_hash: Some(block_hash),
393                        block_number: Some(block_number),
394                        block_timestamp: Some(block_timestamp),
395                        base_fee,
396                    };
397                    let item = f(
398                        tx_info,
399                        TracingCtx {
400                            result: output.result(),
401                            db: executor.evm_mut().state_db_mut(),
402                            inspector,
403                        },
404                    )?;
405                    // the executor is dropped after the last transaction, so its state needs no
406                    // commit
407                    if idx + 1 < tx_count {
408                        executor.commit_transaction(output).map_err(Self::Error::from_evm_err)?;
409                    }
410                    results.push(item);
411                }
412
413                Ok(Some(results))
414            })
415            .await
416        }
417    }
418
419    /// Executes all transactions of a block and returns a list of callback results invoked for each
420    /// transaction in the block.
421    ///
422    /// This
423    /// 1. fetches all transactions of the block
424    /// 2. configures the EVM env
425    /// 3. loops over all transactions and executes them
426    /// 4. calls the callback with the transaction info, the execution result, the changed state
427    ///    _after_ the transaction [`StateCacheDb`] and the database that points to the state right
428    ///    _before_ the transaction.
429    fn trace_block_with<F, R>(
430        &self,
431        block_id: BlockId,
432        block: Option<Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>>,
433        config: TracingInspectorConfig,
434        f: F,
435    ) -> impl Future<Output = Result<Option<Vec<R>>, Self::Error>> + Send
436    where
437        Self: LoadBlock,
438        F: Fn(
439                TransactionInfo,
440                TracingCtx<'_, TracingInspector, EvmTypesFor<Self::Evm>>,
441            ) -> Result<R, Self::Error>
442            + Send
443            + 'static,
444        R: Send + 'static,
445    {
446        self.trace_block_until(block_id, block, None, config, f)
447    }
448
449    /// Executes all transactions of a block and returns a list of callback results invoked for each
450    /// transaction in the block.
451    ///
452    /// This
453    /// 1. fetches all transactions of the block
454    /// 2. configures the EVM env
455    /// 3. loops over all transactions and executes them
456    /// 4. calls the callback with the transaction info, the execution result, the changed state
457    ///    _after_ the transaction `EvmState` and the database that points to the state right
458    ///    _before_ the transaction, in other words the state the transaction was executed on:
459    ///    `changed_state = tx(cached_state)`
460    ///
461    /// This accepts a `inspector_setup` closure that returns the inspector to be used for tracing
462    /// a transaction. This is invoked for each transaction.
463    fn trace_block_inspector<Setup, Insp, F, R>(
464        &self,
465        block_id: BlockId,
466        block: Option<Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>>,
467        insp_setup: Setup,
468        f: F,
469    ) -> impl Future<Output = Result<Option<Vec<R>>, Self::Error>> + Send
470    where
471        Self: LoadBlock,
472        F: Fn(
473                TransactionInfo,
474                TracingCtx<'_, Insp, EvmTypesFor<Self::Evm>>,
475            ) -> Result<R, Self::Error>
476            + Send
477            + 'static,
478        Setup: FnMut() -> Insp + Send + 'static,
479        Insp: evm2::Inspector<EvmTypesFor<Self::Evm>> + 'static,
480        R: Send + 'static,
481    {
482        self.trace_block_until_with_inspector(block_id, block, None, insp_setup, f)
483    }
484
485    /// Applies chain-specific state transitions required before executing a block.
486    ///
487    /// Note: This should only be called when tracing an entire block vs individual transactions.
488    /// When tracing transactions on top of an already committed block state, those transitions are
489    /// already applied.
490    fn apply_pre_execution_changes(
491        &self,
492        block: &RecoveredBlock<ProviderBlock<Self::Provider>>,
493        db: &mut StateCacheDb,
494    ) -> Result<reth_trie_common::bundle_state::BundleState, Self::Error> {
495        let evm_env = self.evm_env_for_header(block.sealed_block().sealed_header())?;
496        let ctx = self
497            .evm_config()
498            .context_for_block(block.sealed_block())
499            .map_err(RethError::other)
500            .map_err(Self::Error::from_eth_err)?;
501        let changes = self
502            .evm_config()
503            .pre_block_state_changes(&mut *db, evm_env, ctx)
504            .map_err(|err| EthApiError::EvmCustom(err.to_string()))
505            .map_err(Self::Error::from_eth_err)?;
506        db.commit_source(&reth_execution_types::BundleSource(&changes));
507        Ok(changes)
508    }
509}