Skip to main content

ethrex_blockchain/
tracing.rs

1use std::{
2    sync::{Arc, Mutex},
3    time::Duration,
4};
5
6use ethrex_common::{
7    H256,
8    tracing::{CallTrace, OpcodeTraceResult, PrestateResult},
9    types::Block,
10};
11use ethrex_crypto::NativeCrypto;
12use ethrex_storage::Store;
13use ethrex_vm::tracing::OpcodeTracerConfig;
14use ethrex_vm::{Evm, EvmError};
15
16use crate::{Blockchain, error::ChainError, vm::StoreVmDatabase};
17
18impl Blockchain {
19    /// Outputs the call trace for the given transaction
20    /// May need to re-execute blocks in order to rebuild the transaction's prestate, up to the amount given by `reexec`
21    pub async fn trace_transaction_calls(
22        &self,
23        tx_hash: H256,
24        reexec: u32,
25        timeout: Duration,
26        only_top_call: bool,
27        with_log: bool,
28    ) -> Result<CallTrace, ChainError> {
29        // Fetch the transaction's location and the block it is contained in
30        let Some((_, block_hash, tx_index)) =
31            self.storage.get_transaction_location(tx_hash).await?
32        else {
33            return Err(ChainError::Custom("Transaction not Found".to_string()));
34        };
35        let tx_index = tx_index as usize;
36        let Some(block) = self.storage.get_block_by_hash(block_hash).await? else {
37            return Err(ChainError::Custom("Block not Found".to_string()));
38        };
39        // Obtain the block's parent state
40        let mut vm = self
41            .rebuild_parent_state(block.header.parent_hash, reexec)
42            .await?;
43        // Run the block until the transaction we want to trace
44        vm.rerun_block(&block, Some(tx_index))?;
45        // Trace the transaction
46        timeout_trace_operation(timeout, move || {
47            vm.trace_tx_calls(&block, tx_index, only_top_call, with_log)
48        })
49        .await
50    }
51
52    /// Outputs the call trace for each transaction in the block along with the transaction's hash
53    /// May need to re-execute blocks in order to rebuild the transaction's prestate, up to the amount given by `reexec`
54    /// Returns transaction call traces from oldest to newest
55    pub async fn trace_block_calls(
56        &self,
57        // We receive the block instead of its hash/number to support multiple potential endpoints
58        block: Block,
59        reexec: u32,
60        timeout: Duration,
61        only_top_call: bool,
62        with_log: bool,
63    ) -> Result<Vec<(H256, CallTrace)>, ChainError> {
64        // Obtain the block's parent state
65        let mut vm = self
66            .rebuild_parent_state(block.header.parent_hash, reexec)
67            .await?;
68        // Run anything necessary before executing the block's transactions (system calls, etc)
69        vm.rerun_block(&block, Some(0))?;
70        // Trace each transaction
71        // We need to do this in order to pass ownership of block & evm to a blocking process without cloning
72        let vm = Arc::new(Mutex::new(vm));
73        let block = Arc::new(block);
74        let mut call_traces = vec![];
75        for index in 0..block.body.transactions.len() {
76            // We are cloning the `Arc`s here, not the structs themselves
77            let block = block.clone();
78            let vm = vm.clone();
79            let tx_hash = block.as_ref().body.transactions[index].hash(&NativeCrypto);
80            let call_trace = timeout_trace_operation(timeout, move || {
81                vm.lock()
82                    .map_err(|_| EvmError::Custom("Unexpected Runtime Error".to_string()))?
83                    .trace_tx_calls(block.as_ref(), index, only_top_call, with_log)
84            })
85            .await?;
86            call_traces.push((tx_hash, call_trace));
87        }
88        Ok(call_traces)
89    }
90
91    /// Outputs the prestate trace for the given transaction.
92    /// If `diff_mode` is true, returns both pre and post state; otherwise returns only pre state.
93    /// `include_empty` keeps default-state entries in pre (only valid when `diff_mode` is false).
94    /// May need to re-execute blocks in order to rebuild the transaction's prestate, up to the amount given by `reexec`.
95    pub async fn trace_transaction_prestate(
96        &self,
97        tx_hash: H256,
98        reexec: u32,
99        timeout: Duration,
100        diff_mode: bool,
101        include_empty: bool,
102    ) -> Result<PrestateResult, ChainError> {
103        let Some((_, block_hash, tx_index)) =
104            self.storage.get_transaction_location(tx_hash).await?
105        else {
106            return Err(ChainError::Custom("Transaction not Found".to_string()));
107        };
108        let tx_index = tx_index as usize;
109        let Some(block) = self.storage.get_block_by_hash(block_hash).await? else {
110            return Err(ChainError::Custom("Block not Found".to_string()));
111        };
112        let mut vm = self
113            .rebuild_parent_state(block.header.parent_hash, reexec)
114            .await?;
115        // Run the block until the transaction we want to trace
116        vm.rerun_block(&block, Some(tx_index))?;
117        // Trace the transaction
118        timeout_trace_operation(timeout, move || {
119            vm.trace_tx_prestate(&block, tx_index, diff_mode, include_empty)
120        })
121        .await
122    }
123
124    /// Outputs the prestate trace for each transaction in the block along with the transaction's hash.
125    /// If `diff_mode` is true, returns both pre and post state per tx; otherwise returns only pre state.
126    /// `include_empty` keeps default-state entries in pre (only valid when `diff_mode` is false).
127    /// May need to re-execute blocks in order to rebuild the block's prestate, up to the amount given by `reexec`.
128    /// Returns prestate traces from oldest to newest transaction.
129    pub async fn trace_block_prestate(
130        &self,
131        block: Block,
132        reexec: u32,
133        timeout: Duration,
134        diff_mode: bool,
135        include_empty: bool,
136    ) -> Result<Vec<(H256, PrestateResult)>, ChainError> {
137        let mut vm = self
138            .rebuild_parent_state(block.header.parent_hash, reexec)
139            .await?;
140        // Run system calls but stop before tx 0
141        vm.rerun_block(&block, Some(0))?;
142        // Trace each transaction sequentially — state accumulates between calls
143        // We need to do this in order to pass ownership of block & evm to a blocking process without cloning
144        let vm = Arc::new(Mutex::new(vm));
145        let block = Arc::new(block);
146        let mut traces = vec![];
147        for index in 0..block.body.transactions.len() {
148            let block = block.clone();
149            let vm = vm.clone();
150            let tx_hash = block.as_ref().body.transactions[index].hash(&NativeCrypto);
151            let result = timeout_trace_operation(timeout, move || {
152                vm.lock()
153                    .map_err(|_| EvmError::Custom("Unexpected Runtime Error".to_string()))?
154                    .trace_tx_prestate(block.as_ref(), index, diff_mode, include_empty)
155            })
156            .await?;
157            traces.push((tx_hash, result));
158        }
159        Ok(traces)
160    }
161
162    /// Outputs the per-opcode (EIP-3155) trace for the given transaction.
163    /// May need to re-execute blocks in order to rebuild the transaction's prestate, up to the amount given by `reexec`.
164    pub async fn trace_transaction_opcodes(
165        &self,
166        tx_hash: H256,
167        reexec: u32,
168        timeout: Duration,
169        cfg: OpcodeTracerConfig,
170    ) -> Result<OpcodeTraceResult, ChainError> {
171        let Some((_, block_hash, tx_index)) =
172            self.storage.get_transaction_location(tx_hash).await?
173        else {
174            return Err(ChainError::Custom("Transaction not Found".to_string()));
175        };
176        let tx_index = tx_index as usize;
177        let Some(block) = self.storage.get_block_by_hash(block_hash).await? else {
178            return Err(ChainError::Custom("Block not Found".to_string()));
179        };
180        let mut vm = self
181            .rebuild_parent_state(block.header.parent_hash, reexec)
182            .await?;
183        vm.rerun_block(&block, Some(tx_index))?;
184        timeout_trace_operation(timeout, move || vm.trace_tx_opcodes(&block, tx_index, cfg)).await
185    }
186
187    /// Outputs the opcode (EIP-3155) trace for each transaction in the block along with
188    /// the transaction's hash.
189    /// May need to re-execute blocks in order to rebuild the block's prestate, up to the amount
190    /// given by `reexec`.
191    /// Returns traces from oldest to newest transaction.
192    pub async fn trace_block_opcodes(
193        &self,
194        block: Block,
195        reexec: u32,
196        timeout: Duration,
197        cfg: OpcodeTracerConfig,
198    ) -> Result<Vec<(H256, OpcodeTraceResult)>, ChainError> {
199        let mut vm = self
200            .rebuild_parent_state(block.header.parent_hash, reexec)
201            .await?;
202        vm.rerun_block(&block, Some(0))?;
203        let vm = Arc::new(Mutex::new(vm));
204        let block = Arc::new(block);
205        let mut traces = vec![];
206        for index in 0..block.body.transactions.len() {
207            let block = block.clone();
208            let vm = vm.clone();
209            let tx_hash = block.as_ref().body.transactions[index].hash(&NativeCrypto);
210            let cfg = cfg.clone();
211            let result = timeout_trace_operation(timeout, move || {
212                vm.lock()
213                    .map_err(|_| EvmError::Custom("Unexpected Runtime Error".to_string()))?
214                    .trace_tx_opcodes(block.as_ref(), index, cfg)
215            })
216            .await?;
217            traces.push((tx_hash, result));
218        }
219        Ok(traces)
220    }
221
222    /// Rebuild the parent state for a block given its parent hash, returning an `Evm` instance with all changes cached
223    /// Will re-execute all ancestor block's which's state is not stored up to a maximum given by `reexec`
224    async fn rebuild_parent_state(
225        &self,
226        parent_hash: H256,
227        reexec: u32,
228    ) -> Result<Evm, ChainError> {
229        // Check if we need to re-execute parent blocks
230        let blocks_to_re_execute =
231            get_missing_state_parents(parent_hash, &self.storage, reexec).await?;
232        // Base our Evm's state on the newest parent block which's state we have available
233        let parent_hash = blocks_to_re_execute
234            .last()
235            .map(|b| b.header.parent_hash)
236            .unwrap_or(parent_hash);
237        // Cache block hashes for all parent blocks so we can access them during execution
238        let block_hash_cache = blocks_to_re_execute
239            .iter()
240            .map(|b| (b.header.number, b.hash()))
241            .collect();
242        let parent_header = self
243            .storage
244            .get_block_header_by_hash(parent_hash)?
245            .ok_or(ChainError::ParentNotFound)?;
246        let vm_db = StoreVmDatabase::new_with_block_hash_cache(
247            self.storage.clone(),
248            parent_header,
249            block_hash_cache,
250        )?;
251        let mut vm = self.new_evm(vm_db)?;
252        // Run parents to rebuild pre-state
253        for block in blocks_to_re_execute.iter().rev() {
254            vm.rerun_block(block, None)?;
255        }
256        Ok(vm)
257    }
258}
259
260/// Returns a list of all the parent blocks (starting from parent hash) who's state we don't have stored.
261/// The list will be sorted from newer to older
262/// We might be missing this state due to using batch execute or other methods while syncing the chain
263/// If we are not able to find a parent block with state after going through the amount of blocks given by `reexec` an error will be returned
264async fn get_missing_state_parents(
265    mut parent_hash: H256,
266    store: &Store,
267    reexec: u32,
268) -> Result<Vec<Block>, ChainError> {
269    let mut missing_state_parents = Vec::new();
270    loop {
271        if missing_state_parents.len() > reexec as usize {
272            return Err(ChainError::Custom(
273                "Exceeded max amount of blocks to re-execute for tracing".to_string(),
274            ));
275        }
276        let Some(parent_block) = store.get_block_by_hash(parent_hash).await? else {
277            return Err(ChainError::Custom("Parent Block not Found".to_string()));
278        };
279        if store.has_state_root(parent_block.header.state_root)? {
280            break;
281        }
282        parent_hash = parent_block.header.parent_hash;
283        // Add parent to re-execute list
284        missing_state_parents.push(parent_block);
285    }
286    Ok(missing_state_parents)
287}
288
289/// Runs the given evm trace operation, aborting if it takes more than the time given by `tiemout`
290async fn timeout_trace_operation<O, T>(timeout: Duration, operation: O) -> Result<T, ChainError>
291where
292    O: FnOnce() -> Result<T, EvmError> + Send + 'static,
293    T: Send + 'static,
294{
295    Ok(
296        tokio::time::timeout(timeout, tokio::task::spawn_blocking(operation))
297            .await
298            .map_err(|_| ChainError::Custom("Tracing Timeout".to_string()))?
299            .map_err(|_| ChainError::Custom("Unexpected Runtime Error".to_string()))??,
300    )
301}