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, Fork, 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        let report = vm.execute()?;
308        let fork = vm.env.config.fork;
309
310        let mut callframe = vm.get_trace_result()?;
311
312        // EIP-8037 two-dimensional gas, reported on the top-level frame only from
313        // Amsterdam onwards (execution-apis `CallFrame`). EIP-8141 frame
314        // transactions (`frame_results` is set) are skipped: their report's
315        // `gas_refunded` holds unused frame gas rather than the EIP-3529 refund
316        // these fields advertise, and the spec defines no aggregate attribution
317        // for them.
318        if fork >= Fork::Amsterdam && report.frame_results.is_none() {
319            // A portion cannot exceed its whole; the saturation below is a
320            // last-resort clamp, not an expected path.
321            debug_assert!(
322                report.state_gas_used <= report.gas_used,
323                "state_gas_used ({}) exceeds gas_used ({})",
324                report.state_gas_used,
325                report.gas_used
326            );
327            callframe.regular_gas_used =
328                Some(report.gas_used.saturating_sub(report.state_gas_used));
329            callframe.state_gas_used = Some(report.state_gas_used);
330            callframe.gas_refund = Some(report.gas_refunded);
331        }
332
333        // We only return the top call because a transaction only has one call with subcalls
334        Ok(vec![callframe])
335    }
336
337    /// Traces every transaction in `block` with the callTracer, oldest to newest.
338    /// `db` must already hold the block's parent state; this runs the block's system calls
339    /// (beacon root, etc.) and then each transaction in order, accumulating state. The
340    /// block-invariant `EVMConfig`/`chain_id`/blob fee are computed once and reused per tx
341    /// (the single-tx entry points recompute them each call). Returns `(tx_hash, trace)`.
342    pub fn trace_block_calls(
343        db: &mut GeneralizedDatabase,
344        block: &Block,
345        only_top_call: bool,
346        with_log: bool,
347        vm_type: VMType,
348        crypto: &dyn Crypto,
349    ) -> Result<Vec<(H256, CallTrace)>, EvmError> {
350        Self::rerun_block(db, block, Some(0), vm_type, crypto, None)?;
351        let (config, chain_id, base_blob_fee) = block_trace_env_config(db, &block.header)?;
352        let mut traces = Vec::with_capacity(block.body.transactions.len());
353        // Running block-absolute log index: the whole block is traced from tx 0, so each
354        // tx's logs continue where the previous tx left off (geth's cumulative `logSize`).
355        let mut log_index_base = 0u64;
356        for (tx, sender) in block
357            .body
358            .get_transactions_with_sender(crypto)
359            .map_err(|e| EvmError::Transaction(e.to_string()))?
360        {
361            let env = Self::setup_env_with_config(
362                tx,
363                sender,
364                &block.header,
365                config,
366                chain_id,
367                vm_type,
368                base_blob_fee,
369            )?;
370            let trace = Self::run_call_trace(
371                db,
372                env,
373                tx,
374                only_top_call,
375                with_log,
376                log_index_base,
377                vm_type,
378                crypto,
379                None,
380            )?;
381            log_index_base = log_index_base.saturating_add(trace.iter().map(count_call_logs).sum());
382            traces.push((tx.hash(crypto), trace));
383        }
384        Ok(traces)
385    }
386
387    /// Traces every transaction in `block` with the prestateTracer. See
388    /// [`Self::trace_block_calls`] for the state/config-reuse semantics.
389    pub fn trace_block_prestate(
390        db: &mut GeneralizedDatabase,
391        block: &Block,
392        diff_mode: bool,
393        include_empty: bool,
394        vm_type: VMType,
395        crypto: &dyn Crypto,
396    ) -> Result<Vec<(H256, PrestateResult)>, EvmError> {
397        Self::rerun_block(db, block, Some(0), vm_type, crypto, None)?;
398        let (config, chain_id, base_blob_fee) = block_trace_env_config(db, &block.header)?;
399        let mut traces = Vec::with_capacity(block.body.transactions.len());
400        for (tx, sender) in block
401            .body
402            .get_transactions_with_sender(crypto)
403            .map_err(|e| EvmError::Transaction(e.to_string()))?
404        {
405            let env = Self::setup_env_with_config(
406                tx,
407                sender,
408                &block.header,
409                config,
410                chain_id,
411                vm_type,
412                base_blob_fee,
413            )?;
414            let trace =
415                Self::run_prestate_trace(db, env, tx, diff_mode, include_empty, vm_type, crypto)?;
416            traces.push((tx.hash(crypto), trace));
417        }
418        Ok(traces)
419    }
420
421    /// Traces every transaction in `block` with the opcode (EIP-3155) tracer. See
422    /// [`Self::trace_block_calls`] for the state/config-reuse semantics.
423    pub fn trace_block_opcodes(
424        db: &mut GeneralizedDatabase,
425        block: &Block,
426        cfg: OpcodeTracerConfig,
427        vm_type: VMType,
428        crypto: &dyn Crypto,
429    ) -> Result<Vec<(H256, OpcodeTraceResult)>, EvmError> {
430        Self::rerun_block(db, block, Some(0), vm_type, crypto, None)?;
431        let (config, chain_id, base_blob_fee) = block_trace_env_config(db, &block.header)?;
432        let mut traces = Vec::with_capacity(block.body.transactions.len());
433        for (tx, sender) in block
434            .body
435            .get_transactions_with_sender(crypto)
436            .map_err(|e| EvmError::Transaction(e.to_string()))?
437        {
438            let env = Self::setup_env_with_config(
439                tx,
440                sender,
441                &block.header,
442                config,
443                chain_id,
444                vm_type,
445                base_blob_fee,
446            )?;
447            let trace = Self::run_opcode_trace(db, env, tx, cfg.clone(), vm_type, crypto)?;
448            traces.push((tx.hash(crypto), trace));
449        }
450        Ok(traces)
451    }
452}
453
454/// Recursively counts the logs captured in a call frame and all its subcalls — the
455/// number of `withLog` logs a traced tx contributes to the block-absolute log index.
456fn count_call_logs(frame: &CallTraceFrame) -> u64 {
457    let own = u64::try_from(frame.logs.len()).unwrap_or(u64::MAX);
458    frame.calls.iter().fold(own, |acc, subcall| {
459        acc.saturating_add(count_call_logs(subcall))
460    })
461}
462
463/// Computes the block-invariant `(EVMConfig, chain_id, base_blob_fee)` once so a
464/// whole-block trace can reuse them across transactions instead of recomputing per tx.
465fn block_trace_env_config(
466    db: &GeneralizedDatabase,
467    header: &BlockHeader,
468) -> Result<(EVMConfig, u64, U256), EvmError> {
469    let chain_config = db.store.get_chain_config()?;
470    let config = EVMConfig::new_from_chain_config(&chain_config, header);
471    let base_blob_fee = get_base_fee_per_blob_gas(header.excess_blob_gas, &config)?;
472    Ok((config, chain_config.chain_id, base_blob_fee))
473}
474
475/// Builds the call-style [`Environment`] and concrete [`Transaction`] for tracing a
476/// synthetic `eth_call`-shaped request (`debug_traceCall`). Mirrors
477/// `simulate_tx_from_generic`: the sender is taken from `tx.from` (no signature
478/// recovery), the block gas-allowance check is skipped, and the base fee is relaxed when
479/// no gas price is provided.
480fn prepare_call_env(
481    tx: &GenericTransaction,
482    block_header: &BlockHeader,
483    db: &GeneralizedDatabase,
484    vm_type: VMType,
485) -> Result<(Environment, Transaction), EvmError> {
486    // `env_from_generic` relaxes the allowance check without touching `block_gas_limit`:
487    // a trace must report the same GASLIMIT the traced block executed with.
488    let mut env = env_from_generic(tx, block_header, db, vm_type)?;
489    adjust_disabled_base_fee(&mut env);
490    let converted_tx = generic_tx_to_transaction(tx)?;
491    Ok((env, converted_tx))
492}
493
494/// Returns `(address, pre_account, post_account)` for every account in `post_cache`.
495/// `pre_account` comes from `pre_snapshot` if cached before the tx, otherwise from
496/// `initial_accounts_state`. Filtering unchanged accounts is the caller's job.
497fn find_touched_accounts<'a>(
498    pre_snapshot: &'a CacheDB,
499    post_cache: &'a CacheDB,
500    db: &'a GeneralizedDatabase,
501) -> Vec<(Address, &'a LevmAccount, &'a LevmAccount)> {
502    let mut touched = Vec::new();
503
504    for (addr, post_account) in post_cache {
505        let pre_account = match pre_snapshot.get(addr) {
506            Some(pre) => pre,
507            None => {
508                let Some(initial) = db.initial_accounts_state.get(addr) else {
509                    continue;
510                };
511                initial
512            }
513        };
514
515        touched.push((*addr, pre_account, post_account));
516    }
517
518    touched
519}
520
521/// Reads code from `db.codes`; caller must `preload_touched_codes` first.
522/// Storage values are passed through as-is (including zero); per-field filtering
523/// for diff-mode post is applied by `build_post_output`.
524fn build_account_output(
525    account: &LevmAccount,
526    db: &GeneralizedDatabase,
527) -> Result<PrestateAccountState, EvmError> {
528    let has_code = account.info.code_hash != *EMPTY_KECCAK_HASH;
529    let code = if has_code {
530        get_preloaded_code(db, &account.info.code_hash)?
531    } else {
532        bytes::Bytes::new()
533    };
534    let code_hash = if has_code {
535        account.info.code_hash
536    } else {
537        H256::zero()
538    };
539
540    let storage = account
541        .storage
542        .iter()
543        .map(|(k, v)| (*k, H256::from_uint(v)))
544        .collect();
545
546    Ok(PrestateAccountState {
547        balance: Some(account.info.balance),
548        nonce: account.info.nonce,
549        code,
550        code_hash,
551        storage,
552    })
553}
554
555/// Returns the bytecode for `hash`; caller must `preload_touched_codes` first.
556fn get_preloaded_code(db: &GeneralizedDatabase, hash: &H256) -> Result<bytes::Bytes, EvmError> {
557    db.codes
558        .get(hash)
559        .map(|c| c.code_bytes())
560        .ok_or_else(|| EvmError::Custom(format!("missing preloaded code for {hash:?}")))
561}
562
563/// Builds the diff-mode post entry for a touched account, emitting only fields whose
564/// value differs from the pre-tx state. Storage entries are limited to slots that
565/// actually changed and have a non-zero post value. Returns `None` if nothing changed.
566fn build_post_output(
567    addr: Address,
568    pre_account: &LevmAccount,
569    post_account: &LevmAccount,
570    pre_snapshot: &CacheDB,
571    db: &GeneralizedDatabase,
572) -> Result<Option<PrestateAccountState>, EvmError> {
573    let mut state = PrestateAccountState::default();
574    let mut modified = false;
575
576    if pre_account.info.balance != post_account.info.balance {
577        state.balance = Some(post_account.info.balance);
578        modified = true;
579    }
580    if pre_account.info.nonce != post_account.info.nonce {
581        state.nonce = post_account.info.nonce;
582        modified = true;
583    }
584    if pre_account.info.code_hash != post_account.info.code_hash {
585        if post_account.info.code_hash != *EMPTY_KECCAK_HASH {
586            state.code_hash = post_account.info.code_hash;
587            state.code = get_preloaded_code(db, &post_account.info.code_hash)?;
588        }
589        modified = true;
590    }
591
592    for (key, post_val) in &post_account.storage {
593        let pre_val = pre_storage_value(addr, key, pre_snapshot, db).unwrap_or_default();
594        if pre_val == *post_val {
595            continue;
596        }
597        modified = true;
598        // Cleared slots (post == 0) are encoded by absence in `post.storage`.
599        if !post_val.is_zero() {
600            state.storage.insert(*key, H256::from_uint(post_val));
601        }
602    }
603
604    Ok(modified.then_some(state))
605}
606
607/// Resolves the pre-tx value of `slot` for `addr`. Slots accessed in earlier txs are in
608/// `pre_snapshot`; slots first loaded in this tx live only in `initial_accounts_state`.
609fn pre_storage_value(
610    addr: Address,
611    slot: &H256,
612    pre_snapshot: &CacheDB,
613    db: &GeneralizedDatabase,
614) -> Option<U256> {
615    if let Some(account) = pre_snapshot.get(&addr)
616        && let Some(value) = account.storage.get(slot)
617    {
618        return Some(*value);
619    }
620    db.initial_accounts_state
621        .get(&addr)
622        .and_then(|a| a.storage.get(slot).copied())
623}
624
625/// Builds the pre-tx state map. Pre storage is restricted to slots accessed by THIS
626/// tx — for accounts cached before this tx that means slots first loaded here or slots
627/// whose value changed; for accounts first accessed here, every slot in `post.storage`.
628/// The final `post.storage` membership check is a defensive guard: it bounds pre to
629/// the set of slots that ended up in the post cache, so unrelated slots that ever leak
630/// into `initial_accounts_state` (e.g. via more eager caching upstream) cannot leak into
631/// pre output.
632fn build_pre_state_map(
633    pre_snapshot: &CacheDB,
634    post_cache: &CacheDB,
635    db: &GeneralizedDatabase,
636) -> Result<PrestateTrace, EvmError> {
637    let mut result = PrestateTrace::new();
638
639    for (addr, pre_account, post_account) in find_touched_accounts(pre_snapshot, post_cache, db) {
640        let mut state = build_account_output(pre_account, db)?;
641
642        // For already-cached accounts, the pre-tx values of slots first loaded in this
643        // tx live in `initial_accounts_state` rather than in `pre_snapshot`. Newly-accessed
644        // accounts already have those values via `pre_account` (which comes from
645        // `initial_accounts_state` in `find_touched_accounts`).
646        if pre_snapshot.contains_key(&addr)
647            && let Some(initial) = db.initial_accounts_state.get(&addr)
648        {
649            for (k, v) in &initial.storage {
650                state
651                    .storage
652                    .entry(*k)
653                    .or_insert_with(|| H256::from_uint(v));
654            }
655        }
656
657        let pre_cached_storage = pre_snapshot.get(&addr).map(|a| &a.storage);
658        state.storage.retain(|k, _| {
659            if !post_account.storage.contains_key(k) {
660                return false;
661            }
662            match pre_cached_storage {
663                Some(pre) if pre.contains_key(k) => pre.get(k) != post_account.storage.get(k),
664                _ => true,
665            }
666        });
667
668        result.insert(addr, state);
669    }
670
671    Ok(result)
672}
673
674/// Loads code into `db.codes` for every touched contract whose code wasn't executed
675/// (SELFDESTRUCT beneficiaries, plain-value transfer recipients) — without this they'd
676/// serialize as `code: 0x` despite a non-empty `code_hash`.
677fn preload_touched_codes(
678    pre_snapshot: &CacheDB,
679    db: &mut GeneralizedDatabase,
680) -> Result<(), EvmError> {
681    let hashes: Vec<H256> = db
682        .current_accounts_state
683        .iter()
684        .flat_map(|(addr, post)| {
685            let pre_hash = pre_snapshot
686                .get(addr)
687                .or_else(|| db.initial_accounts_state.get(addr))
688                .map(|a| a.info.code_hash)
689                .unwrap_or_default();
690            [post.info.code_hash, pre_hash]
691        })
692        .filter(|h| *h != *EMPTY_KECCAK_HASH)
693        .collect();
694
695    for hash in hashes {
696        db.get_code(hash)?;
697    }
698    Ok(())
699}
700
701/// Builds the diff-mode post map and the set of modified-or-destroyed addresses
702/// (used to prune diff `pre`) in a single pass.
703fn build_post_state_map(
704    pre_snapshot: &CacheDB,
705    post_cache: &CacheDB,
706    db: &GeneralizedDatabase,
707) -> Result<(PrestateTrace, std::collections::HashSet<Address>), EvmError> {
708    let mut post = PrestateTrace::new();
709    let mut modified_or_destroyed = std::collections::HashSet::new();
710
711    for (addr, pre_account, post_account) in find_touched_accounts(pre_snapshot, post_cache, db) {
712        if matches!(
713            post_account.status,
714            AccountStatus::Destroyed | AccountStatus::DestroyedModified,
715        ) {
716            modified_or_destroyed.insert(addr);
717            continue;
718        }
719
720        if let Some(state) = build_post_output(addr, pre_account, post_account, pre_snapshot, db)? {
721            modified_or_destroyed.insert(addr);
722            post.insert(addr, state);
723        }
724    }
725
726    Ok((post, modified_or_destroyed))
727}
728
729/// Trims storage entries in a diff-mode pre map: drops slots whose pre value is zero
730/// or whose pre value equals the post value (unchanged in this tx).
731fn filter_diff_pre_storage(pre: &mut PrestateTrace, post_cache: &CacheDB) {
732    for (addr, state) in pre.iter_mut() {
733        let post_storage = post_cache.get(addr).map(|a| &a.storage);
734        state.storage.retain(|k, v| {
735            if v.is_zero() {
736                return false;
737            }
738            let post_val = post_storage
739                .and_then(|s| s.get(k).copied())
740                .unwrap_or_default();
741            *v != H256::from_uint(&post_val)
742        });
743    }
744}