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}