Skip to main content

evm_fork_cache/cache/
overlay.rs

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