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