Skip to main content

evm_fork_cache/cache/
overlay.rs

1//! Per-simulation state overlays layered over a snapshot or the live cache.
2//!
3//! An [`EvmOverlay`] wraps a read-only base (an
4//! [`EvmSnapshot`] or the cache itself) with
5//! a scratch write layer, so a simulation can mutate balances, storage, and code
6//! and run calls without disturbing the base or other overlays. Overlays are the
7//! `Send` unit of parallel fan-out: snapshot once, clone cheaply, overlay per
8//! candidate. Reads fall through the write layer to the base; writes and reverts
9//! stay local to the overlay.
10
11use std::cell::RefCell;
12use std::collections::HashMap;
13use std::rc::Rc;
14use std::sync::Arc;
15
16use alloy_eips::eip2930::{AccessList, AccessListItem};
17use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
18use foundry_fork_db::{DatabaseError, SharedBackend};
19use revm::{
20    Context, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainBuilder, MainContext,
21    context::{BlockEnv, CfgEnv, Journal, LocalContext, TxEnv, result::ExecutionResult},
22    database_interface::{Database, DatabaseRef},
23    state::{AccountInfo, Bytecode},
24};
25
26use super::snapshot::EvmSnapshot;
27use super::{CallSimulationResult, SimStatus, TxConfig, unix_timestamp_secs_saturating};
28use crate::access_set::StorageAccessList;
29use crate::bundle::{BundleOptions, BundleResult, BundleTx, RevertPolicy, TxOutcome};
30use crate::errors::{
31    OverlayError, OverlayResult as Result, SimError, SimHostError, SimulationError,
32    SimulationResult,
33};
34use crate::inspector::TransferInspector;
35
36type OverlayEvm<'a> = revm::MainnetEvm<
37    Context<BlockEnv, TxEnv, CfgEnv, &'a mut EvmOverlay, Journal<&'a mut EvmOverlay>, ()>,
38>;
39
40type InspectorOverlayEvm<'a, INSP> = revm::MainnetEvm<
41    Context<BlockEnv, TxEnv, CfgEnv, &'a mut EvmOverlay, Journal<&'a mut EvmOverlay>, ()>,
42    INSP,
43>;
44
45/// Per-simulation mutable overlay on an immutable snapshot.
46///
47/// Lookup order: dirty layer → snapshot → ext_db (optional RPC fallback).
48///
49/// This type is `Send` (unlike `EvmCache`) because it uses no `Rc`/`RefCell`.
50/// Each simulation task gets its own `EvmOverlay` with a cheap `Arc::clone`
51/// of the shared `EvmSnapshot`.
52///
53/// # Reuse across simulations (Pillar A.2)
54///
55/// A worker doing many sims against the same snapshot can call [`Self::new`]
56/// once and [`Self::reset`] between sims instead of allocating a fresh overlay
57/// each time. The reusable shared-memory buffer is also recycled across calls —
58/// see [`Self::call_raw`] — without making the overlay `!Send`.
59pub struct EvmOverlay {
60    snapshot: Arc<EvmSnapshot>,
61    /// Per-simulation mutations (accounts fetched from ext_db, committed changes).
62    dirty_accounts: HashMap<Address, AccountInfo>,
63    /// Per-simulation storage mutations.
64    dirty_storage: HashMap<Address, HashMap<U256, U256>>,
65    /// Optional RPC fallback for data not in snapshot.
66    ext_db: Option<SharedBackend>,
67    /// Reusable shared-memory buffer, recycled across the build→transact→revert
68    /// call methods to avoid reallocating a 64 KB `Vec` per call.
69    ///
70    /// Stored as a plain `Vec<u8>` (not an `Rc`) so the overlay stays `Send`. A
71    /// call method `mem::take`s it, wraps it in a method-local `Rc<RefCell<_>>`
72    /// for revm's [`LocalContext`], runs, then reclaims and clears it after the
73    /// EVM is dropped (see [`Self::build_evm_with_local`]).
74    reusable_buffer: Vec<u8>,
75    /// Target pre-allocation (bytes) for [`Self::reusable_buffer`] and each
76    /// per-call buffer, taken from the snapshot's configured
77    /// [`SharedMemoryCapacity`](super::SharedMemoryCapacity) so overlays honor the
78    /// capacity set on the originating [`EvmCache`].
79    buffer_capacity: usize,
80    /// Set when a `BLOCKHASH` read fell through to the ZERO fallback (no
81    /// snapshot-provided hash and no `ext_db`). The freshness validator reads
82    /// this via [`Self::blockhash_zero_fallback`] to fail closed instead of
83    /// confirming a sim whose control flow may rest on a hash its overlays
84    /// cannot resolve. Cleared by [`Self::reset`].
85    blockhash_zero_fallback: bool,
86}
87
88impl EvmOverlay {
89    /// Create a new overlay on the given snapshot.
90    ///
91    /// The reusable shared-memory buffer is pre-allocated to the snapshot's
92    /// configured shared-memory capacity (see
93    /// [`SharedMemoryCapacity`](super::SharedMemoryCapacity)).
94    pub fn new(snapshot: Arc<EvmSnapshot>, ext_db: Option<SharedBackend>) -> Self {
95        let buffer_capacity = snapshot.shared_memory_capacity;
96        Self {
97            snapshot,
98            dirty_accounts: HashMap::new(),
99            dirty_storage: HashMap::new(),
100            ext_db,
101            reusable_buffer: Vec::with_capacity(buffer_capacity),
102            buffer_capacity,
103            blockhash_zero_fallback: false,
104        }
105    }
106
107    /// Clear the per-simulation dirty layer so this overlay can be reused for the
108    /// next simulation against the same snapshot, without reallocating (Pillar
109    /// A.2).
110    ///
111    /// A worker doing K sims calls [`Self::new`] once and `reset()` between sims
112    /// instead of allocating a fresh overlay (plus dirty maps plus an `Arc`
113    /// clone) each time. After `reset()` the overlay reads the pristine snapshot
114    /// again — it is exactly equivalent to a freshly-built overlay on the same
115    /// snapshot. The snapshot `Arc`, the optional `ext_db`, and the reusable
116    /// shared-memory buffer (kept at capacity) are retained.
117    pub fn reset(&mut self) {
118        self.dirty_accounts.clear();
119        self.dirty_storage.clear();
120        self.blockhash_zero_fallback = false;
121        // Keep: snapshot Arc, ext_db, and the reusable buffer. The buffer is
122        // already cleared after each call, so nothing to do for it here.
123    }
124
125    /// `true` if any `BLOCKHASH` read on this overlay fell through to the ZERO
126    /// fallback (no snapshot-provided hash for that number and no `ext_db`)
127    /// since construction or the last [`reset`](Self::reset).
128    ///
129    /// The freshness validator uses this to **fail closed**: a sim that read a
130    /// hash its ext-db-less overlays cannot resolve is reported
131    /// [`Unverified`](crate::freshness::Validation::Unverified) rather than
132    /// silently confirmed against a ZERO stand-in.
133    ///
134    /// Only reads revm actually routes to the database can set this: requests
135    /// outside the EVM's valid lookback window (`[current − 256, current)`)
136    /// return spec-mandated ZERO without a database call — that value is
137    /// correct on-chain too, so such reads are deliberately not flagged.
138    pub fn blockhash_zero_fallback(&self) -> bool {
139        self.blockhash_zero_fallback
140    }
141
142    /// Chain ID of the block context captured by the underlying snapshot.
143    ///
144    /// This is the value installed into `cfg.chain_id` by [`Self::build_evm`].
145    pub fn chain_id(&self) -> u64 {
146        self.snapshot.chain_id
147    }
148
149    /// Block number of the snapshot's block context, or `None` if it was not
150    /// captured.
151    ///
152    /// When present this is the `block.number` simulations run against; when
153    /// `None`, [`Self::build_evm`] leaves revm's default block number in place.
154    pub fn block_number(&self) -> Option<u64> {
155        self.snapshot.block_number
156    }
157
158    /// Base fee of the snapshot's block context, or `None` if it was not
159    /// captured.
160    ///
161    /// Note that base-fee checks are disabled in the simulation EVM, so this is
162    /// informational rather than enforced against the transaction.
163    pub fn basefee(&self) -> Option<u64> {
164        self.snapshot.basefee
165    }
166
167    /// Timestamp of the snapshot's block context, or `None` if it was not
168    /// captured.
169    ///
170    /// When `None`, [`Self::build_evm`] substitutes the current wall-clock time
171    /// for `block.timestamp`.
172    pub fn timestamp(&self) -> Option<u64> {
173        self.snapshot.timestamp
174    }
175
176    /// A fresh [`LocalContext`] with a newly-allocated 64 KB shared-memory buffer.
177    ///
178    /// Used by the public [`Self::build_evm`], which hands out the EVM and cannot
179    /// reclaim its buffer afterwards. The internal call methods instead recycle
180    /// [`Self::reusable_buffer`] via [`Self::build_evm_with_local`].
181    fn fresh_local(&self) -> LocalContext {
182        LocalContext {
183            shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(self.buffer_capacity))),
184            precompile_error_message: None,
185        }
186    }
187
188    /// Build a revm EVM instance backed by this overlay, using a caller-supplied
189    /// [`LocalContext`].
190    ///
191    /// This is the shared body behind [`Self::build_evm`] and the internal call
192    /// methods. The call methods pass a `local` wrapping the recycled
193    /// [`Self::reusable_buffer`] (Pillar A.2) and reclaim it after the EVM is
194    /// dropped; [`Self::build_evm`] passes a fresh one.
195    ///
196    /// Note: the returned EVM is `!Send` (due to `LocalContext`'s `Rc<RefCell>`),
197    /// but this is fine because it's created and used within a single task.
198    fn build_evm_with_local(&mut self, local: LocalContext) -> OverlayEvm<'_> {
199        // Read snapshot values before the mutable borrow of self
200        let chain_id = self.snapshot.chain_id;
201        let spec_id = self.snapshot.spec_id;
202        let timestamp = self
203            .snapshot
204            .timestamp
205            .unwrap_or_else(|| unix_timestamp_secs_saturating(std::time::SystemTime::now()));
206        let block_number = self.snapshot.block_number;
207        let basefee = self.snapshot.basefee;
208        let coinbase = self.snapshot.coinbase;
209        let prevrandao = self.snapshot.prevrandao;
210        let gas_limit = self.snapshot.gas_limit;
211
212        let mut evm = Context::mainnet()
213            .with_db(&mut *self)
214            .with_local(local)
215            .modify_cfg_chained(|cfg| {
216                cfg.disable_nonce_check = true;
217                cfg.disable_eip3607 = true;
218                cfg.disable_base_fee = true;
219                cfg.disable_balance_check = true;
220                cfg.chain_id = chain_id;
221                cfg.limit_contract_code_size = None;
222                cfg.tx_chain_id_check = false;
223                cfg.spec = spec_id;
224            })
225            .build_mainnet();
226
227        evm.block.timestamp = U256::from(timestamp);
228        if let Some(number) = block_number {
229            evm.block.number = U256::from(number);
230        }
231        if let Some(basefee) = basefee {
232            evm.block.basefee = basefee;
233        }
234        if let Some(coinbase) = coinbase {
235            evm.block.beneficiary = coinbase;
236        }
237        if let Some(prevrandao) = prevrandao {
238            evm.block.prevrandao = Some(prevrandao);
239        }
240        if let Some(gas_limit) = gas_limit {
241            evm.block.gas_limit = gas_limit;
242        }
243        evm
244    }
245
246    /// Build a revm EVM instance backed by this overlay.
247    ///
248    /// This allocates a fresh 64 KB shared-memory buffer each call: it hands the
249    /// EVM out to the caller and cannot reclaim the buffer afterwards, so it
250    /// cannot recycle the overlay's reusable buffer. The internal call methods
251    /// ([`Self::call_raw`], etc.) recycle the buffer instead (Pillar A.2).
252    ///
253    /// Note: The returned EVM is `!Send` (due to `LocalContext`'s `Rc<RefCell>`),
254    /// but this is fine because it's created and used within a single task.
255    pub fn build_evm(&mut self) -> OverlayEvm<'_> {
256        let local = self.fresh_local();
257        self.build_evm_with_local(local)
258    }
259
260    /// Execute a non-committing call and return the raw [`ExecutionResult`].
261    ///
262    /// The EVM state is reverted to a checkpoint after execution on *both*
263    /// success and failure, so the call never mutates this overlay's dirty
264    /// layer. Each overlay simulation is therefore isolated: repeated calls all
265    /// observe the same base state.
266    ///
267    /// A revert or halt is *not* an error here — it is reported through the
268    /// returned [`ExecutionResult`] variant. Only failure to build or transact
269    /// the call yields `Err`.
270    ///
271    /// # Errors
272    ///
273    /// Returns an error if the [`TxEnv`] cannot be built from the given inputs,
274    /// or if revm fails to transact the call (for example a database error
275    /// while loading state from the RPC fallback).
276    ///
277    /// # Examples
278    ///
279    /// ```no_run
280    /// # use std::sync::Arc;
281    /// # use alloy_primitives::{Address, Bytes};
282    /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
283    /// # fn run(snapshot: Arc<EvmSnapshot>) -> Result<(), Box<dyn std::error::Error>> {
284    /// let mut overlay = EvmOverlay::new(snapshot, None);
285    /// let result = overlay.call_raw(Address::ZERO, Address::ZERO, Bytes::new())?;
286    /// // State is reverted; a second call sees the same base state.
287    /// let _again = overlay.call_raw(Address::ZERO, Address::ZERO, Bytes::new())?;
288    /// # let _ = result;
289    /// # Ok(())
290    /// # }
291    /// ```
292    pub fn call_raw(
293        &mut self,
294        from: Address,
295        to: Address,
296        calldata: Bytes,
297    ) -> Result<ExecutionResult> {
298        let tx = TxEnv::builder()
299            .caller(from)
300            .kind(TxKind::Call(to))
301            .data(calldata)
302            .value(U256::ZERO)
303            .build()
304            .map_err(OverlayError::tx_env)?;
305
306        // Recycle the reusable buffer (Pillar A.2): take it out as a plain Vec
307        // (keeping the overlay Send), lend it to a method-local Rc<RefCell> for
308        // revm's LocalContext, then reclaim and clear it after the EVM is dropped.
309        let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
310        let local = LocalContext {
311            shared_memory_buffer: Rc::clone(&buffer),
312            precompile_error_message: None,
313        };
314
315        let result = {
316            let mut evm = self.build_evm_with_local(local);
317            use revm::context_interface::JournalTr;
318            let checkpoint = evm.journaled_state.checkpoint();
319            let result = evm.transact_one(tx).map_err(OverlayError::transact);
320            evm.journaled_state.checkpoint_revert(checkpoint);
321            result
322        };
323
324        self.reclaim_buffer(buffer);
325        result
326    }
327
328    /// Reclaim the recycled shared-memory buffer after the EVM (and its
329    /// `LocalContext` clone of the `Rc`) has been dropped, clearing it for the
330    /// next call.
331    ///
332    /// The `Rc` was only ever held by the dropped EVM and this method's local, so
333    /// `try_unwrap` succeeds in the normal path. If a panic somewhere left an
334    /// extra strong reference the buffer is simply re-allocated next call — no
335    /// correctness impact.
336    fn reclaim_buffer(&mut self, buffer: Rc<RefCell<Vec<u8>>>) {
337        if let Ok(cell) = Rc::try_unwrap(buffer) {
338            let mut buf = cell.into_inner();
339            buf.clear();
340            self.reusable_buffer = buf;
341        } else {
342            self.reusable_buffer = Vec::with_capacity(self.buffer_capacity);
343        }
344    }
345
346    /// Build a revm EVM instance with an inspector, backed by this overlay, using
347    /// a caller-supplied [`LocalContext`].
348    ///
349    /// Like [`Self::build_evm_with_local`] but attaches `inspector`. The call
350    /// methods pass a `local` wrapping the recycled [`Self::reusable_buffer`]
351    /// (Pillar A.2) and reclaim it after the EVM is dropped.
352    fn build_evm_with_inspector_local<INSP>(
353        &mut self,
354        inspector: INSP,
355        local: LocalContext,
356    ) -> InspectorOverlayEvm<'_, INSP> {
357        let chain_id = self.snapshot.chain_id;
358        let spec_id = self.snapshot.spec_id;
359        let timestamp = self
360            .snapshot
361            .timestamp
362            .unwrap_or_else(|| unix_timestamp_secs_saturating(std::time::SystemTime::now()));
363        let block_number = self.snapshot.block_number;
364        let basefee = self.snapshot.basefee;
365        let coinbase = self.snapshot.coinbase;
366        let prevrandao = self.snapshot.prevrandao;
367        let gas_limit = self.snapshot.gas_limit;
368
369        let mut evm = Context::mainnet()
370            .with_db(&mut *self)
371            .with_local(local)
372            .modify_cfg_chained(|cfg| {
373                cfg.disable_nonce_check = true;
374                cfg.disable_eip3607 = true;
375                cfg.disable_base_fee = true;
376                cfg.disable_balance_check = true;
377                cfg.chain_id = chain_id;
378                cfg.limit_contract_code_size = None;
379                cfg.tx_chain_id_check = false;
380                cfg.spec = spec_id;
381            })
382            .build_mainnet_with_inspector(inspector);
383
384        evm.block.timestamp = U256::from(timestamp);
385        if let Some(number) = block_number {
386            evm.block.number = U256::from(number);
387        }
388        if let Some(basefee) = basefee {
389            evm.block.basefee = basefee;
390        }
391        if let Some(coinbase) = coinbase {
392            evm.block.beneficiary = coinbase;
393        }
394        if let Some(prevrandao) = prevrandao {
395            evm.block.prevrandao = Some(prevrandao);
396        }
397        if let Some(gas_limit) = gas_limit {
398            evm.block.gas_limit = gas_limit;
399        }
400        evm
401    }
402
403    /// Simulate a call with transfer tracking via the `TransferInspector`.
404    ///
405    /// This is the overlay-compatible equivalent of
406    /// [`super::EvmCache::simulate_with_transfer_tracking`]. It captures ERC20
407    /// Transfer events during execution to compute balance deltas for `owner`
408    /// (restricted to `tokens` when provided) without relying on pre/post
409    /// balance queries.
410    ///
411    /// On a reverting or halting call the EVM state is reverted to a checkpoint
412    /// before returning, so a failed simulation never mutates this overlay. On
413    /// success the call either commits the journaled changes into the overlay's
414    /// dirty layer (`commit == true`) or reverts them (`commit == false`); a
415    /// non-committing run leaves each overlay simulation isolated from the next.
416    ///
417    /// # Errors
418    ///
419    /// Returns an error if the [`TxEnv`] cannot be built, if revm fails to
420    /// transact the call, if the call reverts (mapped from the revert payload),
421    /// or if the call halts. In every error case the EVM state is reverted
422    /// first, regardless of `commit`.
423    ///
424    /// # Examples
425    ///
426    /// ```no_run
427    /// # use std::sync::Arc;
428    /// # use alloy_primitives::{Address, Bytes};
429    /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
430    /// # fn run(snapshot: Arc<EvmSnapshot>, token: Address, owner: Address) -> Result<(), Box<dyn std::error::Error>> {
431    /// let mut overlay = EvmOverlay::new(snapshot, None);
432    /// let sim = overlay.simulate_with_transfer_tracking(
433    ///     owner,
434    ///     token,
435    ///     Bytes::new(),
436    ///     owner,
437    ///     Some([token]),
438    ///     false, // non-committing: state is reverted afterwards
439    /// )?;
440    /// let _delta = sim.token_deltas.get(&token);
441    /// # Ok(())
442    /// # }
443    /// ```
444    pub fn simulate_with_transfer_tracking(
445        &mut self,
446        from: Address,
447        to: Address,
448        calldata: Bytes,
449        owner: Address,
450        tokens: Option<impl IntoIterator<Item = Address>>,
451        commit: bool,
452    ) -> SimulationResult<CallSimulationResult> {
453        let tx = TxEnv::builder()
454            .caller(from)
455            .kind(TxKind::Call(to))
456            .data(calldata)
457            .value(U256::ZERO)
458            .build()
459            .map_err(|e| SimError::Other(SimHostError::tx_env(e)))?;
460
461        let inspector = TransferInspector::new();
462
463        // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
464        let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
465        let local = LocalContext {
466            shared_memory_buffer: Rc::clone(&buffer),
467            precompile_error_message: None,
468        };
469
470        let outcome = {
471            let mut evm = self.build_evm_with_inspector_local(inspector, local);
472
473            use revm::context_interface::JournalTr;
474            let checkpoint = evm.journaled_state.checkpoint();
475
476            let result = evm
477                .inspect_one_tx(tx)
478                .map_err(|e| SimError::Other(SimHostError::transact(e)));
479
480            match result {
481                Ok(ExecutionResult::Success {
482                    logs,
483                    gas_used,
484                    output,
485                    ..
486                }) => {
487                    let token_deltas = if let Some(token_list) = tokens {
488                        evm.inspector.balance_deltas_for_tokens(owner, token_list)
489                    } else {
490                        evm.inspector.balance_deltas(owner)
491                    };
492
493                    // Extract EIP-2930 access list from journaled state
494                    let access_list = extract_access_list(&evm.journaled_state.state);
495
496                    if commit {
497                        evm.commit_inner();
498                    } else {
499                        evm.journaled_state.checkpoint_revert(checkpoint);
500                    }
501
502                    Ok(CallSimulationResult {
503                        status: SimStatus::Success,
504                        gas_used,
505                        token_deltas,
506                        logs,
507                        access_list,
508                        output: output.into_data(),
509                    })
510                }
511                Ok(ExecutionResult::Revert { gas_used, output }) => {
512                    evm.journaled_state.checkpoint_revert(checkpoint);
513                    Err(SimulationError::from_revert(gas_used, output).into())
514                }
515                Ok(ExecutionResult::Halt { reason, gas_used }) => {
516                    evm.journaled_state.checkpoint_revert(checkpoint);
517                    Err(SimError::Halt {
518                        reason: format!("{reason:?}"),
519                        gas_used,
520                    })
521                }
522                Err(err) => {
523                    evm.journaled_state.checkpoint_revert(checkpoint);
524                    Err(err)
525                }
526            }
527        };
528
529        self.reclaim_buffer(buffer);
530        outcome
531    }
532
533    /// Run a single call with a caller-supplied [`Inspector`](revm::Inspector),
534    /// returning the raw [`ExecutionResult`] and handing the inspector back for the
535    /// caller to read.
536    ///
537    /// This is the inspector-generic public seam: where
538    /// [`Self::simulate_with_transfer_tracking`] hard-wires the
539    /// [`TransferInspector`], this accepts any
540    /// [`revm::Inspector`] — a [`CallTracer`](crate::tracing::CallTracer), an
541    /// [`InspectorStack`](crate::tracing::InspectorStack) composing several, or a
542    /// caller-defined one. It honors a full [`TxConfig`] (value/gas/nonce/access
543    /// list) exactly like [`Self::call_raw_with_access_list_with`] and recycles the
544    /// reusable shared-memory buffer like the other call methods.
545    ///
546    /// Unlike `simulate_with_transfer_tracking`, a revert or halt is **not** an
547    /// error: the raw [`ExecutionResult`] variant
548    /// ([`Success`](ExecutionResult::Success) /
549    /// [`Revert`](ExecutionResult::Revert) / [`Halt`](ExecutionResult::Halt)) is
550    /// returned as `Ok` so the inspector's captured frames (e.g. a reverted call
551    /// tree) remain observable. Only a tx-env build failure or a transact/database
552    /// error yields `Err`.
553    ///
554    /// On a successful transact the journaled changes are either committed into the
555    /// overlay's dirty layer (`commit == true`) or reverted (`commit == false`),
556    /// matching [`Self::simulate_with_transfer_tracking`]. On a revert/halt the
557    /// checkpoint is always reverted regardless of `commit`, so a failed call never
558    /// mutates this overlay. On a transact error the checkpoint is reverted too.
559    ///
560    /// # Errors
561    ///
562    /// Returns an error if the [`TxEnv`] cannot be built from `from`/`to`/`tx`, or
563    /// if revm fails to transact the call (e.g. a database error while loading
564    /// state).
565    ///
566    /// # Examples
567    ///
568    /// ```no_run
569    /// # use std::sync::Arc;
570    /// # use alloy_primitives::{Address, Bytes};
571    /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot, TxConfig};
572    /// # use evm_fork_cache::CallTracer;
573    /// # fn run(snapshot: Arc<EvmSnapshot>, to: Address) -> Result<(), Box<dyn std::error::Error>> {
574    /// let mut overlay = EvmOverlay::new(snapshot, None);
575    /// let (result, tracer) = overlay.call_raw_with_inspector(
576    ///     Address::ZERO,
577    ///     to,
578    ///     Bytes::new(),
579    ///     &TxConfig::default(),
580    ///     CallTracer::new(),
581    ///     false,
582    /// )?;
583    /// let _ = result;
584    /// let _trace = tracer.into_trace();
585    /// # Ok(())
586    /// # }
587    /// ```
588    pub fn call_raw_with_inspector<I>(
589        &mut self,
590        from: Address,
591        to: Address,
592        calldata: Bytes,
593        tx: &TxConfig,
594        inspector: I,
595        commit: bool,
596    ) -> SimulationResult<(ExecutionResult, I)>
597    where
598        I: for<'a> revm::Inspector<
599                Context<
600                    BlockEnv,
601                    TxEnv,
602                    CfgEnv,
603                    &'a mut EvmOverlay,
604                    Journal<&'a mut EvmOverlay>,
605                    (),
606                >,
607            >,
608    {
609        let mut builder = TxEnv::builder()
610            .caller(from)
611            .kind(TxKind::Call(to))
612            .data(calldata)
613            .value(tx.value);
614        if let Some(gas_limit) = tx.gas_limit {
615            builder = builder.gas_limit(gas_limit);
616        }
617        if let Some(gas_price) = tx.gas_price {
618            builder = builder.gas_price(gas_price);
619        }
620        if let Some(nonce) = tx.nonce {
621            builder = builder.nonce(nonce);
622        }
623        if let Some(access_list) = &tx.access_list {
624            builder = builder.access_list(access_list.clone());
625        }
626        let tx_env = builder
627            .build()
628            .map_err(|e| SimError::Other(SimHostError::tx_env(e)))?;
629
630        // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
631        let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
632        let local = LocalContext {
633            shared_memory_buffer: Rc::clone(&buffer),
634            precompile_error_message: None,
635        };
636
637        let outcome = {
638            let mut evm = self.build_evm_with_inspector_local(inspector, local);
639
640            use revm::context_interface::JournalTr;
641            let checkpoint = evm.journaled_state.checkpoint();
642
643            match evm.inspect_one_tx(tx_env) {
644                Ok(result) => {
645                    if commit && matches!(result, ExecutionResult::Success { .. }) {
646                        evm.commit_inner();
647                    } else {
648                        evm.journaled_state.checkpoint_revert(checkpoint);
649                    }
650                    // Hand the inspector back to the caller.
651                    Ok((result, evm.inspector))
652                }
653                Err(e) => {
654                    evm.journaled_state.checkpoint_revert(checkpoint);
655                    Err(SimError::Other(SimHostError::transact(e)))
656                }
657            }
658        };
659
660        self.reclaim_buffer(buffer);
661        outcome
662    }
663
664    /// Apply `txs` in order against this overlay over **cumulative** block state,
665    /// with a revert policy and coinbase/miner-payment accounting (Phase 6
666    /// Track A+B).
667    ///
668    /// Each transaction observes the committed writes of the ones before it:
669    /// the bundle runs on a single overlay/EVM with one outer checkpoint plus a
670    /// per-transaction inner checkpoint, so it does **not** rebuild a fresh
671    /// overlay per transaction. See the [`bundle`](crate::bundle) module for the
672    /// public vocabulary ([`BundleTx`], [`BundleOptions`], [`RevertPolicy`],
673    /// [`TxOutcome`], [`BundleResult`]).
674    ///
675    /// # Revert policy
676    ///
677    /// - [`RevertPolicy::Atomic`]: the first transaction that reverts/halts
678    ///   rolls the whole bundle back to the outer checkpoint, sets
679    ///   `succeeded = false`, and stops (`per_tx` ends at the failing
680    ///   transaction). `coinbase_payment` is `0` and the overlay is unchanged.
681    /// - [`RevertPolicy::AllowReverts`]: a revert at a whitelisted index rolls
682    ///   back only that transaction (inner checkpoint) and execution continues;
683    ///   a revert at a non-whitelisted index behaves like `Atomic`.
684    ///
685    /// # Coinbase accounting
686    ///
687    /// `coinbase_payment` is the block beneficiary's balance delta across the kept
688    /// transactions. Under EIP-1559 revm credits the beneficiary only the priority
689    /// fee (`(effective_gas_price − basefee) × gas_used`) and burns the base fee
690    /// in-EVM, so the delta is the honest miner payment (plus any direct coinbase
691    /// tips). Saturating.
692    ///
693    /// # Commit semantics
694    ///
695    /// `opts.commit == true` folds the bundle's cumulative state into this
696    /// overlay's dirty layer (observable by subsequent overlay calls);
697    /// `false` reverts the outer checkpoint so the overlay is unchanged. A
698    /// failed atomic bundle never leaves partial state regardless of `commit`.
699    ///
700    /// # Errors
701    ///
702    /// Returns [`SimError`] if a transaction environment cannot be built or revm
703    /// fails to transact (e.g. a database error). A transaction *reverting* is
704    /// not an error — it is reported through the per-transaction
705    /// [`TxOutcome`] and the revert policy.
706    pub fn simulate_bundle(
707        &mut self,
708        txs: &[BundleTx],
709        opts: &BundleOptions,
710    ) -> SimulationResult<BundleResult> {
711        // Build every TxEnv up front so a build failure surfaces as an error
712        // before we touch the EVM/journal (and the borrow of `self` is clean).
713        let tx_envs: Vec<TxEnv> = txs
714            .iter()
715            .map(|bt| {
716                let mut builder = TxEnv::builder()
717                    .caller(bt.from)
718                    .kind(TxKind::Call(bt.to))
719                    .data(bt.calldata.clone())
720                    .value(bt.tx.value);
721                if let Some(gas_limit) = bt.tx.gas_limit {
722                    builder = builder.gas_limit(gas_limit);
723                }
724                if let Some(gas_price) = bt.tx.gas_price {
725                    builder = builder.gas_price(gas_price);
726                }
727                if let Some(nonce) = bt.tx.nonce {
728                    builder = builder.nonce(nonce);
729                }
730                if let Some(access_list) = &bt.tx.access_list {
731                    builder = builder.access_list(access_list.clone());
732                }
733                builder
734                    .build()
735                    .map_err(|e| SimError::Other(SimHostError::tx_env(e)))
736            })
737            .collect::<std::result::Result<_, _>>()?;
738
739        // Resolve the beneficiary and read its pre-bundle balance before the
740        // mutable borrow of `self` by the EVM (the post-bundle delta is the miner
741        // payment; revm already burns the base fee per EIP-1559).
742        let beneficiary = self
743            .snapshot
744            .coinbase
745            .unwrap_or_else(|| revm::context::BlockEnv::default().beneficiary);
746        let pre_beneficiary_balance = self
747            .basic(beneficiary)
748            .map_err(|e| SimError::Other(SimHostError::database(e)))?
749            .map(|info| info.balance)
750            .unwrap_or(U256::ZERO);
751
752        // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
753        let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
754        let local = LocalContext {
755            shared_memory_buffer: Rc::clone(&buffer),
756            precompile_error_message: None,
757        };
758
759        let outcome = {
760            use revm::context_interface::JournalTr;
761            let mut evm = self.build_evm_with_local(local);
762
763            // Outer checkpoint: the whole-bundle savepoint.
764            let outer = evm.journaled_state.checkpoint();
765
766            let mut per_tx: Vec<TxOutcome> = Vec::with_capacity(tx_envs.len());
767            let mut total_gas: u64 = 0;
768            let mut aborted = false;
769
770            'bundle: for (idx, tx_env) in tx_envs.into_iter().enumerate() {
771                // Inner checkpoint: this transaction's savepoint.
772                let inner = evm.journaled_state.checkpoint();
773                let result = match evm.transact_one(tx_env) {
774                    Ok(result) => result,
775                    Err(e) => {
776                        // Host/transact error: undo this tx and the whole bundle,
777                        // reclaim the buffer, and surface as SimError.
778                        evm.journaled_state.checkpoint_revert(inner);
779                        evm.journaled_state.checkpoint_revert(outer);
780                        drop(evm);
781                        self.reclaim_buffer(buffer);
782                        return Err(SimError::Other(SimHostError::transact(e)));
783                    }
784                };
785
786                let gas_used = result.gas_used();
787                let reverted = !result.is_success();
788                let logs = result.logs().to_vec();
789                total_gas = total_gas.saturating_add(gas_used);
790
791                per_tx.push(TxOutcome {
792                    result,
793                    gas_used,
794                    reverted,
795                    logs,
796                });
797
798                if reverted {
799                    let allowed = match &opts.revert_policy {
800                        RevertPolicy::Atomic => false,
801                        RevertPolicy::AllowReverts(idxs) => idxs.contains(&idx),
802                    };
803                    if allowed {
804                        // Roll back only this transaction; later txs still run.
805                        evm.journaled_state.checkpoint_revert(inner);
806                        continue 'bundle;
807                    } else {
808                        // Atomic abort: roll the whole bundle back and stop.
809                        evm.journaled_state.checkpoint_revert(outer);
810                        aborted = true;
811                        break 'bundle;
812                    }
813                }
814                // Successful tx: its effects stay journaled for the next tx.
815            }
816
817            // Partition total gas into successful/reverted buckets in a single
818            // pass. Saturating (consistent with `total_gas`); the invariant
819            // `successful_tx_gas + reverted_tx_gas == total_gas` holds by
820            // construction since every executed tx lands in exactly one bucket.
821            let (successful_tx_gas, reverted_tx_gas) =
822                per_tx.iter().fold((0u64, 0u64), |(succ, rev), tx| {
823                    if tx.reverted {
824                        (succ, rev.saturating_add(tx.gas_used))
825                    } else {
826                        (succ.saturating_add(tx.gas_used), rev)
827                    }
828                });
829
830            if aborted {
831                // State is reverted to the pre-bundle outer checkpoint regardless
832                // of `commit`; no payment.
833                BundleResult {
834                    per_tx,
835                    coinbase_payment: U256::ZERO,
836                    gas_used: total_gas,
837                    successful_tx_gas,
838                    reverted_tx_gas,
839                    succeeded: false,
840                }
841            } else {
842                // Read the beneficiary's post-bundle balance from the journaled
843                // state (present iff it was touched) BEFORE commit/revert, since
844                // `commit_inner` finalizes (drains) the journal and an outer
845                // revert would undo the credit.
846                let post_beneficiary_balance = evm
847                    .journaled_state
848                    .state
849                    .get(&beneficiary)
850                    .map(|acct| acct.info.balance)
851                    .unwrap_or(pre_beneficiary_balance);
852                // revm already excludes the base fee from the beneficiary credit
853                // (EIP-1559), so the delta is the honest miner payment.
854                let coinbase_payment =
855                    post_beneficiary_balance.saturating_sub(pre_beneficiary_balance);
856
857                if opts.commit {
858                    evm.commit_inner();
859                } else {
860                    evm.journaled_state.checkpoint_revert(outer);
861                }
862
863                BundleResult {
864                    per_tx,
865                    coinbase_payment,
866                    gas_used: total_gas,
867                    successful_tx_gas,
868                    reverted_tx_gas,
869                    succeeded: true,
870                }
871            }
872        };
873
874        self.reclaim_buffer(buffer);
875        Ok(outcome)
876    }
877
878    /// Execute a non-committing call and return the result plus the touched
879    /// [`StorageAccessList`].
880    ///
881    /// The access list is collected from every account marked touched in the
882    /// journaled state after execution, recording both the touched accounts and
883    /// the storage slots accessed under each.
884    ///
885    /// The EVM state is reverted to a checkpoint after a successful transact on
886    /// both success and revert/halt outcomes, so the call never mutates this
887    /// overlay's dirty layer and each overlay simulation stays isolated. As with
888    /// [`Self::call_raw`], a revert or halt is reported through the returned
889    /// [`ExecutionResult`] rather than as an error.
890    ///
891    /// # Errors
892    ///
893    /// Returns an error if the [`TxEnv`] cannot be built, or if revm fails to
894    /// transact the call (for example a database error while loading state).
895    ///
896    /// # Examples
897    ///
898    /// ```no_run
899    /// # use std::sync::Arc;
900    /// # use alloy_primitives::{Address, Bytes};
901    /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
902    /// # fn run(snapshot: Arc<EvmSnapshot>) -> Result<(), Box<dyn std::error::Error>> {
903    /// let mut overlay = EvmOverlay::new(snapshot, None);
904    /// let (result, access_list) =
905    ///     overlay.call_raw_with_access_list(Address::ZERO, Address::ZERO, Bytes::new())?;
906    /// # let _ = (result, access_list);
907    /// # Ok(())
908    /// # }
909    /// ```
910    pub fn call_raw_with_access_list(
911        &mut self,
912        from: Address,
913        to: Address,
914        calldata: Bytes,
915    ) -> Result<(ExecutionResult, StorageAccessList)> {
916        self.call_raw_with_access_list_with(from, to, calldata, &TxConfig::default())
917    }
918
919    /// Like [`call_raw_with_access_list`](Self::call_raw_with_access_list) but
920    /// honors a full [`TxConfig`]: native `value`, `gas_limit`, `gas_price`,
921    /// `nonce`, and a pre-warming EIP-2930 `access_list`.
922    ///
923    /// This is what the freshness optimistic loop uses so a [`SimRequest`]'s tx
924    /// environment — e.g. a payable call carrying `value`, or a gas-bounded call
925    /// — is reproduced faithfully instead of silently running as a zero-value,
926    /// default-gas call. Like the shorthand it is non-committing (the checkpoint
927    /// is reverted) and returns the captured storage access list.
928    ///
929    /// [`SimRequest`]: crate::freshness::SimRequest
930    pub fn call_raw_with_access_list_with(
931        &mut self,
932        from: Address,
933        to: Address,
934        calldata: Bytes,
935        tx: &TxConfig,
936    ) -> Result<(ExecutionResult, StorageAccessList)> {
937        let mut builder = TxEnv::builder()
938            .caller(from)
939            .kind(TxKind::Call(to))
940            .data(calldata)
941            .value(tx.value);
942        if let Some(gas_limit) = tx.gas_limit {
943            builder = builder.gas_limit(gas_limit);
944        }
945        if let Some(gas_price) = tx.gas_price {
946            builder = builder.gas_price(gas_price);
947        }
948        if let Some(nonce) = tx.nonce {
949            builder = builder.nonce(nonce);
950        }
951        if let Some(access_list) = &tx.access_list {
952            builder = builder.access_list(access_list.clone());
953        }
954        let tx_env = builder.build().map_err(OverlayError::tx_env)?;
955
956        // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
957        let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
958        let local = LocalContext {
959            shared_memory_buffer: Rc::clone(&buffer),
960            precompile_error_message: None,
961        };
962
963        let outcome = {
964            let mut evm = self.build_evm_with_local(local);
965            use revm::context_interface::JournalTr;
966            let checkpoint = evm.journaled_state.checkpoint();
967            match evm.transact_one(tx_env) {
968                Ok(result) => {
969                    let mut access_list = StorageAccessList::default();
970                    for (address, account) in evm.journaled_state.state.iter() {
971                        if account.is_touched() {
972                            access_list.accounts.insert(*address);
973                            for (slot_key, _) in account.storage.iter() {
974                                access_list.slots.insert((*address, *slot_key));
975                            }
976                        }
977                    }
978                    evm.journaled_state.checkpoint_revert(checkpoint);
979                    Ok((result, access_list))
980                }
981                Err(e) => {
982                    // Revert the checkpoint even on a host/transact error so the EVM
983                    // journal is not left dirty (mirrors `call_raw`).
984                    evm.journaled_state.checkpoint_revert(checkpoint);
985                    Err(OverlayError::transact(e))
986                }
987            }
988        };
989
990        self.reclaim_buffer(buffer);
991        outcome
992    }
993
994    /// Write a storage value into this overlay's dirty layer.
995    ///
996    /// The dirty layer takes precedence over the snapshot on subsequent reads
997    /// (see the lookup order on [`EvmOverlay`]), so this injects a value into a
998    /// snapshot-backed overlay without mutating the shared snapshot.
999    ///
1000    /// # Freshness validation
1001    ///
1002    /// This is the freshness validator's correction step. When a slot the
1003    /// snapshot captured is found to be stale, the validator writes the
1004    /// freshly-fetched value here and then re-runs the simulation (e.g. via
1005    /// [`Self::call_raw`]): the re-run reads the corrected slot out of the dirty
1006    /// layer instead of the stale snapshot value, so the corrected result
1007    /// becomes observable. Because the override lives only in this overlay,
1008    /// other overlays sharing the same `Arc<EvmSnapshot>` are unaffected.
1009    ///
1010    /// # Examples
1011    ///
1012    /// ```no_run
1013    /// # use std::sync::Arc;
1014    /// # use alloy_primitives::{Address, Bytes, U256};
1015    /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
1016    /// # fn run(snapshot: Arc<EvmSnapshot>, token: Address, slot: U256) -> Result<(), Box<dyn std::error::Error>> {
1017    /// let mut overlay = EvmOverlay::new(snapshot, None);
1018    /// // Inject the fresh value, then re-run to observe the corrected result.
1019    /// overlay.override_slot(token, slot, U256::from(42u64));
1020    /// let corrected = overlay.call_raw(Address::ZERO, token, Bytes::new())?;
1021    /// # let _ = corrected;
1022    /// # Ok(())
1023    /// # }
1024    /// ```
1025    pub fn override_slot(&mut self, address: Address, slot: U256, value: U256) {
1026        self.dirty_storage
1027            .entry(address)
1028            .or_default()
1029            .insert(slot, value);
1030    }
1031}
1032
1033impl revm::database_interface::DatabaseCommit for EvmOverlay {
1034    fn commit(&mut self, changes: alloy_primitives::map::HashMap<Address, revm::state::Account>) {
1035        for (address, account) in changes {
1036            self.dirty_accounts.insert(address, account.info);
1037            let storage = self.dirty_storage.entry(address).or_default();
1038            for (slot, value) in account.storage {
1039                storage.insert(slot, value.present_value);
1040            }
1041        }
1042    }
1043}
1044
1045impl Database for EvmOverlay {
1046    type Error = DatabaseError;
1047
1048    fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
1049        // 1. Check dirty layer
1050        if let Some(info) = self.dirty_accounts.get(&address) {
1051            return Ok(Some(info.clone()));
1052        }
1053        // 2. Check snapshot (O(1) HashMap lookup, no locks). `account_info` folds
1054        //    the two snapshot tiers (overlay ▸ base) and already short-circuits a
1055        //    NotExisting account to None — it must NOT fall through to the ext_db,
1056        //    mirroring revm `DbAccount::info()` and the live `EvmCache` read.
1057        if self.snapshot.accounts_not_existing.contains(&address) {
1058            return Ok(None);
1059        }
1060        if let Some(info) = self.snapshot.account_info(address) {
1061            return Ok(Some(info.clone()));
1062        }
1063        // 3. RPC fallback
1064        if let Some(ref ext_db) = self.ext_db {
1065            let info = ext_db.basic_ref(address)?;
1066            if let Some(ref info) = info {
1067                self.dirty_accounts.insert(address, info.clone());
1068            }
1069            return Ok(info);
1070        }
1071        Ok(None)
1072    }
1073
1074    fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error> {
1075        // Check dirty accounts first
1076        for info in self.dirty_accounts.values() {
1077            if info.code_hash == code_hash
1078                && let Some(code) = &info.code
1079            {
1080                return Ok(code.clone());
1081            }
1082        }
1083        // Check the snapshot's code index (overlay ▸ base).
1084        if let Some(code) = self.snapshot.code(code_hash) {
1085            return Ok(code.clone());
1086        }
1087        // RPC fallback
1088        if let Some(ref ext_db) = self.ext_db {
1089            return ext_db.code_by_hash_ref(code_hash);
1090        }
1091        Ok(Bytecode::default())
1092    }
1093
1094    fn storage(&mut self, address: Address, index: U256) -> Result<U256, Self::Error> {
1095        // 1. Check dirty layer
1096        if let Some(account_storage) = self.dirty_storage.get(&address)
1097            && let Some(value) = account_storage.get(&index)
1098        {
1099            return Ok(*value);
1100        }
1101        // 2. Check snapshot (O(1)). `storage_value` folds the two tiers (overlay ▸
1102        //    cleared-as-ZERO ▸ base); a cleared account's absent slot reads ZERO
1103        //    and must NOT fall through to the ext_db, mirroring the live EVM SLOAD
1104        //    for a StorageCleared/NotExisting account.
1105        if let Some(value) = self.snapshot.storage_value(address, index) {
1106            return Ok(value);
1107        }
1108        // 3. RPC fallback
1109        if let Some(ref ext_db) = self.ext_db {
1110            let value = ext_db.storage_ref(address, index)?;
1111            self.dirty_storage
1112                .entry(address)
1113                .or_default()
1114                .insert(index, value);
1115            return Ok(value);
1116        }
1117        Ok(U256::ZERO)
1118    }
1119
1120    fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error> {
1121        if let Some(hash) = self.snapshot.block_hashes.get(&number) {
1122            return Ok(*hash);
1123        }
1124        if let Some(ref ext_db) = self.ext_db {
1125            return ext_db.block_hash_ref(number);
1126        }
1127        // Snapshots never populate `block_hashes` (the live cache does not track
1128        // block hashes), so without an `ext_db` the `BLOCKHASH` opcode resolves to
1129        // ZERO. Overlays built internally (e.g. the freshness validator) pass
1130        // `ext_db = None`; the fallback is recorded so the validator can fail
1131        // closed (`Unverified`) instead of confirming a sim whose control flow
1132        // may depend on the real hash. See `blockhash_zero_fallback()`.
1133        self.blockhash_zero_fallback = true;
1134        Ok(B256::ZERO)
1135    }
1136}
1137
1138fn extract_access_list(state: &revm::state::EvmState) -> AccessList {
1139    let items: Vec<AccessListItem> = state
1140        .iter()
1141        .filter(|(_, account)| account.is_touched())
1142        .map(|(address, account)| AccessListItem {
1143            address: *address,
1144            storage_keys: account
1145                .storage
1146                .keys()
1147                .map(|slot| B256::from(*slot))
1148                .collect(),
1149        })
1150        .collect();
1151    AccessList(items)
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156    use super::*;
1157    use crate::cache::snapshot::BaseState;
1158    use revm::primitives::hardfork::SpecId;
1159    use std::collections::HashSet;
1160
1161    /// Build a two-tier `EvmSnapshot` whose cold base holds the given accounts,
1162    /// storage, and code, with an empty hot overlay — the shape
1163    /// `snapshot_deep_clone` produces. The `Arc`-per-account storage of the
1164    /// base is built from the plain per-account maps.
1165    fn snap(
1166        accounts: HashMap<Address, AccountInfo>,
1167        storage: HashMap<Address, HashMap<U256, U256>>,
1168        code_by_hash: HashMap<B256, Bytecode>,
1169        block_hashes: HashMap<u64, B256>,
1170    ) -> Arc<EvmSnapshot> {
1171        let base = BaseState {
1172            accounts,
1173            storage: storage
1174                .into_iter()
1175                .map(|(addr, slots)| (addr, Arc::new(slots)))
1176                .collect(),
1177            code_by_hash,
1178        };
1179        Arc::new(EvmSnapshot {
1180            base: Arc::new(base),
1181            overlay_accounts: HashMap::new(),
1182            overlay_storage: HashMap::new(),
1183            overlay_code_by_hash: HashMap::new(),
1184            storage_cleared: HashSet::new(),
1185            accounts_not_existing: HashSet::new(),
1186            block_hashes,
1187            block_number: None,
1188            basefee: None,
1189            coinbase: None,
1190            prevrandao: None,
1191            gas_limit: None,
1192            chain_id: 42161,
1193            timestamp: None,
1194            spec_id: SpecId::CANCUN,
1195            shared_memory_capacity: 64_000,
1196        })
1197    }
1198
1199    #[test]
1200    fn test_overlay_is_send() {
1201        fn assert_send<T: Send>() {}
1202        assert_send::<EvmOverlay>();
1203    }
1204
1205    #[test]
1206    fn blockhash_zero_fallback_flags_only_unresolved_reads() {
1207        let known = B256::repeat_byte(0xAB);
1208        let snapshot = snap(
1209            HashMap::new(),
1210            HashMap::new(),
1211            HashMap::new(),
1212            HashMap::from([(5u64, known)]),
1213        );
1214        let mut overlay = EvmOverlay::new(snapshot, None);
1215
1216        // A snapshot-provided hash resolves for real: no flag.
1217        assert_eq!(overlay.block_hash(5).unwrap(), known);
1218        assert!(!overlay.blockhash_zero_fallback());
1219
1220        // An untracked number falls back to ZERO and is flagged so the
1221        // freshness validator can fail closed.
1222        assert_eq!(overlay.block_hash(6).unwrap(), B256::ZERO);
1223        assert!(overlay.blockhash_zero_fallback());
1224
1225        // The flag is per-simulation state: reset clears it.
1226        overlay.reset();
1227        assert!(!overlay.blockhash_zero_fallback());
1228    }
1229
1230    #[test]
1231    fn test_overlay_basic_from_snapshot() {
1232        let mut accounts = HashMap::new();
1233        let info = AccountInfo {
1234            balance: U256::from(1000),
1235            nonce: 1,
1236            code_hash: B256::ZERO,
1237            code: None,
1238            account_id: None,
1239        };
1240        let addr = Address::repeat_byte(0x01);
1241        accounts.insert(addr, info);
1242
1243        let snapshot = snap(accounts, HashMap::new(), HashMap::new(), HashMap::new());
1244
1245        let mut overlay = EvmOverlay::new(snapshot, None);
1246        let result = overlay.basic(addr).unwrap();
1247        assert!(result.is_some());
1248        assert_eq!(result.unwrap().balance, U256::from(1000));
1249    }
1250
1251    #[test]
1252    fn test_overlay_storage_from_snapshot() {
1253        let addr = Address::repeat_byte(0x01);
1254        let slot = U256::from(42);
1255        let value = U256::from(999);
1256
1257        let mut storage = HashMap::new();
1258        let mut account_storage = HashMap::new();
1259        account_storage.insert(slot, value);
1260        storage.insert(addr, account_storage);
1261
1262        let snapshot = snap(HashMap::new(), storage, HashMap::new(), HashMap::new());
1263
1264        let mut overlay = EvmOverlay::new(snapshot, None);
1265        let result = overlay.storage(addr, slot).unwrap();
1266        assert_eq!(result, value);
1267    }
1268
1269    #[test]
1270    fn test_overlay_dirty_overrides_snapshot() {
1271        let addr = Address::repeat_byte(0x01);
1272        let slot = U256::from(42);
1273
1274        let mut storage = HashMap::new();
1275        let mut account_storage = HashMap::new();
1276        account_storage.insert(slot, U256::from(100));
1277        storage.insert(addr, account_storage);
1278
1279        let snapshot = snap(HashMap::new(), storage, HashMap::new(), HashMap::new());
1280
1281        let mut overlay = EvmOverlay::new(snapshot, None);
1282
1283        // Write to dirty layer
1284        overlay
1285            .dirty_storage
1286            .entry(addr)
1287            .or_default()
1288            .insert(slot, U256::from(200));
1289
1290        // Should read dirty value, not snapshot
1291        let result = overlay.storage(addr, slot).unwrap();
1292        assert_eq!(result, U256::from(200));
1293    }
1294
1295    #[test]
1296    fn test_overlay_missing_returns_zero() {
1297        let snapshot = snap(
1298            HashMap::new(),
1299            HashMap::new(),
1300            HashMap::new(),
1301            HashMap::new(),
1302        );
1303
1304        let mut overlay = EvmOverlay::new(snapshot, None);
1305        let addr = Address::repeat_byte(0x99);
1306        let result = overlay.storage(addr, U256::from(1)).unwrap();
1307        assert_eq!(result, U256::ZERO);
1308
1309        let account = overlay.basic(addr).unwrap();
1310        assert!(account.is_none());
1311    }
1312
1313    #[test]
1314    fn test_overlay_code_by_hash_from_snapshot() {
1315        let code = Bytecode::new_raw(Bytes::from(vec![0x60, 0x00, 0x60, 0x00]));
1316        let hash = code.hash_slow();
1317
1318        let mut code_by_hash = HashMap::new();
1319        code_by_hash.insert(hash, code.clone());
1320
1321        let snapshot = snap(HashMap::new(), HashMap::new(), code_by_hash, HashMap::new());
1322
1323        let mut overlay = EvmOverlay::new(snapshot, None);
1324        let result = overlay.code_by_hash(hash).unwrap();
1325        assert_eq!(result.len(), 4);
1326    }
1327
1328    #[test]
1329    fn test_overlay_block_hash() {
1330        let mut block_hashes = HashMap::new();
1331        let hash = B256::repeat_byte(0xAB);
1332        block_hashes.insert(42u64, hash);
1333
1334        let snapshot = snap(HashMap::new(), HashMap::new(), HashMap::new(), block_hashes);
1335
1336        let mut overlay = EvmOverlay::new(snapshot, None);
1337        assert_eq!(overlay.block_hash(42).unwrap(), hash);
1338        assert_eq!(overlay.block_hash(99).unwrap(), B256::ZERO);
1339    }
1340}