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