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, GenericTransaction, Transaction};
4use ethrex_common::{
5    Address, BigEndianHash, H256, U256,
6    tracing::{CallTrace, CallTraceFrame, OpcodeTraceResult},
7    types::BlockHeader,
8};
9use ethrex_crypto::Crypto;
10use ethrex_levm::StatelessValidator;
11use ethrex_levm::account::{AccountStatus, LevmAccount};
12use ethrex_levm::db::gen_db::CacheDB;
13use ethrex_levm::utils::get_base_fee_per_blob_gas;
14use ethrex_levm::vm::VMType;
15use ethrex_levm::{
16    EVMConfig, Environment,
17    db::gen_db::GeneralizedDatabase,
18    tracing::{LevmCallTracer, LevmOpcodeTracer, OpcodeTracerConfig},
19    vm::VM,
20};
21
22use crate::backends::levm::{
23    adjust_disabled_base_fee, env_from_generic, generic_tx_to_transaction,
24};
25use crate::{EvmError, backends::levm::LEVM};
26
27impl LEVM {
28    /// Execute all transactions of the block up until a certain transaction specified in `stop_index`.
29    /// The goal is to just mutate the state up to that point, without needing to process transaction receipts or requests.
30    pub fn rerun_block(
31        db: &mut GeneralizedDatabase,
32        block: &Block,
33        stop_index: Option<usize>,
34        vm_type: VMType,
35        crypto: &dyn Crypto,
36        stateless_validator: Option<&dyn StatelessValidator>,
37    ) -> Result<(), EvmError> {
38        Self::prepare_block(block, db, vm_type, crypto)?;
39
40        // Executes transactions and stops when the index matches the stop index.
41        for (index, (tx, sender)) in block
42            .body
43            .get_transactions_with_sender(crypto)
44            .map_err(|error| EvmError::Transaction(error.to_string()))?
45            .into_iter()
46            .enumerate()
47        {
48            if stop_index.is_some_and(|stop| stop == index) {
49                break;
50            }
51
52            Self::execute_tx(
53                tx,
54                sender,
55                &block.header,
56                db,
57                vm_type,
58                crypto,
59                stateless_validator,
60            )?;
61        }
62
63        // Process withdrawals only if the whole block has been executed.
64        if stop_index.is_none()
65            && let Some(withdrawals) = &block.body.withdrawals
66        {
67            Self::process_withdrawals(db, withdrawals)?;
68        };
69
70        Ok(())
71    }
72
73    /// Executes `tx` and returns the prestateTracer result. `diff_mode` toggles between
74    /// pre-only and pre+post output. `include_empty` keeps entries that would otherwise
75    /// be all-default (must be false in diff mode). Assumes `db` already reflects all
76    /// prior txs in the block.
77    pub fn trace_tx_prestate(
78        db: &mut GeneralizedDatabase,
79        block_header: &BlockHeader,
80        tx: &Transaction,
81        diff_mode: bool,
82        include_empty: bool,
83        vm_type: VMType,
84        crypto: &dyn Crypto,
85    ) -> Result<PrestateResult, EvmError> {
86        let sender = tx
87            .sender(crypto)
88            .map_err(|e| EvmError::Transaction(format!("Couldn't recover sender: {e}")))?;
89        let env = Self::setup_env(tx, sender, block_header, db, vm_type)?;
90        Self::run_prestate_trace(db, env, tx, diff_mode, include_empty, vm_type, crypto)
91    }
92
93    /// `debug_traceCall` counterpart of [`Self::trace_tx_prestate`]: traces a synthetic
94    /// `eth_call`-shaped request against `db` (which must already hold the target state).
95    pub fn trace_call_prestate(
96        db: &mut GeneralizedDatabase,
97        block_header: &BlockHeader,
98        tx: &GenericTransaction,
99        diff_mode: bool,
100        include_empty: bool,
101        vm_type: VMType,
102        crypto: &dyn Crypto,
103    ) -> Result<PrestateResult, EvmError> {
104        let (env, converted) = prepare_call_env(tx, block_header, db, vm_type)?;
105        Self::run_prestate_trace(
106            db,
107            env,
108            &converted,
109            diff_mode,
110            include_empty,
111            vm_type,
112            crypto,
113        )
114    }
115
116    /// Runs `tx` with the prestateTracer over a prepared `env`, returning pre (and post,
117    /// when `diff_mode`) account state. Shared by the tx and call entry points.
118    fn run_prestate_trace(
119        db: &mut GeneralizedDatabase,
120        env: Environment,
121        tx: &Transaction,
122        diff_mode: bool,
123        include_empty: bool,
124        vm_type: VMType,
125        crypto: &dyn Crypto,
126    ) -> Result<PrestateResult, EvmError> {
127        let pre_snapshot: CacheDB = db.current_accounts_state.clone();
128
129        let mut vm = VM::new(
130            env,
131            db,
132            tx,
133            LevmCallTracer::disabled(),
134            vm_type,
135            crypto,
136            None,
137        )?;
138        vm.execute()?;
139
140        preload_touched_codes(&pre_snapshot, db)?;
141
142        let mut pre_map = build_pre_state_map(&pre_snapshot, &db.current_accounts_state, db)?;
143
144        if diff_mode {
145            let (post_map, kept) =
146                build_post_state_map(&pre_snapshot, &db.current_accounts_state, db)?;
147            filter_diff_pre_storage(&mut pre_map, &db.current_accounts_state);
148            pre_map.retain(|addr, _| kept.contains(addr));
149            pre_map.retain(|_, state| !state.is_empty());
150            Ok(PrestateResult::Diff(PrePostState {
151                pre: pre_map,
152                post: post_map,
153            }))
154        } else {
155            if !include_empty {
156                pre_map.retain(|_, state| !state.is_empty());
157            }
158            Ok(PrestateResult::Prestate(pre_map))
159        }
160    }
161
162    /// Run transaction with opcode (EIP-3155) tracer activated.
163    pub fn trace_tx_opcodes(
164        db: &mut GeneralizedDatabase,
165        block_header: &BlockHeader,
166        tx: &Transaction,
167        cfg: OpcodeTracerConfig,
168        vm_type: VMType,
169        crypto: &dyn Crypto,
170    ) -> Result<OpcodeTraceResult, EvmError> {
171        let env = Self::setup_env(
172            tx,
173            tx.sender(crypto).map_err(|error| {
174                EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
175            })?,
176            block_header,
177            db,
178            vm_type,
179        )?;
180        Self::run_opcode_trace(db, env, tx, cfg, vm_type, crypto)
181    }
182
183    /// `debug_traceCall` counterpart of [`Self::trace_tx_opcodes`].
184    pub fn trace_call_opcodes(
185        db: &mut GeneralizedDatabase,
186        block_header: &BlockHeader,
187        tx: &GenericTransaction,
188        cfg: OpcodeTracerConfig,
189        vm_type: VMType,
190        crypto: &dyn Crypto,
191    ) -> Result<OpcodeTraceResult, EvmError> {
192        let (env, converted) = prepare_call_env(tx, block_header, db, vm_type)?;
193        Self::run_opcode_trace(db, env, &converted, cfg, vm_type, crypto)
194    }
195
196    /// Runs `tx` with the opcode (EIP-3155) tracer over a prepared `env`. Shared by the
197    /// tx and call entry points.
198    fn run_opcode_trace(
199        db: &mut GeneralizedDatabase,
200        env: Environment,
201        tx: &Transaction,
202        cfg: OpcodeTracerConfig,
203        vm_type: VMType,
204        crypto: &dyn Crypto,
205    ) -> Result<OpcodeTraceResult, EvmError> {
206        let mut vm = VM::new(
207            env,
208            db,
209            tx,
210            LevmCallTracer::disabled(),
211            vm_type,
212            crypto,
213            None,
214        )?;
215        vm.opcode_tracer = LevmOpcodeTracer::new(cfg);
216        vm.execute()?;
217        Ok(vm.opcode_tracer.take_result())
218    }
219
220    /// Run transaction with callTracer activated. `log_index_base` is the number of logs
221    /// emitted by preceding txs in the block, so `withLog` logs get geth's block-absolute
222    /// `index` (pass 0 when there is no preceding context or logs aren't collected).
223    #[expect(clippy::too_many_arguments)]
224    pub fn trace_tx_calls(
225        db: &mut GeneralizedDatabase,
226        block_header: &BlockHeader,
227        tx: &Transaction,
228        only_top_call: bool,
229        with_log: bool,
230        log_index_base: u64,
231        vm_type: VMType,
232        crypto: &dyn Crypto,
233        stateless_validator: Option<&dyn StatelessValidator>,
234    ) -> Result<CallTrace, EvmError> {
235        let env = Self::setup_env(
236            tx,
237            tx.sender(crypto).map_err(|error| {
238                EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
239            })?,
240            block_header,
241            db,
242            vm_type,
243        )?;
244        Self::run_call_trace(
245            db,
246            env,
247            tx,
248            only_top_call,
249            with_log,
250            log_index_base,
251            vm_type,
252            crypto,
253            stateless_validator,
254        )
255    }
256
257    /// `debug_traceCall` counterpart of [`Self::trace_tx_calls`].
258    #[allow(clippy::too_many_arguments)]
259    pub fn trace_call_calls(
260        db: &mut GeneralizedDatabase,
261        block_header: &BlockHeader,
262        tx: &GenericTransaction,
263        only_top_call: bool,
264        with_log: bool,
265        log_index_base: u64,
266        vm_type: VMType,
267        crypto: &dyn Crypto,
268    ) -> Result<CallTrace, EvmError> {
269        let (env, converted) = prepare_call_env(tx, block_header, db, vm_type)?;
270        Self::run_call_trace(
271            db,
272            env,
273            &converted,
274            only_top_call,
275            with_log,
276            log_index_base,
277            vm_type,
278            crypto,
279            None,
280        )
281    }
282
283    /// Runs `tx` with the callTracer over a prepared `env`. Shared by the tx and call
284    /// entry points.
285    #[expect(clippy::too_many_arguments)]
286    fn run_call_trace(
287        db: &mut GeneralizedDatabase,
288        env: Environment,
289        tx: &Transaction,
290        only_top_call: bool,
291        with_log: bool,
292        log_index_base: u64,
293        vm_type: VMType,
294        crypto: &dyn Crypto,
295        stateless_validator: Option<&dyn StatelessValidator>,
296    ) -> Result<CallTrace, EvmError> {
297        let mut vm = VM::new(
298            env,
299            db,
300            tx,
301            LevmCallTracer::new(only_top_call, with_log, log_index_base),
302            vm_type,
303            crypto,
304            stateless_validator,
305        )?;
306
307        vm.execute()?;
308
309        let callframe = vm.get_trace_result()?;
310
311        // We only return the top call because a transaction only has one call with subcalls
312        Ok(vec![callframe])
313    }
314
315    /// Traces every transaction in `block` with the callTracer, oldest to newest.
316    /// `db` must already hold the block's parent state; this runs the block's system calls
317    /// (beacon root, etc.) and then each transaction in order, accumulating state. The
318    /// block-invariant `EVMConfig`/`chain_id`/blob fee are computed once and reused per tx
319    /// (the single-tx entry points recompute them each call). Returns `(tx_hash, trace)`.
320    pub fn trace_block_calls(
321        db: &mut GeneralizedDatabase,
322        block: &Block,
323        only_top_call: bool,
324        with_log: bool,
325        vm_type: VMType,
326        crypto: &dyn Crypto,
327    ) -> Result<Vec<(H256, CallTrace)>, EvmError> {
328        Self::rerun_block(db, block, Some(0), vm_type, crypto, None)?;
329        let (config, chain_id, base_blob_fee) = block_trace_env_config(db, &block.header)?;
330        let mut traces = Vec::with_capacity(block.body.transactions.len());
331        // Running block-absolute log index: the whole block is traced from tx 0, so each
332        // tx's logs continue where the previous tx left off (geth's cumulative `logSize`).
333        let mut log_index_base = 0u64;
334        for (tx, sender) in block
335            .body
336            .get_transactions_with_sender(crypto)
337            .map_err(|e| EvmError::Transaction(e.to_string()))?
338        {
339            let env = Self::setup_env_with_config(
340                tx,
341                sender,
342                &block.header,
343                config,
344                chain_id,
345                vm_type,
346                base_blob_fee,
347            )?;
348            let trace = Self::run_call_trace(
349                db,
350                env,
351                tx,
352                only_top_call,
353                with_log,
354                log_index_base,
355                vm_type,
356                crypto,
357                None,
358            )?;
359            log_index_base = log_index_base.saturating_add(trace.iter().map(count_call_logs).sum());
360            traces.push((tx.hash(crypto), trace));
361        }
362        Ok(traces)
363    }
364
365    /// Traces every transaction in `block` with the prestateTracer. See
366    /// [`Self::trace_block_calls`] for the state/config-reuse semantics.
367    pub fn trace_block_prestate(
368        db: &mut GeneralizedDatabase,
369        block: &Block,
370        diff_mode: bool,
371        include_empty: bool,
372        vm_type: VMType,
373        crypto: &dyn Crypto,
374    ) -> Result<Vec<(H256, PrestateResult)>, EvmError> {
375        Self::rerun_block(db, block, Some(0), vm_type, crypto, None)?;
376        let (config, chain_id, base_blob_fee) = block_trace_env_config(db, &block.header)?;
377        let mut traces = Vec::with_capacity(block.body.transactions.len());
378        for (tx, sender) in block
379            .body
380            .get_transactions_with_sender(crypto)
381            .map_err(|e| EvmError::Transaction(e.to_string()))?
382        {
383            let env = Self::setup_env_with_config(
384                tx,
385                sender,
386                &block.header,
387                config,
388                chain_id,
389                vm_type,
390                base_blob_fee,
391            )?;
392            let trace =
393                Self::run_prestate_trace(db, env, tx, diff_mode, include_empty, vm_type, crypto)?;
394            traces.push((tx.hash(crypto), trace));
395        }
396        Ok(traces)
397    }
398
399    /// Traces every transaction in `block` with the opcode (EIP-3155) tracer. See
400    /// [`Self::trace_block_calls`] for the state/config-reuse semantics.
401    pub fn trace_block_opcodes(
402        db: &mut GeneralizedDatabase,
403        block: &Block,
404        cfg: OpcodeTracerConfig,
405        vm_type: VMType,
406        crypto: &dyn Crypto,
407    ) -> Result<Vec<(H256, OpcodeTraceResult)>, EvmError> {
408        Self::rerun_block(db, block, Some(0), vm_type, crypto, None)?;
409        let (config, chain_id, base_blob_fee) = block_trace_env_config(db, &block.header)?;
410        let mut traces = Vec::with_capacity(block.body.transactions.len());
411        for (tx, sender) in block
412            .body
413            .get_transactions_with_sender(crypto)
414            .map_err(|e| EvmError::Transaction(e.to_string()))?
415        {
416            let env = Self::setup_env_with_config(
417                tx,
418                sender,
419                &block.header,
420                config,
421                chain_id,
422                vm_type,
423                base_blob_fee,
424            )?;
425            let trace = Self::run_opcode_trace(db, env, tx, cfg.clone(), vm_type, crypto)?;
426            traces.push((tx.hash(crypto), trace));
427        }
428        Ok(traces)
429    }
430}
431
432/// Recursively counts the logs captured in a call frame and all its subcalls — the
433/// number of `withLog` logs a traced tx contributes to the block-absolute log index.
434fn count_call_logs(frame: &CallTraceFrame) -> u64 {
435    let own = u64::try_from(frame.logs.len()).unwrap_or(u64::MAX);
436    frame.calls.iter().fold(own, |acc, subcall| {
437        acc.saturating_add(count_call_logs(subcall))
438    })
439}
440
441/// Computes the block-invariant `(EVMConfig, chain_id, base_blob_fee)` once so a
442/// whole-block trace can reuse them across transactions instead of recomputing per tx.
443fn block_trace_env_config(
444    db: &GeneralizedDatabase,
445    header: &BlockHeader,
446) -> Result<(EVMConfig, u64, U256), EvmError> {
447    let chain_config = db.store.get_chain_config()?;
448    let config = EVMConfig::new_from_chain_config(&chain_config, header);
449    let base_blob_fee = get_base_fee_per_blob_gas(header.excess_blob_gas, &config)?;
450    Ok((config, chain_config.chain_id, base_blob_fee))
451}
452
453/// Builds the call-style [`Environment`] and concrete [`Transaction`] for tracing a
454/// synthetic `eth_call`-shaped request (`debug_traceCall`). Mirrors
455/// `simulate_tx_from_generic`: the sender is taken from `tx.from` (no signature
456/// recovery), the block gas-allowance check is skipped, and the base fee is relaxed when
457/// no gas price is provided.
458fn prepare_call_env(
459    tx: &GenericTransaction,
460    block_header: &BlockHeader,
461    db: &GeneralizedDatabase,
462    vm_type: VMType,
463) -> Result<(Environment, Transaction), EvmError> {
464    let mut env = env_from_generic(tx, block_header, db, vm_type)?;
465    // Skip the allowance check without touching `block_gas_limit`; see
466    // `simulate_tx_from_generic`. A trace must report the same GASLIMIT the traced block
467    // executed with.
468    env.disable_gas_allowance_check = true;
469    adjust_disabled_base_fee(&mut env);
470    let converted_tx = generic_tx_to_transaction(tx)?;
471    Ok((env, converted_tx))
472}
473
474/// Returns `(address, pre_account, post_account)` for every account in `post_cache`.
475/// `pre_account` comes from `pre_snapshot` if cached before the tx, otherwise from
476/// `initial_accounts_state`. Filtering unchanged accounts is the caller's job.
477fn find_touched_accounts<'a>(
478    pre_snapshot: &'a CacheDB,
479    post_cache: &'a CacheDB,
480    db: &'a GeneralizedDatabase,
481) -> Vec<(Address, &'a LevmAccount, &'a LevmAccount)> {
482    let mut touched = Vec::new();
483
484    for (addr, post_account) in post_cache {
485        let pre_account = match pre_snapshot.get(addr) {
486            Some(pre) => pre,
487            None => {
488                let Some(initial) = db.initial_accounts_state.get(addr) else {
489                    continue;
490                };
491                initial
492            }
493        };
494
495        touched.push((*addr, pre_account, post_account));
496    }
497
498    touched
499}
500
501/// Reads code from `db.codes`; caller must `preload_touched_codes` first.
502/// Storage values are passed through as-is (including zero); per-field filtering
503/// for diff-mode post is applied by `build_post_output`.
504fn build_account_output(
505    account: &LevmAccount,
506    db: &GeneralizedDatabase,
507) -> Result<PrestateAccountState, EvmError> {
508    let has_code = account.info.code_hash != *EMPTY_KECCAK_HASH;
509    let code = if has_code {
510        get_preloaded_code(db, &account.info.code_hash)?
511    } else {
512        bytes::Bytes::new()
513    };
514    let code_hash = if has_code {
515        account.info.code_hash
516    } else {
517        H256::zero()
518    };
519
520    let storage = account
521        .storage
522        .iter()
523        .map(|(k, v)| (*k, H256::from_uint(v)))
524        .collect();
525
526    Ok(PrestateAccountState {
527        balance: Some(account.info.balance),
528        nonce: account.info.nonce,
529        code,
530        code_hash,
531        storage,
532    })
533}
534
535/// Returns the bytecode for `hash`; caller must `preload_touched_codes` first.
536fn get_preloaded_code(db: &GeneralizedDatabase, hash: &H256) -> Result<bytes::Bytes, EvmError> {
537    db.codes
538        .get(hash)
539        .map(|c| c.code_bytes())
540        .ok_or_else(|| EvmError::Custom(format!("missing preloaded code for {hash:?}")))
541}
542
543/// Builds the diff-mode post entry for a touched account, emitting only fields whose
544/// value differs from the pre-tx state. Storage entries are limited to slots that
545/// actually changed and have a non-zero post value. Returns `None` if nothing changed.
546fn build_post_output(
547    addr: Address,
548    pre_account: &LevmAccount,
549    post_account: &LevmAccount,
550    pre_snapshot: &CacheDB,
551    db: &GeneralizedDatabase,
552) -> Result<Option<PrestateAccountState>, EvmError> {
553    let mut state = PrestateAccountState::default();
554    let mut modified = false;
555
556    if pre_account.info.balance != post_account.info.balance {
557        state.balance = Some(post_account.info.balance);
558        modified = true;
559    }
560    if pre_account.info.nonce != post_account.info.nonce {
561        state.nonce = post_account.info.nonce;
562        modified = true;
563    }
564    if pre_account.info.code_hash != post_account.info.code_hash {
565        if post_account.info.code_hash != *EMPTY_KECCAK_HASH {
566            state.code_hash = post_account.info.code_hash;
567            state.code = get_preloaded_code(db, &post_account.info.code_hash)?;
568        }
569        modified = true;
570    }
571
572    for (key, post_val) in &post_account.storage {
573        let pre_val = pre_storage_value(addr, key, pre_snapshot, db).unwrap_or_default();
574        if pre_val == *post_val {
575            continue;
576        }
577        modified = true;
578        // Cleared slots (post == 0) are encoded by absence in `post.storage`.
579        if !post_val.is_zero() {
580            state.storage.insert(*key, H256::from_uint(post_val));
581        }
582    }
583
584    Ok(modified.then_some(state))
585}
586
587/// Resolves the pre-tx value of `slot` for `addr`. Slots accessed in earlier txs are in
588/// `pre_snapshot`; slots first loaded in this tx live only in `initial_accounts_state`.
589fn pre_storage_value(
590    addr: Address,
591    slot: &H256,
592    pre_snapshot: &CacheDB,
593    db: &GeneralizedDatabase,
594) -> Option<U256> {
595    if let Some(account) = pre_snapshot.get(&addr)
596        && let Some(value) = account.storage.get(slot)
597    {
598        return Some(*value);
599    }
600    db.initial_accounts_state
601        .get(&addr)
602        .and_then(|a| a.storage.get(slot).copied())
603}
604
605/// Builds the pre-tx state map. Pre storage is restricted to slots accessed by THIS
606/// tx — for accounts cached before this tx that means slots first loaded here or slots
607/// whose value changed; for accounts first accessed here, every slot in `post.storage`.
608/// The final `post.storage` membership check is a defensive guard: it bounds pre to
609/// the set of slots that ended up in the post cache, so unrelated slots that ever leak
610/// into `initial_accounts_state` (e.g. via more eager caching upstream) cannot leak into
611/// pre output.
612fn build_pre_state_map(
613    pre_snapshot: &CacheDB,
614    post_cache: &CacheDB,
615    db: &GeneralizedDatabase,
616) -> Result<PrestateTrace, EvmError> {
617    let mut result = PrestateTrace::new();
618
619    for (addr, pre_account, post_account) in find_touched_accounts(pre_snapshot, post_cache, db) {
620        let mut state = build_account_output(pre_account, db)?;
621
622        // For already-cached accounts, the pre-tx values of slots first loaded in this
623        // tx live in `initial_accounts_state` rather than in `pre_snapshot`. Newly-accessed
624        // accounts already have those values via `pre_account` (which comes from
625        // `initial_accounts_state` in `find_touched_accounts`).
626        if pre_snapshot.contains_key(&addr)
627            && let Some(initial) = db.initial_accounts_state.get(&addr)
628        {
629            for (k, v) in &initial.storage {
630                state
631                    .storage
632                    .entry(*k)
633                    .or_insert_with(|| H256::from_uint(v));
634            }
635        }
636
637        let pre_cached_storage = pre_snapshot.get(&addr).map(|a| &a.storage);
638        state.storage.retain(|k, _| {
639            if !post_account.storage.contains_key(k) {
640                return false;
641            }
642            match pre_cached_storage {
643                Some(pre) if pre.contains_key(k) => pre.get(k) != post_account.storage.get(k),
644                _ => true,
645            }
646        });
647
648        result.insert(addr, state);
649    }
650
651    Ok(result)
652}
653
654/// Loads code into `db.codes` for every touched contract whose code wasn't executed
655/// (SELFDESTRUCT beneficiaries, plain-value transfer recipients) — without this they'd
656/// serialize as `code: 0x` despite a non-empty `code_hash`.
657fn preload_touched_codes(
658    pre_snapshot: &CacheDB,
659    db: &mut GeneralizedDatabase,
660) -> Result<(), EvmError> {
661    let hashes: Vec<H256> = db
662        .current_accounts_state
663        .iter()
664        .flat_map(|(addr, post)| {
665            let pre_hash = pre_snapshot
666                .get(addr)
667                .or_else(|| db.initial_accounts_state.get(addr))
668                .map(|a| a.info.code_hash)
669                .unwrap_or_default();
670            [post.info.code_hash, pre_hash]
671        })
672        .filter(|h| *h != *EMPTY_KECCAK_HASH)
673        .collect();
674
675    for hash in hashes {
676        db.get_code(hash)?;
677    }
678    Ok(())
679}
680
681/// Builds the diff-mode post map and the set of modified-or-destroyed addresses
682/// (used to prune diff `pre`) in a single pass.
683fn build_post_state_map(
684    pre_snapshot: &CacheDB,
685    post_cache: &CacheDB,
686    db: &GeneralizedDatabase,
687) -> Result<(PrestateTrace, std::collections::HashSet<Address>), EvmError> {
688    let mut post = PrestateTrace::new();
689    let mut modified_or_destroyed = std::collections::HashSet::new();
690
691    for (addr, pre_account, post_account) in find_touched_accounts(pre_snapshot, post_cache, db) {
692        if matches!(
693            post_account.status,
694            AccountStatus::Destroyed | AccountStatus::DestroyedModified,
695        ) {
696            modified_or_destroyed.insert(addr);
697            continue;
698        }
699
700        if let Some(state) = build_post_output(addr, pre_account, post_account, pre_snapshot, db)? {
701            modified_or_destroyed.insert(addr);
702            post.insert(addr, state);
703        }
704    }
705
706    Ok((post, modified_or_destroyed))
707}
708
709/// Trims storage entries in a diff-mode pre map: drops slots whose pre value is zero
710/// or whose pre value equals the post value (unchanged in this tx).
711fn filter_diff_pre_storage(pre: &mut PrestateTrace, post_cache: &CacheDB) {
712    for (addr, state) in pre.iter_mut() {
713        let post_storage = post_cache.get(addr).map(|a| &a.storage);
714        state.storage.retain(|k, v| {
715            if v.is_zero() {
716                return false;
717            }
718            let post_val = post_storage
719                .and_then(|s| s.get(k).copied())
720                .unwrap_or_default();
721            *v != H256::from_uint(&post_val)
722        });
723    }
724}