Skip to main content

ethrex_vm/backends/levm/
tracing.rs

1use ethrex_common::constants::EMPTY_KECCAK_HASH;
2use ethrex_common::tracing::{PrePostState, PrestateAccountState, PrestateResult, PrestateTrace};
3use ethrex_common::types::{Block, Transaction};
4use ethrex_common::{
5    Address, BigEndianHash, H256, U256,
6    tracing::{CallTrace, OpcodeTraceResult},
7    types::BlockHeader,
8};
9use ethrex_crypto::Crypto;
10use ethrex_levm::account::{AccountStatus, LevmAccount};
11use ethrex_levm::db::gen_db::CacheDB;
12use ethrex_levm::vm::VMType;
13use ethrex_levm::{
14    db::gen_db::GeneralizedDatabase,
15    tracing::{LevmCallTracer, LevmOpcodeTracer, OpcodeTracerConfig},
16    vm::VM,
17};
18
19use crate::{EvmError, backends::levm::LEVM};
20
21impl LEVM {
22    /// Execute all transactions of the block up until a certain transaction specified in `stop_index`.
23    /// The goal is to just mutate the state up to that point, without needing to process transaction receipts or requests.
24    pub fn rerun_block(
25        db: &mut GeneralizedDatabase,
26        block: &Block,
27        stop_index: Option<usize>,
28        vm_type: VMType,
29        crypto: &dyn Crypto,
30    ) -> Result<(), EvmError> {
31        Self::prepare_block(block, db, vm_type, crypto)?;
32
33        // Executes transactions and stops when the index matches the stop index.
34        for (index, (tx, sender)) in block
35            .body
36            .get_transactions_with_sender(crypto)
37            .map_err(|error| EvmError::Transaction(error.to_string()))?
38            .into_iter()
39            .enumerate()
40        {
41            if stop_index.is_some_and(|stop| stop == index) {
42                break;
43            }
44
45            Self::execute_tx(tx, sender, &block.header, db, vm_type, crypto)?;
46        }
47
48        // Process withdrawals only if the whole block has been executed.
49        if stop_index.is_none()
50            && let Some(withdrawals) = &block.body.withdrawals
51        {
52            Self::process_withdrawals(db, withdrawals)?;
53        };
54
55        Ok(())
56    }
57
58    /// Executes `tx` and returns the prestateTracer result. `diff_mode` toggles between
59    /// pre-only and pre+post output. `include_empty` keeps entries that would otherwise
60    /// be all-default (must be false in diff mode). Assumes `db` already reflects all
61    /// prior txs in the block.
62    pub fn trace_tx_prestate(
63        db: &mut GeneralizedDatabase,
64        block_header: &BlockHeader,
65        tx: &Transaction,
66        diff_mode: bool,
67        include_empty: bool,
68        vm_type: VMType,
69        crypto: &dyn Crypto,
70    ) -> Result<PrestateResult, EvmError> {
71        let pre_snapshot: CacheDB = db.current_accounts_state.clone();
72
73        let sender = tx
74            .sender(crypto)
75            .map_err(|e| EvmError::Transaction(format!("Couldn't recover sender: {e}")))?;
76        let env = Self::setup_env(tx, sender, block_header, db, vm_type)?;
77        let mut vm = VM::new(env, db, tx, LevmCallTracer::disabled(), vm_type, crypto)?;
78        vm.execute()?;
79
80        preload_touched_codes(&pre_snapshot, db)?;
81
82        let mut pre_map = build_pre_state_map(&pre_snapshot, &db.current_accounts_state, db)?;
83
84        if diff_mode {
85            let (post_map, kept) =
86                build_post_state_map(&pre_snapshot, &db.current_accounts_state, db)?;
87            filter_diff_pre_storage(&mut pre_map, &db.current_accounts_state);
88            pre_map.retain(|addr, _| kept.contains(addr));
89            pre_map.retain(|_, state| !state.is_empty());
90            Ok(PrestateResult::Diff(PrePostState {
91                pre: pre_map,
92                post: post_map,
93            }))
94        } else {
95            if !include_empty {
96                pre_map.retain(|_, state| !state.is_empty());
97            }
98            Ok(PrestateResult::Prestate(pre_map))
99        }
100    }
101
102    /// Run transaction with opcode (EIP-3155) tracer activated.
103    pub fn trace_tx_opcodes(
104        db: &mut GeneralizedDatabase,
105        block_header: &BlockHeader,
106        tx: &Transaction,
107        cfg: OpcodeTracerConfig,
108        vm_type: VMType,
109        crypto: &dyn Crypto,
110    ) -> Result<OpcodeTraceResult, EvmError> {
111        let env = Self::setup_env(
112            tx,
113            tx.sender(crypto).map_err(|error| {
114                EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
115            })?,
116            block_header,
117            db,
118            vm_type,
119        )?;
120        let mut vm = VM::new(env, db, tx, LevmCallTracer::disabled(), vm_type, crypto)?;
121        vm.opcode_tracer = LevmOpcodeTracer::new(cfg);
122        vm.execute()?;
123        Ok(vm.opcode_tracer.take_result())
124    }
125
126    /// Run transaction with callTracer activated.
127    pub fn trace_tx_calls(
128        db: &mut GeneralizedDatabase,
129        block_header: &BlockHeader,
130        tx: &Transaction,
131        only_top_call: bool,
132        with_log: bool,
133        vm_type: VMType,
134        crypto: &dyn Crypto,
135    ) -> Result<CallTrace, EvmError> {
136        let env = Self::setup_env(
137            tx,
138            tx.sender(crypto).map_err(|error| {
139                EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
140            })?,
141            block_header,
142            db,
143            vm_type,
144        )?;
145        let mut vm = VM::new(
146            env,
147            db,
148            tx,
149            LevmCallTracer::new(only_top_call, with_log),
150            vm_type,
151            crypto,
152        )?;
153
154        vm.execute()?;
155
156        let callframe = vm.get_trace_result()?;
157
158        // We only return the top call because a transaction only has one call with subcalls
159        Ok(vec![callframe])
160    }
161}
162
163/// Returns `(address, pre_account, post_account)` for every account in `post_cache`.
164/// `pre_account` comes from `pre_snapshot` if cached before the tx, otherwise from
165/// `initial_accounts_state`. Filtering unchanged accounts is the caller's job.
166fn find_touched_accounts<'a>(
167    pre_snapshot: &'a CacheDB,
168    post_cache: &'a CacheDB,
169    db: &'a GeneralizedDatabase,
170) -> Vec<(Address, &'a LevmAccount, &'a LevmAccount)> {
171    let mut touched = Vec::new();
172
173    for (addr, post_account) in post_cache {
174        let pre_account = match pre_snapshot.get(addr) {
175            Some(pre) => pre,
176            None => {
177                let Some(initial) = db.initial_accounts_state.get(addr) else {
178                    continue;
179                };
180                initial
181            }
182        };
183
184        touched.push((*addr, pre_account, post_account));
185    }
186
187    touched
188}
189
190/// Reads code from `db.codes`; caller must `preload_touched_codes` first.
191/// Storage values are passed through as-is (including zero); per-field filtering
192/// for diff-mode post is applied by `build_post_output`.
193fn build_account_output(
194    account: &LevmAccount,
195    db: &GeneralizedDatabase,
196) -> Result<PrestateAccountState, EvmError> {
197    let has_code = account.info.code_hash != *EMPTY_KECCAK_HASH;
198    let code = if has_code {
199        get_preloaded_code(db, &account.info.code_hash)?
200    } else {
201        bytes::Bytes::new()
202    };
203    let code_hash = if has_code {
204        account.info.code_hash
205    } else {
206        H256::zero()
207    };
208
209    let storage = account
210        .storage
211        .iter()
212        .map(|(k, v)| (*k, H256::from_uint(v)))
213        .collect();
214
215    Ok(PrestateAccountState {
216        balance: Some(account.info.balance),
217        nonce: account.info.nonce,
218        code,
219        code_hash,
220        storage,
221    })
222}
223
224/// Returns the bytecode for `hash`; caller must `preload_touched_codes` first.
225fn get_preloaded_code(db: &GeneralizedDatabase, hash: &H256) -> Result<bytes::Bytes, EvmError> {
226    db.codes
227        .get(hash)
228        .map(|c| c.code_bytes())
229        .ok_or_else(|| EvmError::Custom(format!("missing preloaded code for {hash:?}")))
230}
231
232/// Builds the diff-mode post entry for a touched account, emitting only fields whose
233/// value differs from the pre-tx state. Storage entries are limited to slots that
234/// actually changed and have a non-zero post value. Returns `None` if nothing changed.
235fn build_post_output(
236    addr: Address,
237    pre_account: &LevmAccount,
238    post_account: &LevmAccount,
239    pre_snapshot: &CacheDB,
240    db: &GeneralizedDatabase,
241) -> Result<Option<PrestateAccountState>, EvmError> {
242    let mut state = PrestateAccountState::default();
243    let mut modified = false;
244
245    if pre_account.info.balance != post_account.info.balance {
246        state.balance = Some(post_account.info.balance);
247        modified = true;
248    }
249    if pre_account.info.nonce != post_account.info.nonce {
250        state.nonce = post_account.info.nonce;
251        modified = true;
252    }
253    if pre_account.info.code_hash != post_account.info.code_hash {
254        if post_account.info.code_hash != *EMPTY_KECCAK_HASH {
255            state.code_hash = post_account.info.code_hash;
256            state.code = get_preloaded_code(db, &post_account.info.code_hash)?;
257        }
258        modified = true;
259    }
260
261    for (key, post_val) in &post_account.storage {
262        let pre_val = pre_storage_value(addr, key, pre_snapshot, db).unwrap_or_default();
263        if pre_val == *post_val {
264            continue;
265        }
266        modified = true;
267        // Cleared slots (post == 0) are encoded by absence in `post.storage`.
268        if !post_val.is_zero() {
269            state.storage.insert(*key, H256::from_uint(post_val));
270        }
271    }
272
273    Ok(modified.then_some(state))
274}
275
276/// Resolves the pre-tx value of `slot` for `addr`. Slots accessed in earlier txs are in
277/// `pre_snapshot`; slots first loaded in this tx live only in `initial_accounts_state`.
278fn pre_storage_value(
279    addr: Address,
280    slot: &H256,
281    pre_snapshot: &CacheDB,
282    db: &GeneralizedDatabase,
283) -> Option<U256> {
284    if let Some(account) = pre_snapshot.get(&addr)
285        && let Some(value) = account.storage.get(slot)
286    {
287        return Some(*value);
288    }
289    db.initial_accounts_state
290        .get(&addr)
291        .and_then(|a| a.storage.get(slot).copied())
292}
293
294/// Builds the pre-tx state map. Pre storage is restricted to slots accessed by THIS
295/// tx — for accounts cached before this tx that means slots first loaded here or slots
296/// whose value changed; for accounts first accessed here, every slot in `post.storage`.
297/// The final `post.storage` membership check is a defensive guard: it bounds pre to
298/// the set of slots that ended up in the post cache, so unrelated slots that ever leak
299/// into `initial_accounts_state` (e.g. via more eager caching upstream) cannot leak into
300/// pre output.
301fn build_pre_state_map(
302    pre_snapshot: &CacheDB,
303    post_cache: &CacheDB,
304    db: &GeneralizedDatabase,
305) -> Result<PrestateTrace, EvmError> {
306    let mut result = PrestateTrace::new();
307
308    for (addr, pre_account, post_account) in find_touched_accounts(pre_snapshot, post_cache, db) {
309        let mut state = build_account_output(pre_account, db)?;
310
311        // For already-cached accounts, the pre-tx values of slots first loaded in this
312        // tx live in `initial_accounts_state` rather than in `pre_snapshot`. Newly-accessed
313        // accounts already have those values via `pre_account` (which comes from
314        // `initial_accounts_state` in `find_touched_accounts`).
315        if pre_snapshot.contains_key(&addr)
316            && let Some(initial) = db.initial_accounts_state.get(&addr)
317        {
318            for (k, v) in &initial.storage {
319                state
320                    .storage
321                    .entry(*k)
322                    .or_insert_with(|| H256::from_uint(v));
323            }
324        }
325
326        let pre_cached_storage = pre_snapshot.get(&addr).map(|a| &a.storage);
327        state.storage.retain(|k, _| {
328            if !post_account.storage.contains_key(k) {
329                return false;
330            }
331            match pre_cached_storage {
332                Some(pre) if pre.contains_key(k) => pre.get(k) != post_account.storage.get(k),
333                _ => true,
334            }
335        });
336
337        result.insert(addr, state);
338    }
339
340    Ok(result)
341}
342
343/// Loads code into `db.codes` for every touched contract whose code wasn't executed
344/// (SELFDESTRUCT beneficiaries, plain-value transfer recipients) — without this they'd
345/// serialize as `code: 0x` despite a non-empty `code_hash`.
346fn preload_touched_codes(
347    pre_snapshot: &CacheDB,
348    db: &mut GeneralizedDatabase,
349) -> Result<(), EvmError> {
350    let hashes: Vec<H256> = db
351        .current_accounts_state
352        .iter()
353        .flat_map(|(addr, post)| {
354            let pre_hash = pre_snapshot
355                .get(addr)
356                .or_else(|| db.initial_accounts_state.get(addr))
357                .map(|a| a.info.code_hash)
358                .unwrap_or_default();
359            [post.info.code_hash, pre_hash]
360        })
361        .filter(|h| *h != *EMPTY_KECCAK_HASH)
362        .collect();
363
364    for hash in hashes {
365        db.get_code(hash)?;
366    }
367    Ok(())
368}
369
370/// Builds the diff-mode post map and the set of modified-or-destroyed addresses
371/// (used to prune diff `pre`) in a single pass.
372fn build_post_state_map(
373    pre_snapshot: &CacheDB,
374    post_cache: &CacheDB,
375    db: &GeneralizedDatabase,
376) -> Result<(PrestateTrace, std::collections::HashSet<Address>), EvmError> {
377    let mut post = PrestateTrace::new();
378    let mut modified_or_destroyed = std::collections::HashSet::new();
379
380    for (addr, pre_account, post_account) in find_touched_accounts(pre_snapshot, post_cache, db) {
381        if matches!(
382            post_account.status,
383            AccountStatus::Destroyed | AccountStatus::DestroyedModified,
384        ) {
385            modified_or_destroyed.insert(addr);
386            continue;
387        }
388
389        if let Some(state) = build_post_output(addr, pre_account, post_account, pre_snapshot, db)? {
390            modified_or_destroyed.insert(addr);
391            post.insert(addr, state);
392        }
393    }
394
395    Ok((post, modified_or_destroyed))
396}
397
398/// Trims storage entries in a diff-mode pre map: drops slots whose pre value is zero
399/// or whose pre value equals the post value (unchanged in this tx).
400fn filter_diff_pre_storage(pre: &mut PrestateTrace, post_cache: &CacheDB) {
401    for (addr, state) in pre.iter_mut() {
402        let post_storage = post_cache.get(addr).map(|a| &a.storage);
403        state.storage.retain(|k, v| {
404            if v.is_zero() {
405                return false;
406            }
407            let post_val = post_storage
408                .and_then(|s| s.get(k).copied())
409                .unwrap_or_default();
410            *v != H256::from_uint(&post_val)
411        });
412    }
413}