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, HashSet};
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, IERC20, SimStatus, TxConfig, unix_timestamp_secs_saturating};
28use crate::access_set::StorageAccessList;
29use crate::bundle::{BundleOptions, BundleResult, BundleTx, RevertPolicy, TxOutcome};
30use crate::cancellation::SimulationCancellationToken;
31use crate::errors::{
32 OverlayError, OverlayResult as Result, SimError, SimHostError, SimulationError,
33 SimulationResult,
34};
35use crate::inspector::TransferInspector;
36use crate::mapping_probe::HashStorageProbe;
37use alloy_sol_types::SolCall;
38
39#[derive(Clone, Debug)]
40struct SimulationCancellationInspector {
41 token: SimulationCancellationToken,
42}
43
44impl<CTX, INTR> revm::Inspector<CTX, INTR> for SimulationCancellationInspector
45where
46 INTR: revm::interpreter::InterpreterTypes,
47{
48 fn step(&mut self, interpreter: &mut revm::interpreter::Interpreter<INTR>, _context: &mut CTX) {
49 self.token.mark_started();
50 if self.token.is_cancelled() {
51 interpreter.halt(revm::interpreter::InstructionResult::Stop);
52 }
53 }
54}
55
56type OverlayEvm<'a> = revm::MainnetEvm<
57 Context<BlockEnv, TxEnv, CfgEnv, &'a mut EvmOverlay, Journal<&'a mut EvmOverlay>, ()>,
58>;
59
60type InspectorOverlayEvm<'a, INSP> = revm::MainnetEvm<
61 Context<BlockEnv, TxEnv, CfgEnv, &'a mut EvmOverlay, Journal<&'a mut EvmOverlay>, ()>,
62 INSP,
63>;
64
65/// State an RPC-disconnected overlay could not resolve from its immutable
66/// snapshot.
67///
68/// The EVM database interface requires a value even when an offline snapshot is
69/// incomplete. [`EvmOverlay`] continues to return the protocol-neutral fallback
70/// (`None`, empty bytecode, ZERO storage, or ZERO block hash), but records every
71/// such fallback here. Callers must treat a non-empty report as an incomplete
72/// simulation rather than authoritative execution.
73#[derive(Clone, Debug, Default, PartialEq, Eq)]
74pub struct MissingState {
75 /// Account headers absent from the snapshot.
76 pub accounts: HashSet<Address>,
77 /// Runtime-code hashes absent from the snapshot.
78 pub code_hashes: HashSet<B256>,
79 /// Storage slots absent from the snapshot.
80 pub storage: HashSet<(Address, U256)>,
81 /// In-range block numbers whose hashes were absent from the snapshot.
82 pub block_hashes: HashSet<u64>,
83}
84
85impl MissingState {
86 /// Whether the offline overlay resolved every database read locally.
87 pub fn is_empty(&self) -> bool {
88 self.accounts.is_empty()
89 && self.code_hashes.is_empty()
90 && self.storage.is_empty()
91 && self.block_hashes.is_empty()
92 }
93
94 /// Convert unresolved reads into the generic execution read-set shape.
95 pub fn as_read_set(&self) -> StorageAccessList {
96 StorageAccessList {
97 accounts: self.accounts.clone(),
98 code_hashes: self.code_hashes.clone(),
99 slots: self.storage.clone(),
100 block_numbers: self.block_hashes.clone(),
101 }
102 }
103}
104
105/// Per-simulation mutable overlay on an immutable snapshot.
106///
107/// Lookup order: dirty layer → snapshot → ext_db (optional RPC fallback).
108///
109/// This type is `Send` (unlike `EvmCache`) because it uses no `Rc`/`RefCell`.
110/// Each simulation task gets its own `EvmOverlay` with a cheap `Arc::clone`
111/// of the shared `EvmSnapshot`.
112///
113/// # Reuse across simulations (Pillar A.2)
114///
115/// A worker doing many sims against the same snapshot can call [`Self::new`]
116/// once and [`Self::reset`] between sims instead of allocating a fresh overlay
117/// each time. The reusable shared-memory buffer is also recycled across calls —
118/// see [`Self::call_raw`] — without making the overlay `!Send`.
119pub struct EvmOverlay {
120 snapshot: Arc<EvmSnapshot>,
121 /// Per-simulation mutations (accounts fetched from ext_db, committed changes).
122 dirty_accounts: HashMap<Address, AccountInfo>,
123 /// Per-simulation storage mutations.
124 dirty_storage: HashMap<Address, HashMap<U256, U256>>,
125 /// Optional RPC fallback for data not in snapshot.
126 ext_db: Option<SharedBackend>,
127 /// Reusable shared-memory buffer, recycled across the build→transact→revert
128 /// call methods to avoid reallocating a 64 KB `Vec` per call.
129 ///
130 /// Stored as a plain `Vec<u8>` (not an `Rc`) so the overlay stays `Send`. A
131 /// call method `mem::take`s it, wraps it in a method-local `Rc<RefCell<_>>`
132 /// for revm's [`LocalContext`], runs, then reclaims and clears it after the
133 /// EVM is dropped (see [`Self::build_evm_with_local`]).
134 reusable_buffer: Vec<u8>,
135 /// Target pre-allocation (bytes) for [`Self::reusable_buffer`] and each
136 /// per-call buffer, taken from the snapshot's configured
137 /// [`SharedMemoryCapacity`](super::SharedMemoryCapacity) so overlays honor the
138 /// capacity set on the originating [`EvmCache`].
139 buffer_capacity: usize,
140 /// Set when a `BLOCKHASH` read fell through to the ZERO fallback (no
141 /// snapshot-provided hash and no `ext_db`). The freshness validator reads
142 /// this via [`Self::blockhash_zero_fallback`] to fail closed instead of
143 /// confirming a sim whose control flow may rest on a hash its overlays
144 /// cannot resolve. Cleared by [`Self::reset`].
145 blockhash_zero_fallback: bool,
146 /// Exact unresolved reads observed while no external database was attached.
147 missing_state: MissingState,
148}
149
150impl EvmOverlay {
151 /// Create a new overlay on the given snapshot.
152 ///
153 /// The reusable shared-memory buffer is pre-allocated to the snapshot's
154 /// configured shared-memory capacity (see
155 /// [`SharedMemoryCapacity`](super::SharedMemoryCapacity)).
156 pub fn new(snapshot: Arc<EvmSnapshot>, ext_db: Option<SharedBackend>) -> Self {
157 let buffer_capacity = snapshot.shared_memory_capacity;
158 Self {
159 snapshot,
160 dirty_accounts: HashMap::new(),
161 dirty_storage: HashMap::new(),
162 ext_db,
163 reusable_buffer: Vec::with_capacity(buffer_capacity),
164 buffer_capacity,
165 blockhash_zero_fallback: false,
166 missing_state: MissingState::default(),
167 }
168 }
169
170 /// Clear the per-simulation dirty layer so this overlay can be reused for the
171 /// next simulation against the same snapshot, without reallocating (Pillar
172 /// A.2).
173 ///
174 /// A worker doing K sims calls [`Self::new`] once and `reset()` between sims
175 /// instead of allocating a fresh overlay (plus dirty maps plus an `Arc`
176 /// clone) each time. After `reset()` the overlay reads the pristine snapshot
177 /// again — it is exactly equivalent to a freshly-built overlay on the same
178 /// snapshot. The snapshot `Arc`, the optional `ext_db`, and the reusable
179 /// shared-memory buffer (kept at capacity) are retained.
180 pub fn reset(&mut self) {
181 self.dirty_accounts.clear();
182 self.dirty_storage.clear();
183 self.blockhash_zero_fallback = false;
184 self.missing_state = MissingState::default();
185 // Keep: snapshot Arc, ext_db, and the reusable buffer. The buffer is
186 // already cleared after each call, so nothing to do for it here.
187 }
188
189 /// `true` if any `BLOCKHASH` read on this overlay fell through to the ZERO
190 /// fallback (no snapshot-provided hash for that number and no `ext_db`)
191 /// since construction or the last [`reset`](Self::reset).
192 ///
193 /// The freshness validator uses this to **fail closed**: a sim that read a
194 /// hash its ext-db-less overlays cannot resolve is reported
195 /// [`Unverified`](crate::freshness::Validation::Unverified) rather than
196 /// silently confirmed against a ZERO stand-in.
197 ///
198 /// Only reads revm actually routes to the database can set this: requests
199 /// outside the EVM's valid lookback window (`[current − 256, current)`)
200 /// return spec-mandated ZERO without a database call — that value is
201 /// correct on-chain too, so such reads are deliberately not flagged.
202 pub fn blockhash_zero_fallback(&self) -> bool {
203 self.blockhash_zero_fallback
204 }
205
206 /// Exact database reads that fell back because this overlay has no external
207 /// provider and its immutable snapshot did not contain the requested state.
208 ///
209 /// A non-empty report makes the simulation non-authoritative even when EVM
210 /// execution itself returned success: a missing storage slot, for example,
211 /// is represented as ZERO to satisfy the database trait.
212 pub fn missing_state(&self) -> &MissingState {
213 &self.missing_state
214 }
215
216 /// Chain ID of the block context captured by the underlying snapshot.
217 ///
218 /// This is the value installed into `cfg.chain_id` by [`Self::build_evm`].
219 pub fn chain_id(&self) -> u64 {
220 self.snapshot.chain_id
221 }
222
223 /// Block number of the snapshot's block context, or `None` if it was not
224 /// captured.
225 ///
226 /// When present this is the `block.number` simulations run against; when
227 /// `None`, [`Self::build_evm`] leaves revm's default block number in place.
228 pub fn block_number(&self) -> Option<u64> {
229 self.snapshot.block_number
230 }
231
232 /// Base fee of the snapshot's block context, or `None` if it was not
233 /// captured.
234 ///
235 /// Note that base-fee checks are disabled in the simulation EVM, so this is
236 /// informational rather than enforced against the transaction.
237 pub fn basefee(&self) -> Option<u64> {
238 self.snapshot.basefee
239 }
240
241 /// Timestamp of the snapshot's block context, or `None` if it was not
242 /// captured.
243 ///
244 /// When `None`, [`Self::build_evm`] substitutes the current wall-clock time
245 /// for `block.timestamp`.
246 pub fn timestamp(&self) -> Option<u64> {
247 self.snapshot.timestamp
248 }
249
250 /// A fresh [`LocalContext`] with a newly-allocated 64 KB shared-memory buffer.
251 ///
252 /// Used by the public [`Self::build_evm`], which hands out the EVM and cannot
253 /// reclaim its buffer afterwards. The internal call methods instead recycle
254 /// [`Self::reusable_buffer`] via [`Self::build_evm_with_local`].
255 fn fresh_local(&self) -> LocalContext {
256 LocalContext {
257 shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(self.buffer_capacity))),
258 precompile_error_message: None,
259 }
260 }
261
262 /// Build a revm EVM instance backed by this overlay, using a caller-supplied
263 /// [`LocalContext`].
264 ///
265 /// This is the shared body behind [`Self::build_evm`] and the internal call
266 /// methods. The call methods pass a `local` wrapping the recycled
267 /// [`Self::reusable_buffer`] (Pillar A.2) and reclaim it after the EVM is
268 /// dropped; [`Self::build_evm`] passes a fresh one.
269 ///
270 /// Note: the returned EVM is `!Send` (due to `LocalContext`'s `Rc<RefCell>`),
271 /// but this is fine because it's created and used within a single task.
272 fn build_evm_with_local(&mut self, local: LocalContext) -> OverlayEvm<'_> {
273 // Read snapshot values before the mutable borrow of self
274 let chain_id = self.snapshot.chain_id;
275 let spec_id = self.snapshot.spec_id;
276 let timestamp = self
277 .snapshot
278 .timestamp
279 .unwrap_or_else(|| unix_timestamp_secs_saturating(std::time::SystemTime::now()));
280 let block_number = self.snapshot.block_number;
281 let basefee = self.snapshot.basefee;
282 let coinbase = self.snapshot.coinbase;
283 let prevrandao = self.snapshot.prevrandao;
284 let gas_limit = self.snapshot.gas_limit;
285
286 let mut evm = Context::mainnet()
287 .with_db(&mut *self)
288 .with_local(local)
289 .modify_cfg_chained(|cfg| {
290 cfg.disable_nonce_check = true;
291 cfg.disable_eip3607 = true;
292 cfg.disable_base_fee = true;
293 cfg.disable_balance_check = true;
294 cfg.chain_id = chain_id;
295 cfg.limit_contract_code_size = None;
296 cfg.tx_chain_id_check = false;
297 cfg.spec = spec_id;
298 })
299 .build_mainnet();
300
301 evm.block.timestamp = U256::from(timestamp);
302 if let Some(number) = block_number {
303 evm.block.number = U256::from(number);
304 }
305 if let Some(basefee) = basefee {
306 evm.block.basefee = basefee;
307 }
308 if let Some(coinbase) = coinbase {
309 evm.block.beneficiary = coinbase;
310 }
311 if let Some(prevrandao) = prevrandao {
312 evm.block.prevrandao = Some(prevrandao);
313 }
314 if let Some(gas_limit) = gas_limit {
315 evm.block.gas_limit = gas_limit;
316 }
317 evm
318 }
319
320 /// Build a revm EVM instance backed by this overlay.
321 ///
322 /// This allocates a fresh 64 KB shared-memory buffer each call: it hands the
323 /// EVM out to the caller and cannot reclaim the buffer afterwards, so it
324 /// cannot recycle the overlay's reusable buffer. The internal call methods
325 /// ([`Self::call_raw`], etc.) recycle the buffer instead (Pillar A.2).
326 ///
327 /// Note: The returned EVM is `!Send` (due to `LocalContext`'s `Rc<RefCell>`),
328 /// but this is fine because it's created and used within a single task.
329 pub fn build_evm(&mut self) -> OverlayEvm<'_> {
330 let local = self.fresh_local();
331 self.build_evm_with_local(local)
332 }
333
334 /// Execute a non-committing call and return the raw [`ExecutionResult`].
335 ///
336 /// The EVM state is reverted to a checkpoint after execution on *both*
337 /// success and failure, so the call never mutates this overlay's dirty
338 /// layer. Each overlay simulation is therefore isolated: repeated calls all
339 /// observe the same base state.
340 ///
341 /// A revert or halt is *not* an error here — it is reported through the
342 /// returned [`ExecutionResult`] variant. Only failure to build or transact
343 /// the call yields `Err`.
344 ///
345 /// # Errors
346 ///
347 /// Returns an error if the [`TxEnv`] cannot be built from the given inputs,
348 /// or if revm fails to transact the call (for example a database error
349 /// while loading state from the RPC fallback).
350 ///
351 /// # Examples
352 ///
353 /// ```no_run
354 /// # use std::sync::Arc;
355 /// # use alloy_primitives::{Address, Bytes};
356 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
357 /// # fn run(snapshot: Arc<EvmSnapshot>) -> Result<(), Box<dyn std::error::Error>> {
358 /// let mut overlay = EvmOverlay::new(snapshot, None);
359 /// let result = overlay.call_raw(Address::ZERO, Address::ZERO, Bytes::new())?;
360 /// // State is reverted; a second call sees the same base state.
361 /// let _again = overlay.call_raw(Address::ZERO, Address::ZERO, Bytes::new())?;
362 /// # let _ = result;
363 /// # Ok(())
364 /// # }
365 /// ```
366 pub fn call_raw(
367 &mut self,
368 from: Address,
369 to: Address,
370 calldata: Bytes,
371 ) -> Result<ExecutionResult> {
372 let tx = TxEnv::builder()
373 .caller(from)
374 .kind(TxKind::Call(to))
375 .data(calldata)
376 .value(U256::ZERO)
377 .build()
378 .map_err(OverlayError::tx_env)?;
379
380 // Recycle the reusable buffer (Pillar A.2): take it out as a plain Vec
381 // (keeping the overlay Send), lend it to a method-local Rc<RefCell> for
382 // revm's LocalContext, then reclaim and clear it after the EVM is dropped.
383 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
384 let local = LocalContext {
385 shared_memory_buffer: Rc::clone(&buffer),
386 precompile_error_message: None,
387 };
388
389 let result = {
390 let mut evm = self.build_evm_with_local(local);
391 use revm::context_interface::JournalTr;
392 let checkpoint = evm.journaled_state.checkpoint();
393 let result = evm.transact_one(tx).map_err(OverlayError::transact);
394 evm.journaled_state.checkpoint_revert(checkpoint);
395 result
396 };
397
398 self.reclaim_buffer(buffer);
399 result
400 }
401
402 /// Execute one non-committing call with temporary account-code overrides.
403 ///
404 /// Overrides live only for this call: the previous dirty-layer account (or
405 /// its absence) is restored before this method returns, including when the
406 /// EVM returns an execution error. The shared snapshot is never mutated.
407 /// This is useful for execution helpers whose control flow depends on an
408 /// external call but whose result does not depend on that callee's state;
409 /// for example, a revert-based V3 quoter transfers the output token before
410 /// deliberately reverting with the quote payload.
411 ///
412 /// Each supplied runtime bytecode replaces only the account's code and code
413 /// hash. Its balance, nonce, and account id continue to come from the
414 /// overlay/snapshot account when present.
415 ///
416 /// # Errors
417 ///
418 /// Returns an error if an overridden account cannot be loaded, the
419 /// transaction environment cannot be built, or revm cannot transact the
420 /// call. Empty bytecode is accepted and behaves like an EOA.
421 pub fn call_raw_with_code_overrides(
422 &mut self,
423 from: Address,
424 to: Address,
425 calldata: Bytes,
426 overrides: &[(Address, Bytes)],
427 ) -> Result<ExecutionResult> {
428 let mut prior = Vec::with_capacity(overrides.len());
429 for (address, runtime_bytecode) in overrides {
430 let old_dirty = self.dirty_accounts.get(address).cloned();
431 let mut info = match self.basic(*address) {
432 Ok(info) => info.unwrap_or_default(),
433 Err(error) => {
434 for (prior_address, prior_info) in prior.into_iter().rev() {
435 if let Some(info) = prior_info {
436 self.dirty_accounts.insert(prior_address, info);
437 } else {
438 self.dirty_accounts.remove(&prior_address);
439 }
440 }
441 return Err(OverlayError::transact(error));
442 }
443 };
444 let bytecode = Bytecode::new_raw(runtime_bytecode.clone());
445 info.code_hash = bytecode.hash_slow();
446 info.code = Some(bytecode);
447 self.dirty_accounts.insert(*address, info);
448 prior.push((*address, old_dirty));
449 }
450
451 let result = self.call_raw(from, to, calldata);
452
453 for (address, old_dirty) in prior.into_iter().rev() {
454 if let Some(info) = old_dirty {
455 self.dirty_accounts.insert(address, info);
456 } else {
457 self.dirty_accounts.remove(&address);
458 }
459 }
460 result
461 }
462
463 /// Reclaim the recycled shared-memory buffer after the EVM (and its
464 /// `LocalContext` clone of the `Rc`) has been dropped, clearing it for the
465 /// next call.
466 ///
467 /// The `Rc` was only ever held by the dropped EVM and this method's local, so
468 /// `try_unwrap` succeeds in the normal path. If a panic somewhere left an
469 /// extra strong reference the buffer is simply re-allocated next call — no
470 /// correctness impact.
471 fn reclaim_buffer(&mut self, buffer: Rc<RefCell<Vec<u8>>>) {
472 if let Ok(cell) = Rc::try_unwrap(buffer) {
473 let mut buf = cell.into_inner();
474 buf.clear();
475 self.reusable_buffer = buf;
476 } else {
477 self.reusable_buffer = Vec::with_capacity(self.buffer_capacity);
478 }
479 }
480
481 /// Build a revm EVM instance with an inspector, backed by this overlay, using
482 /// a caller-supplied [`LocalContext`].
483 ///
484 /// Like [`Self::build_evm_with_local`] but attaches `inspector`. The call
485 /// methods pass a `local` wrapping the recycled [`Self::reusable_buffer`]
486 /// (Pillar A.2) and reclaim it after the EVM is dropped.
487 fn build_evm_with_inspector_local<INSP>(
488 &mut self,
489 inspector: INSP,
490 local: LocalContext,
491 ) -> InspectorOverlayEvm<'_, INSP> {
492 let chain_id = self.snapshot.chain_id;
493 let spec_id = self.snapshot.spec_id;
494 let timestamp = self
495 .snapshot
496 .timestamp
497 .unwrap_or_else(|| unix_timestamp_secs_saturating(std::time::SystemTime::now()));
498 let block_number = self.snapshot.block_number;
499 let basefee = self.snapshot.basefee;
500 let coinbase = self.snapshot.coinbase;
501 let prevrandao = self.snapshot.prevrandao;
502 let gas_limit = self.snapshot.gas_limit;
503
504 let mut evm = Context::mainnet()
505 .with_db(&mut *self)
506 .with_local(local)
507 .modify_cfg_chained(|cfg| {
508 cfg.disable_nonce_check = true;
509 cfg.disable_eip3607 = true;
510 cfg.disable_base_fee = true;
511 cfg.disable_balance_check = true;
512 cfg.chain_id = chain_id;
513 cfg.limit_contract_code_size = None;
514 cfg.tx_chain_id_check = false;
515 cfg.spec = spec_id;
516 })
517 .build_mainnet_with_inspector(inspector);
518
519 evm.block.timestamp = U256::from(timestamp);
520 if let Some(number) = block_number {
521 evm.block.number = U256::from(number);
522 }
523 if let Some(basefee) = basefee {
524 evm.block.basefee = basefee;
525 }
526 if let Some(coinbase) = coinbase {
527 evm.block.beneficiary = coinbase;
528 }
529 if let Some(prevrandao) = prevrandao {
530 evm.block.prevrandao = Some(prevrandao);
531 }
532 if let Some(gas_limit) = gas_limit {
533 evm.block.gas_limit = gas_limit;
534 }
535 evm
536 }
537
538 /// Simulate a call with transfer tracking via the `TransferInspector`.
539 ///
540 /// This is the overlay-compatible equivalent of
541 /// [`super::EvmCache::simulate_with_transfer_tracking`]. It captures ERC20
542 /// Transfer events during execution to compute balance deltas for `owner`
543 /// (restricted to `tokens` when provided) without relying on pre/post
544 /// balance queries.
545 ///
546 /// On a reverting or halting call the EVM state is reverted to a checkpoint
547 /// before returning, so a failed simulation never mutates this overlay. On
548 /// success the call either commits the journaled changes into the overlay's
549 /// dirty layer (`commit == true`) or reverts them (`commit == false`); a
550 /// non-committing run leaves each overlay simulation isolated from the next.
551 ///
552 /// # Errors
553 ///
554 /// Returns an error if the [`TxEnv`] cannot be built, if revm fails to
555 /// transact the call, if the call reverts (mapped from the revert payload),
556 /// or if the call halts. In every error case the EVM state is reverted
557 /// first, regardless of `commit`.
558 ///
559 /// # Examples
560 ///
561 /// ```no_run
562 /// # use std::sync::Arc;
563 /// # use alloy_primitives::{Address, Bytes};
564 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
565 /// # fn run(snapshot: Arc<EvmSnapshot>, token: Address, owner: Address) -> Result<(), Box<dyn std::error::Error>> {
566 /// let mut overlay = EvmOverlay::new(snapshot, None);
567 /// let sim = overlay.simulate_with_transfer_tracking(
568 /// owner,
569 /// token,
570 /// Bytes::new(),
571 /// owner,
572 /// Some([token]),
573 /// false, // non-committing: state is reverted afterwards
574 /// )?;
575 /// let _delta = sim.token_deltas.get(&token);
576 /// # Ok(())
577 /// # }
578 /// ```
579 pub fn simulate_with_transfer_tracking(
580 &mut self,
581 from: Address,
582 to: Address,
583 calldata: Bytes,
584 owner: Address,
585 tokens: Option<impl IntoIterator<Item = Address>>,
586 commit: bool,
587 ) -> SimulationResult<CallSimulationResult> {
588 let tx = TxEnv::builder()
589 .caller(from)
590 .kind(TxKind::Call(to))
591 .data(calldata)
592 .value(U256::ZERO)
593 .build()
594 .map_err(|e| SimError::Other(SimHostError::tx_env(e)))?;
595
596 let inspector = TransferInspector::new();
597
598 // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
599 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
600 let local = LocalContext {
601 shared_memory_buffer: Rc::clone(&buffer),
602 precompile_error_message: None,
603 };
604
605 let outcome = {
606 let mut evm = self.build_evm_with_inspector_local(inspector, local);
607
608 use revm::context_interface::JournalTr;
609 let checkpoint = evm.journaled_state.checkpoint();
610
611 let result = evm
612 .inspect_one_tx(tx)
613 .map_err(|e| SimError::Other(SimHostError::transact(e)));
614
615 match result {
616 Ok(ExecutionResult::Success {
617 logs,
618 gas_used,
619 output,
620 ..
621 }) => {
622 let token_deltas = if let Some(token_list) = tokens {
623 evm.inspector.balance_deltas_for_tokens(owner, token_list)
624 } else {
625 evm.inspector.balance_deltas(owner)
626 };
627
628 // Extract EIP-2930 access list from journaled state
629 let access_list = extract_access_list(&evm.journaled_state.state);
630
631 if commit {
632 evm.commit_inner();
633 } else {
634 evm.journaled_state.checkpoint_revert(checkpoint);
635 }
636
637 Ok(CallSimulationResult {
638 status: SimStatus::Success,
639 gas_used,
640 token_deltas,
641 logs,
642 access_list,
643 output: output.into_data(),
644 })
645 }
646 Ok(ExecutionResult::Revert { gas_used, output }) => {
647 evm.journaled_state.checkpoint_revert(checkpoint);
648 Err(SimulationError::from_revert(gas_used, output).into())
649 }
650 Ok(ExecutionResult::Halt { reason, gas_used }) => {
651 evm.journaled_state.checkpoint_revert(checkpoint);
652 Err(SimError::Halt {
653 reason: format!("{reason:?}"),
654 gas_used,
655 })
656 }
657 Err(err) => {
658 evm.journaled_state.checkpoint_revert(checkpoint);
659 Err(err)
660 }
661 }
662 };
663
664 self.reclaim_buffer(buffer);
665 outcome
666 }
667
668 /// Run a single call with a caller-supplied [`Inspector`](revm::Inspector),
669 /// returning the raw [`ExecutionResult`] and handing the inspector back for the
670 /// caller to read.
671 ///
672 /// This is the inspector-generic public seam: where
673 /// [`Self::simulate_with_transfer_tracking`] hard-wires the
674 /// [`TransferInspector`], this accepts any
675 /// [`revm::Inspector`] — a [`CallTracer`](crate::tracing::CallTracer), an
676 /// [`InspectorStack`](crate::tracing::InspectorStack) composing several, or a
677 /// caller-defined one. It honors a full [`TxConfig`] (value/gas/nonce/access
678 /// list) exactly like [`Self::call_raw_with_access_list_with`] and recycles the
679 /// reusable shared-memory buffer like the other call methods.
680 ///
681 /// Unlike `simulate_with_transfer_tracking`, a revert or halt is **not** an
682 /// error: the raw [`ExecutionResult`] variant
683 /// ([`Success`](ExecutionResult::Success) /
684 /// [`Revert`](ExecutionResult::Revert) / [`Halt`](ExecutionResult::Halt)) is
685 /// returned as `Ok` so the inspector's captured frames (e.g. a reverted call
686 /// tree) remain observable. Only a tx-env build failure or a transact/database
687 /// error yields `Err`.
688 ///
689 /// On a successful transact the journaled changes are either committed into the
690 /// overlay's dirty layer (`commit == true`) or reverted (`commit == false`),
691 /// matching [`Self::simulate_with_transfer_tracking`]. On a revert/halt the
692 /// checkpoint is always reverted regardless of `commit`, so a failed call never
693 /// mutates this overlay. On a transact error the checkpoint is reverted too.
694 ///
695 /// # Errors
696 ///
697 /// Returns an error if the [`TxEnv`] cannot be built from `from`/`to`/`tx`, or
698 /// if revm fails to transact the call (e.g. a database error while loading
699 /// state).
700 ///
701 /// # Examples
702 ///
703 /// ```no_run
704 /// # use std::sync::Arc;
705 /// # use alloy_primitives::{Address, Bytes};
706 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot, TxConfig};
707 /// # use evm_fork_cache::CallTracer;
708 /// # fn run(snapshot: Arc<EvmSnapshot>, to: Address) -> Result<(), Box<dyn std::error::Error>> {
709 /// let mut overlay = EvmOverlay::new(snapshot, None);
710 /// let (result, tracer) = overlay.call_raw_with_inspector(
711 /// Address::ZERO,
712 /// to,
713 /// Bytes::new(),
714 /// &TxConfig::default(),
715 /// CallTracer::new(),
716 /// false,
717 /// )?;
718 /// let _ = result;
719 /// let _trace = tracer.into_trace();
720 /// # Ok(())
721 /// # }
722 /// ```
723 pub fn call_raw_with_inspector<I>(
724 &mut self,
725 from: Address,
726 to: Address,
727 calldata: Bytes,
728 tx: &TxConfig,
729 inspector: I,
730 commit: bool,
731 ) -> SimulationResult<(ExecutionResult, I)>
732 where
733 I: for<'a> revm::Inspector<
734 Context<
735 BlockEnv,
736 TxEnv,
737 CfgEnv,
738 &'a mut EvmOverlay,
739 Journal<&'a mut EvmOverlay>,
740 (),
741 >,
742 >,
743 {
744 let mut builder = TxEnv::builder()
745 .caller(from)
746 .kind(TxKind::Call(to))
747 .data(calldata)
748 .value(tx.value);
749 if let Some(gas_limit) = tx.gas_limit {
750 builder = builder.gas_limit(gas_limit);
751 }
752 if let Some(gas_price) = tx.gas_price {
753 builder = builder.gas_price(gas_price);
754 }
755 if let Some(nonce) = tx.nonce {
756 builder = builder.nonce(nonce);
757 }
758 if let Some(access_list) = &tx.access_list {
759 builder = builder.access_list(access_list.clone());
760 }
761 let tx_env = builder
762 .build()
763 .map_err(|e| SimError::Other(SimHostError::tx_env(e)))?;
764
765 // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
766 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
767 let local = LocalContext {
768 shared_memory_buffer: Rc::clone(&buffer),
769 precompile_error_message: None,
770 };
771
772 let outcome = {
773 let mut evm = self.build_evm_with_inspector_local(inspector, local);
774
775 use revm::context_interface::JournalTr;
776 let checkpoint = evm.journaled_state.checkpoint();
777
778 match evm.inspect_one_tx(tx_env) {
779 Ok(result) => {
780 if commit && matches!(result, ExecutionResult::Success { .. }) {
781 evm.commit_inner();
782 } else {
783 evm.journaled_state.checkpoint_revert(checkpoint);
784 }
785 // Hand the inspector back to the caller.
786 Ok((result, evm.inspector))
787 }
788 Err(e) => {
789 evm.journaled_state.checkpoint_revert(checkpoint);
790 Err(SimError::Other(SimHostError::transact(e)))
791 }
792 }
793 };
794
795 self.reclaim_buffer(buffer);
796 outcome
797 }
798
799 /// Apply `txs` in order against this overlay over **cumulative** block state,
800 /// with a revert policy and coinbase/miner-payment accounting (Phase 6
801 /// Track A+B).
802 ///
803 /// Each transaction observes the committed writes of the ones before it:
804 /// the bundle runs on a single overlay/EVM with one outer checkpoint plus a
805 /// per-transaction inner checkpoint, so it does **not** rebuild a fresh
806 /// overlay per transaction. See the [`bundle`](crate::bundle) module for the
807 /// public vocabulary ([`BundleTx`], [`BundleOptions`], [`RevertPolicy`],
808 /// [`TxOutcome`], [`BundleResult`]).
809 ///
810 /// # Revert policy
811 ///
812 /// - [`RevertPolicy::Atomic`]: the first transaction that reverts/halts
813 /// rolls the whole bundle back to the outer checkpoint, sets
814 /// `succeeded = false`, and stops (`per_tx` ends at the failing
815 /// transaction). `coinbase_payment` is `0` and the overlay is unchanged.
816 /// - [`RevertPolicy::AllowReverts`]: a revert at a whitelisted index rolls
817 /// back only that transaction (inner checkpoint) and execution continues;
818 /// a revert at a non-whitelisted index behaves like `Atomic`.
819 ///
820 /// # Coinbase accounting
821 ///
822 /// `coinbase_payment` is the block beneficiary's balance delta across the kept
823 /// transactions. Under EIP-1559 revm credits the beneficiary only the priority
824 /// fee (`(effective_gas_price − basefee) × gas_used`) and burns the base fee
825 /// in-EVM, so the delta is the honest miner payment (plus any direct coinbase
826 /// tips). Saturating.
827 ///
828 /// # Commit semantics
829 ///
830 /// `opts.commit == true` folds the bundle's cumulative state into this
831 /// overlay's dirty layer (observable by subsequent overlay calls);
832 /// `false` reverts the outer checkpoint so the overlay is unchanged. A
833 /// failed atomic bundle never leaves partial state regardless of `commit`.
834 ///
835 /// # Errors
836 ///
837 /// Returns [`SimError`] if a transaction environment cannot be built or revm
838 /// fails to transact (e.g. a database error). A transaction *reverting* is
839 /// not an error — it is reported through the per-transaction
840 /// [`TxOutcome`] and the revert policy.
841 pub fn simulate_bundle(
842 &mut self,
843 txs: &[BundleTx],
844 opts: &BundleOptions,
845 ) -> SimulationResult<BundleResult> {
846 // Build every TxEnv up front so a build failure surfaces as an error
847 // before we touch the EVM/journal (and the borrow of `self` is clean).
848 let tx_envs: Vec<TxEnv> = txs
849 .iter()
850 .map(|bt| {
851 let mut builder = TxEnv::builder()
852 .caller(bt.from)
853 .kind(TxKind::Call(bt.to))
854 .data(bt.calldata.clone())
855 .value(bt.tx.value);
856 if let Some(gas_limit) = bt.tx.gas_limit {
857 builder = builder.gas_limit(gas_limit);
858 }
859 if let Some(gas_price) = bt.tx.gas_price {
860 builder = builder.gas_price(gas_price);
861 }
862 if let Some(nonce) = bt.tx.nonce {
863 builder = builder.nonce(nonce);
864 }
865 if let Some(access_list) = &bt.tx.access_list {
866 builder = builder.access_list(access_list.clone());
867 }
868 builder
869 .build()
870 .map_err(|e| SimError::Other(SimHostError::tx_env(e)))
871 })
872 .collect::<std::result::Result<_, _>>()?;
873
874 // Resolve the beneficiary and read its pre-bundle balance before the
875 // mutable borrow of `self` by the EVM (the post-bundle delta is the miner
876 // payment; revm already burns the base fee per EIP-1559).
877 let beneficiary = self
878 .snapshot
879 .coinbase
880 .unwrap_or_else(|| revm::context::BlockEnv::default().beneficiary);
881 let pre_beneficiary_balance = self
882 .basic(beneficiary)
883 .map_err(|e| SimError::Other(SimHostError::database(e)))?
884 .map(|info| info.balance)
885 .unwrap_or(U256::ZERO);
886
887 // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
888 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
889 let local = LocalContext {
890 shared_memory_buffer: Rc::clone(&buffer),
891 precompile_error_message: None,
892 };
893
894 let outcome = {
895 use revm::context_interface::JournalTr;
896 let mut evm = self.build_evm_with_local(local);
897
898 // Outer checkpoint: the whole-bundle savepoint.
899 let outer = evm.journaled_state.checkpoint();
900
901 let mut per_tx: Vec<TxOutcome> = Vec::with_capacity(tx_envs.len());
902 let mut total_gas: u64 = 0;
903 let mut aborted = false;
904
905 'bundle: for (idx, tx_env) in tx_envs.into_iter().enumerate() {
906 // Inner checkpoint: this transaction's savepoint.
907 let inner = evm.journaled_state.checkpoint();
908 let result = match evm.transact_one(tx_env) {
909 Ok(result) => result,
910 Err(e) => {
911 // Host/transact error: undo this tx and the whole bundle,
912 // reclaim the buffer, and surface as SimError.
913 evm.journaled_state.checkpoint_revert(inner);
914 evm.journaled_state.checkpoint_revert(outer);
915 drop(evm);
916 self.reclaim_buffer(buffer);
917 return Err(SimError::Other(SimHostError::transact(e)));
918 }
919 };
920
921 let gas_used = result.gas_used();
922 let reverted = !result.is_success();
923 let logs = result.logs().to_vec();
924 total_gas = total_gas.saturating_add(gas_used);
925
926 per_tx.push(TxOutcome {
927 result,
928 gas_used,
929 reverted,
930 logs,
931 });
932
933 if reverted {
934 let allowed = match &opts.revert_policy {
935 RevertPolicy::Atomic => false,
936 RevertPolicy::AllowReverts(idxs) => idxs.contains(&idx),
937 };
938 if allowed {
939 // Roll back only this transaction; later txs still run.
940 evm.journaled_state.checkpoint_revert(inner);
941 continue 'bundle;
942 } else {
943 // Atomic abort: roll the whole bundle back and stop.
944 evm.journaled_state.checkpoint_revert(outer);
945 aborted = true;
946 break 'bundle;
947 }
948 }
949 // Successful tx: its effects stay journaled for the next tx.
950 }
951
952 // Partition total gas into successful/reverted buckets in a single
953 // pass. Saturating (consistent with `total_gas`); the invariant
954 // `successful_tx_gas + reverted_tx_gas == total_gas` holds by
955 // construction since every executed tx lands in exactly one bucket.
956 let (successful_tx_gas, reverted_tx_gas) =
957 per_tx.iter().fold((0u64, 0u64), |(succ, rev), tx| {
958 if tx.reverted {
959 (succ, rev.saturating_add(tx.gas_used))
960 } else {
961 (succ.saturating_add(tx.gas_used), rev)
962 }
963 });
964
965 if aborted {
966 // State is reverted to the pre-bundle outer checkpoint regardless
967 // of `commit`; no payment.
968 BundleResult {
969 per_tx,
970 coinbase_payment: U256::ZERO,
971 gas_used: total_gas,
972 successful_tx_gas,
973 reverted_tx_gas,
974 succeeded: false,
975 }
976 } else {
977 // Read the beneficiary's post-bundle balance from the journaled
978 // state (present iff it was touched) BEFORE commit/revert, since
979 // `commit_inner` finalizes (drains) the journal and an outer
980 // revert would undo the credit.
981 let post_beneficiary_balance = evm
982 .journaled_state
983 .state
984 .get(&beneficiary)
985 .map(|acct| acct.info.balance)
986 .unwrap_or(pre_beneficiary_balance);
987 // revm already excludes the base fee from the beneficiary credit
988 // (EIP-1559), so the delta is the honest miner payment.
989 let coinbase_payment =
990 post_beneficiary_balance.saturating_sub(pre_beneficiary_balance);
991
992 if opts.commit {
993 evm.commit_inner();
994 } else {
995 evm.journaled_state.checkpoint_revert(outer);
996 }
997
998 BundleResult {
999 per_tx,
1000 coinbase_payment,
1001 gas_used: total_gas,
1002 successful_tx_gas,
1003 reverted_tx_gas,
1004 succeeded: true,
1005 }
1006 }
1007 };
1008
1009 self.reclaim_buffer(buffer);
1010 Ok(outcome)
1011 }
1012
1013 /// Execute a non-committing call and return the result plus the touched
1014 /// [`StorageAccessList`].
1015 ///
1016 /// The access list is collected from every account marked touched in the
1017 /// journaled state after execution, recording both the touched accounts and
1018 /// the storage slots accessed under each.
1019 ///
1020 /// The EVM state is reverted to a checkpoint after a successful transact on
1021 /// both success and revert/halt outcomes, so the call never mutates this
1022 /// overlay's dirty layer and each overlay simulation stays isolated. As with
1023 /// [`Self::call_raw`], a revert or halt is reported through the returned
1024 /// [`ExecutionResult`] rather than as an error.
1025 ///
1026 /// # Errors
1027 ///
1028 /// Returns an error if the [`TxEnv`] cannot be built, or if revm fails to
1029 /// transact the call (for example a database error while loading state).
1030 ///
1031 /// # Examples
1032 ///
1033 /// ```no_run
1034 /// # use std::sync::Arc;
1035 /// # use alloy_primitives::{Address, Bytes};
1036 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
1037 /// # fn run(snapshot: Arc<EvmSnapshot>) -> Result<(), Box<dyn std::error::Error>> {
1038 /// let mut overlay = EvmOverlay::new(snapshot, None);
1039 /// let (result, access_list) =
1040 /// overlay.call_raw_with_access_list(Address::ZERO, Address::ZERO, Bytes::new())?;
1041 /// # let _ = (result, access_list);
1042 /// # Ok(())
1043 /// # }
1044 /// ```
1045 pub fn call_raw_with_access_list(
1046 &mut self,
1047 from: Address,
1048 to: Address,
1049 calldata: Bytes,
1050 ) -> Result<(ExecutionResult, StorageAccessList)> {
1051 self.call_raw_with_access_list_with(from, to, calldata, &TxConfig::default())
1052 }
1053
1054 /// Like [`call_raw_with_access_list`](Self::call_raw_with_access_list) but
1055 /// honors a full [`TxConfig`]: native `value`, `gas_limit`, `gas_price`,
1056 /// `nonce`, and a pre-warming EIP-2930 `access_list`.
1057 ///
1058 /// This is what the freshness optimistic loop uses so a [`SimRequest`]'s tx
1059 /// environment — e.g. a payable call carrying `value`, or a gas-bounded call
1060 /// — is reproduced faithfully instead of silently running as a zero-value,
1061 /// default-gas call. Like the shorthand it is non-committing (the checkpoint
1062 /// is reverted) and returns the captured storage access list.
1063 ///
1064 /// [`SimRequest`]: crate::freshness::SimRequest
1065 pub fn call_raw_with_access_list_with(
1066 &mut self,
1067 from: Address,
1068 to: Address,
1069 calldata: Bytes,
1070 tx: &TxConfig,
1071 ) -> Result<(ExecutionResult, StorageAccessList)> {
1072 let mut builder = TxEnv::builder()
1073 .caller(from)
1074 .kind(TxKind::Call(to))
1075 .data(calldata)
1076 .value(tx.value);
1077 if let Some(gas_limit) = tx.gas_limit {
1078 builder = builder.gas_limit(gas_limit);
1079 }
1080 if let Some(gas_price) = tx.gas_price {
1081 builder = builder.gas_price(gas_price);
1082 }
1083 if let Some(nonce) = tx.nonce {
1084 builder = builder.nonce(nonce);
1085 }
1086 if let Some(access_list) = &tx.access_list {
1087 builder = builder.access_list(access_list.clone());
1088 }
1089 let tx_env = builder.build().map_err(OverlayError::tx_env)?;
1090
1091 // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
1092 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
1093 let local = LocalContext {
1094 shared_memory_buffer: Rc::clone(&buffer),
1095 precompile_error_message: None,
1096 };
1097
1098 let outcome = {
1099 let mut evm = self.build_evm_with_local(local);
1100 use revm::context_interface::JournalTr;
1101 let checkpoint = evm.journaled_state.checkpoint();
1102 match evm.transact_one(tx_env) {
1103 Ok(result) => {
1104 let mut access_list = StorageAccessList::default();
1105 for (address, account) in evm.journaled_state.state.iter() {
1106 if account.is_touched() {
1107 access_list.accounts.insert(*address);
1108 let code_hash = account.info.code_hash;
1109 if code_hash != B256::ZERO
1110 && code_hash != revm::primitives::KECCAK_EMPTY
1111 {
1112 access_list.code_hashes.insert(code_hash);
1113 }
1114 for slot_key in account.storage.keys() {
1115 access_list.slots.insert((*address, *slot_key));
1116 }
1117 }
1118 }
1119 evm.journaled_state.checkpoint_revert(checkpoint);
1120 Ok((result, access_list))
1121 }
1122 Err(e) => {
1123 // Revert the checkpoint even on a host/transact error so the EVM
1124 // journal is not left dirty (mirrors `call_raw`).
1125 evm.journaled_state.checkpoint_revert(checkpoint);
1126 Err(OverlayError::transact(e))
1127 }
1128 }
1129 };
1130
1131 self.reclaim_buffer(buffer);
1132 outcome
1133 }
1134
1135 /// Execute a non-committing call with cooperative cancellation.
1136 ///
1137 /// This has the same transaction and access-list semantics as
1138 /// [`Self::call_raw_with_access_list_with`]. The supplied cancellation scope
1139 /// may be shared by related overlay calls and is checked at every EVM
1140 /// instruction boundary. If cancellation is observed, execution halts, the
1141 /// journal checkpoint is reverted, and [`OverlayError::Cancelled`] is
1142 /// returned. An uncancelled execution returns the same result and access-list
1143 /// evidence as the existing API.
1144 ///
1145 /// Cancellation cannot interrupt a database callback or precompile that is
1146 /// already executing; it is observed at the next EVM instruction boundary.
1147 /// For a wholly provider-free cancellation path, construct this overlay with
1148 /// no external database. A scope cannot be reset after cancellation and must
1149 /// not be reused for a later independent candidate.
1150 ///
1151 /// # Errors
1152 ///
1153 /// Returns [`OverlayError::Cancelled`] when the token is cancelled, or the
1154 /// same transaction-environment and host errors as
1155 /// [`Self::call_raw_with_access_list_with`].
1156 pub fn call_raw_with_access_list_with_cancellation(
1157 &mut self,
1158 from: Address,
1159 to: Address,
1160 calldata: Bytes,
1161 tx: &TxConfig,
1162 cancellation: &SimulationCancellationToken,
1163 ) -> Result<(ExecutionResult, StorageAccessList)> {
1164 if cancellation.is_cancelled() {
1165 return Err(OverlayError::Cancelled);
1166 }
1167
1168 let mut builder = TxEnv::builder()
1169 .caller(from)
1170 .kind(TxKind::Call(to))
1171 .data(calldata)
1172 .value(tx.value);
1173 if let Some(gas_limit) = tx.gas_limit {
1174 builder = builder.gas_limit(gas_limit);
1175 }
1176 if let Some(gas_price) = tx.gas_price {
1177 builder = builder.gas_price(gas_price);
1178 }
1179 if let Some(nonce) = tx.nonce {
1180 builder = builder.nonce(nonce);
1181 }
1182 if let Some(access_list) = &tx.access_list {
1183 builder = builder.access_list(access_list.clone());
1184 }
1185 let tx_env = builder.build().map_err(OverlayError::tx_env)?;
1186
1187 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
1188 let local = LocalContext {
1189 shared_memory_buffer: Rc::clone(&buffer),
1190 precompile_error_message: None,
1191 };
1192 let inspector = SimulationCancellationInspector {
1193 token: cancellation.clone(),
1194 };
1195
1196 let outcome = {
1197 let mut evm = self.build_evm_with_inspector_local(inspector, local);
1198 use revm::context_interface::JournalTr;
1199 let checkpoint = evm.journaled_state.checkpoint();
1200 match evm.inspect_one_tx(tx_env) {
1201 Ok(result) => {
1202 let cancelled = cancellation.is_cancelled();
1203 let mut access_list = StorageAccessList::default();
1204 if !cancelled {
1205 for (address, account) in evm.journaled_state.state.iter() {
1206 if account.is_touched() {
1207 access_list.accounts.insert(*address);
1208 let code_hash = account.info.code_hash;
1209 if code_hash != B256::ZERO
1210 && code_hash != revm::primitives::KECCAK_EMPTY
1211 {
1212 access_list.code_hashes.insert(code_hash);
1213 }
1214 for slot_key in account.storage.keys() {
1215 access_list.slots.insert((*address, *slot_key));
1216 }
1217 }
1218 }
1219 }
1220 evm.journaled_state.checkpoint_revert(checkpoint);
1221 if cancelled {
1222 Err(OverlayError::Cancelled)
1223 } else {
1224 Ok((result, access_list))
1225 }
1226 }
1227 Err(error) => {
1228 evm.journaled_state.checkpoint_revert(checkpoint);
1229 if cancellation.is_cancelled() {
1230 Err(OverlayError::Cancelled)
1231 } else {
1232 Err(OverlayError::transact(error))
1233 }
1234 }
1235 }
1236 };
1237
1238 self.reclaim_buffer(buffer);
1239 outcome
1240 }
1241
1242 /// Write a storage value into this overlay's dirty layer.
1243 ///
1244 /// The dirty layer takes precedence over the snapshot on subsequent reads
1245 /// (see the lookup order on [`EvmOverlay`]), so this injects a value into a
1246 /// snapshot-backed overlay without mutating the shared snapshot.
1247 ///
1248 /// # Freshness validation
1249 ///
1250 /// This is the freshness validator's correction step. When a slot the
1251 /// snapshot captured is found to be stale, the validator writes the
1252 /// freshly-fetched value here and then re-runs the simulation (e.g. via
1253 /// [`Self::call_raw`]): the re-run reads the corrected slot out of the dirty
1254 /// layer instead of the stale snapshot value, so the corrected result
1255 /// becomes observable. Because the override lives only in this overlay,
1256 /// other overlays sharing the same `Arc<EvmSnapshot>` are unaffected.
1257 ///
1258 /// # Examples
1259 ///
1260 /// ```no_run
1261 /// # use std::sync::Arc;
1262 /// # use alloy_primitives::{Address, Bytes, U256};
1263 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
1264 /// # fn run(snapshot: Arc<EvmSnapshot>, token: Address, slot: U256) -> Result<(), Box<dyn std::error::Error>> {
1265 /// let mut overlay = EvmOverlay::new(snapshot, None);
1266 /// // Inject the fresh value, then re-run to observe the corrected result.
1267 /// overlay.override_slot(token, slot, U256::from(42u64));
1268 /// let corrected = overlay.call_raw(Address::ZERO, token, Bytes::new())?;
1269 /// # let _ = corrected;
1270 /// # Ok(())
1271 /// # }
1272 /// ```
1273 pub fn override_slot(&mut self, address: Address, slot: U256, value: U256) {
1274 self.dirty_storage
1275 .entry(address)
1276 .or_default()
1277 .insert(slot, value);
1278 }
1279
1280 /// Execute a non-committing typed Solidity call from [`Address::ZERO`],
1281 /// decoding the return — the overlay counterpart to
1282 /// [`EvmCache::call_sol`](super::EvmCache::call_sol).
1283 ///
1284 /// ```no_run
1285 /// # use std::sync::Arc;
1286 /// # use alloy_primitives::Address;
1287 /// # use alloy_sol_types::sol;
1288 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
1289 /// # sol! { interface IErc20 { function balanceOf(address account) returns (uint256); } }
1290 /// # fn run(mut overlay: EvmOverlay, token: Address, alice: Address) -> Result<(), Box<dyn std::error::Error>> {
1291 /// let bal = overlay.call_sol(token, IErc20::balanceOfCall { account: alice })?;
1292 /// # let _ = bal; Ok(()) }
1293 /// ```
1294 pub fn call_sol<C: SolCall>(&mut self, to: Address, call: C) -> Result<C::Return> {
1295 self.call_sol_from(Address::ZERO, to, call)
1296 }
1297
1298 /// Execute a non-committing typed Solidity call from an explicit sender,
1299 /// decoding the return.
1300 pub fn call_sol_from<C: SolCall>(
1301 &mut self,
1302 from: Address,
1303 to: Address,
1304 call: C,
1305 ) -> Result<C::Return> {
1306 let result = self.call_raw(from, to, Bytes::from(call.abi_encode()))?;
1307 match result {
1308 ExecutionResult::Success { output, .. } => {
1309 let output = output.into_data();
1310 C::abi_decode_returns(&output).map_err(|error| OverlayError::SolCallDecode {
1311 signature: C::SIGNATURE,
1312 from,
1313 to,
1314 output_len: output.len(),
1315 details: format!("{error:?}"),
1316 })
1317 }
1318 other => Err(OverlayError::SolCallFailed {
1319 signature: C::SIGNATURE,
1320 from,
1321 to,
1322 result: format!("{other:?}"),
1323 }),
1324 }
1325 }
1326
1327 /// Mock `holder`'s ERC-20 balance of `token` to `amount` — **overlay-local**.
1328 ///
1329 /// Discovers the balance mapping slot and layout (Solidity / Vyper / Solady)
1330 /// from a single `balanceOf(holder)` simulation, writes `amount` to that slot
1331 /// in this overlay's dirty layer via [`override_slot`](Self::override_slot),
1332 /// and verifies. The cache and snapshot are never mutated; the mock is
1333 /// dropped with the overlay.
1334 ///
1335 /// Returns `Ok(true)` if set and verified, `Ok(false)` if no balance slot was
1336 /// discoverable or the write did not drive the return (e.g. a rebasing token,
1337 /// or `holder == Address::ZERO`, which is refused). A failed attempt leaves no
1338 /// stray write.
1339 pub fn mock_balance(
1340 &mut self,
1341 token: Address,
1342 holder: Address,
1343 amount: U256,
1344 ) -> SimulationResult<bool> {
1345 if holder == Address::ZERO {
1346 return Ok(false);
1347 }
1348 let calldata = Bytes::from(IERC20::balanceOfCall { target: holder }.abi_encode());
1349 let holder_word = holder.into_word();
1350 self.mock_slot_driving(token, calldata, amount, move |probe, ret| {
1351 probe
1352 .accesses(&[holder_word])
1353 .into_iter()
1354 .filter(|a| a.keyed_by(holder_word))
1355 .max_by_key(|a| (a.value == ret, a.confidence))
1356 .map(|a| (a.slot, a.value))
1357 })
1358 }
1359
1360 /// Mock `owner`'s ERC-20 allowance to `spender` on `token` — overlay-local.
1361 ///
1362 /// Discovers the (nested) `allowance` mapping entry keyed by both addresses,
1363 /// writes `amount` (pass `U256::MAX` for "unlimited"), and verifies. Refuses
1364 /// `owner == Address::ZERO`. Same isolation and failure semantics as
1365 /// [`mock_balance`](Self::mock_balance).
1366 pub fn mock_allowance(
1367 &mut self,
1368 token: Address,
1369 owner: Address,
1370 spender: Address,
1371 amount: U256,
1372 ) -> SimulationResult<bool> {
1373 if owner == Address::ZERO {
1374 return Ok(false);
1375 }
1376 let calldata = Bytes::from(IERC20::allowanceCall { owner, spender }.abi_encode());
1377 let (owner_word, spender_word) = (owner.into_word(), spender.into_word());
1378 self.mock_slot_driving(token, calldata, amount, move |probe, _ret| {
1379 probe
1380 .accesses(&[owner_word, spender_word])
1381 .into_iter()
1382 .filter(|a| a.keyed_by(owner_word) && a.keyed_by(spender_word))
1383 .max_by_key(|a| (a.depth, a.confidence))
1384 .map(|a| (a.slot, a.value))
1385 })
1386 }
1387
1388 /// Mock the return value of a single-word view call by finding the storage
1389 /// slot that drives it and overriding that slot — overlay-local.
1390 ///
1391 /// Runs `to.calldata`, identifies the `SLOAD` whose loaded value equals the
1392 /// call's returned word (see
1393 /// [`HashStorageProbe::slots_returning`](crate::mapping_probe::HashStorageProbe::slots_returning)),
1394 /// writes `desired` there, and verifies the call now returns `desired`. Works
1395 /// for balances, allowances, `totalSupply`, and any getter that returns a
1396 /// single stored word. Returns `Ok(false)` (leaving no stray write) when the
1397 /// return is computed from more than one slot, so it can't be set by a single
1398 /// override.
1399 pub fn mock_view(
1400 &mut self,
1401 to: Address,
1402 calldata: Bytes,
1403 desired: U256,
1404 ) -> SimulationResult<bool> {
1405 self.mock_slot_driving(to, calldata, desired, |probe, ret| {
1406 probe
1407 .slots_returning(ret)
1408 .into_iter()
1409 .next()
1410 .map(|slot| (slot, ret))
1411 })
1412 }
1413
1414 /// Typed [`mock_view`](Self::mock_view): mock the `desired` return of a
1415 /// [`SolCall`] getter that returns a single word.
1416 ///
1417 /// ```no_run
1418 /// # use std::sync::Arc;
1419 /// # use alloy_primitives::{Address, U256};
1420 /// # use alloy_sol_types::sol;
1421 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
1422 /// # sol! { interface IErc20 { function totalSupply() returns (uint256); } }
1423 /// # fn run(mut overlay: EvmOverlay, token: Address) -> Result<(), Box<dyn std::error::Error>> {
1424 /// overlay.mock_call(token, IErc20::totalSupplyCall {}, U256::from(1_000u64))?;
1425 /// # Ok(()) }
1426 /// ```
1427 pub fn mock_call<C: SolCall>(
1428 &mut self,
1429 to: Address,
1430 call: C,
1431 desired: U256,
1432 ) -> SimulationResult<bool> {
1433 self.mock_view(to, Bytes::from(call.abi_encode()), desired)
1434 }
1435
1436 /// Extract the leading 32-byte word of a successful call's return data.
1437 fn success_word(result: &ExecutionResult) -> Option<U256> {
1438 match result {
1439 ExecutionResult::Success { output, .. } => {
1440 let data = output.data();
1441 (data.len() >= 32).then(|| U256::from_be_slice(&data[..32]))
1442 }
1443 _ => None,
1444 }
1445 }
1446
1447 /// Shared core for the `mock_*` methods: discover the slot driving
1448 /// `to.calldata`'s return via `choose`, override it to `desired`, verify, and
1449 /// restore the slot on a failed verify so a mis-pick leaves no stray write.
1450 fn mock_slot_driving<F>(
1451 &mut self,
1452 to: Address,
1453 calldata: Bytes,
1454 desired: U256,
1455 choose: F,
1456 ) -> SimulationResult<bool>
1457 where
1458 F: FnOnce(&HashStorageProbe, U256) -> Option<(B256, U256)>,
1459 {
1460 let (result, probe) = self.call_raw_with_inspector(
1461 Address::ZERO,
1462 to,
1463 calldata.clone(),
1464 &TxConfig::default(),
1465 HashStorageProbe::new(),
1466 false,
1467 )?;
1468 let Some(ret) = Self::success_word(&result) else {
1469 return Ok(false);
1470 };
1471 let Some((slot, prev)) = choose(&probe, ret) else {
1472 return Ok(false);
1473 };
1474 let slot_u = U256::from_be_bytes(slot.0);
1475 self.override_slot(to, slot_u, desired);
1476
1477 let (verify, _) = self.call_raw_with_inspector(
1478 Address::ZERO,
1479 to,
1480 calldata,
1481 &TxConfig::default(),
1482 HashStorageProbe::new(),
1483 false,
1484 )?;
1485 if Self::success_word(&verify) == Some(desired) {
1486 Ok(true)
1487 } else {
1488 self.override_slot(to, slot_u, prev); // undo the mis-pick
1489 Ok(false)
1490 }
1491 }
1492}
1493
1494impl revm::database_interface::DatabaseCommit for EvmOverlay {
1495 fn commit(&mut self, changes: alloy_primitives::map::HashMap<Address, revm::state::Account>) {
1496 for (address, account) in changes {
1497 self.dirty_accounts.insert(address, account.info);
1498 let storage = self.dirty_storage.entry(address).or_default();
1499 for (slot, value) in account.storage {
1500 storage.insert(slot, value.present_value);
1501 }
1502 }
1503 }
1504}
1505
1506impl Database for EvmOverlay {
1507 type Error = DatabaseError;
1508
1509 fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
1510 // 1. Check dirty layer
1511 if let Some(info) = self.dirty_accounts.get(&address) {
1512 return Ok(Some(info.clone()));
1513 }
1514 // 2. Check snapshot (O(1) HashMap lookup, no locks). `account_info` folds
1515 // the two snapshot tiers (overlay ▸ base) and already short-circuits a
1516 // NotExisting account to None — it must NOT fall through to the ext_db,
1517 // mirroring revm `DbAccount::info()` and the live `EvmCache` read.
1518 if self.snapshot.accounts_not_existing.contains(&address) {
1519 return Ok(None);
1520 }
1521 if let Some(info) = self.snapshot.account_info(address) {
1522 return Ok(Some(info.clone()));
1523 }
1524 // 3. RPC fallback
1525 if let Some(ref ext_db) = self.ext_db {
1526 let info = ext_db.basic_ref(address)?;
1527 if let Some(ref info) = info {
1528 self.dirty_accounts.insert(address, info.clone());
1529 }
1530 return Ok(info);
1531 }
1532 self.missing_state.accounts.insert(address);
1533 Ok(None)
1534 }
1535
1536 fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error> {
1537 // Check dirty accounts first
1538 for info in self.dirty_accounts.values() {
1539 if info.code_hash == code_hash
1540 && let Some(code) = &info.code
1541 {
1542 return Ok(code.clone());
1543 }
1544 }
1545 // Check the snapshot's code index (overlay ▸ base).
1546 if let Some(code) = self.snapshot.code(code_hash) {
1547 return Ok(code.clone());
1548 }
1549 // RPC fallback
1550 if let Some(ref ext_db) = self.ext_db {
1551 return ext_db.code_by_hash_ref(code_hash);
1552 }
1553 self.missing_state.code_hashes.insert(code_hash);
1554 Ok(Bytecode::default())
1555 }
1556
1557 fn storage(&mut self, address: Address, index: U256) -> Result<U256, Self::Error> {
1558 // 1. Check dirty layer
1559 if let Some(account_storage) = self.dirty_storage.get(&address)
1560 && let Some(value) = account_storage.get(&index)
1561 {
1562 return Ok(*value);
1563 }
1564 // 2. Check snapshot (O(1)). `storage_value` folds the two tiers (overlay ▸
1565 // cleared-as-ZERO ▸ base); a cleared account's absent slot reads ZERO
1566 // and must NOT fall through to the ext_db, mirroring the live EVM SLOAD
1567 // for a StorageCleared/NotExisting account.
1568 if let Some(value) = self.snapshot.storage_value(address, index) {
1569 return Ok(value);
1570 }
1571 // 3. RPC fallback
1572 if let Some(ref ext_db) = self.ext_db {
1573 let value = ext_db.storage_ref(address, index)?;
1574 self.dirty_storage
1575 .entry(address)
1576 .or_default()
1577 .insert(index, value);
1578 return Ok(value);
1579 }
1580 self.missing_state.storage.insert((address, index));
1581 Ok(U256::ZERO)
1582 }
1583
1584 fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error> {
1585 if let Some(hash) = self.snapshot.block_hashes.get(&number) {
1586 return Ok(*hash);
1587 }
1588 if let Some(ref ext_db) = self.ext_db {
1589 return ext_db.block_hash_ref(number);
1590 }
1591 // A hash that was not resident when the snapshot was taken cannot be
1592 // fetched by an RPC-disconnected overlay, so `BLOCKHASH` resolves to
1593 // ZERO. The fallback is recorded so readiness validation fails closed
1594 // instead of confirming control flow that may depend on the real hash.
1595 self.blockhash_zero_fallback = true;
1596 self.missing_state.block_hashes.insert(number);
1597 Ok(B256::ZERO)
1598 }
1599}
1600
1601fn extract_access_list(state: &revm::state::EvmState) -> AccessList {
1602 let items: Vec<AccessListItem> = state
1603 .iter()
1604 .filter(|(_, account)| account.is_touched())
1605 .map(|(address, account)| AccessListItem {
1606 address: *address,
1607 storage_keys: account
1608 .storage
1609 .keys()
1610 .map(|slot| B256::from(*slot))
1611 .collect(),
1612 })
1613 .collect();
1614 AccessList(items)
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619 use super::*;
1620 use crate::cache::snapshot::BaseState;
1621 use revm::primitives::hardfork::SpecId;
1622 use std::collections::HashSet;
1623
1624 /// Build a two-tier `EvmSnapshot` whose cold base holds the given accounts,
1625 /// storage, and code, with an empty hot overlay — the shape
1626 /// `snapshot_deep_clone` produces. The `Arc`-per-account storage of the
1627 /// base is built from the plain per-account maps.
1628 fn snap(
1629 accounts: HashMap<Address, AccountInfo>,
1630 storage: HashMap<Address, HashMap<U256, U256>>,
1631 code_by_hash: HashMap<B256, Bytecode>,
1632 block_hashes: HashMap<u64, B256>,
1633 ) -> Arc<EvmSnapshot> {
1634 let base = BaseState {
1635 accounts,
1636 storage: storage
1637 .into_iter()
1638 .map(|(addr, slots)| (addr, Arc::new(slots)))
1639 .collect(),
1640 code_by_hash,
1641 };
1642 Arc::new(EvmSnapshot {
1643 base: Arc::new(base),
1644 overlay_accounts: HashMap::new(),
1645 overlay_storage: HashMap::new(),
1646 overlay_code_by_hash: HashMap::new(),
1647 storage_cleared: HashSet::new(),
1648 accounts_not_existing: HashSet::new(),
1649 block_hashes,
1650 block_context_hash: None,
1651 block_number: None,
1652 basefee: None,
1653 coinbase: None,
1654 prevrandao: None,
1655 gas_limit: None,
1656 chain_id: 42161,
1657 timestamp: None,
1658 spec_id: SpecId::CANCUN,
1659 shared_memory_capacity: 64_000,
1660 })
1661 }
1662
1663 #[test]
1664 fn test_overlay_is_send() {
1665 fn assert_send<T: Send>() {}
1666 assert_send::<EvmOverlay>();
1667 }
1668
1669 #[test]
1670 fn blockhash_zero_fallback_flags_only_unresolved_reads() {
1671 let known = B256::repeat_byte(0xAB);
1672 let snapshot = snap(
1673 HashMap::new(),
1674 HashMap::new(),
1675 HashMap::new(),
1676 HashMap::from([(5u64, known)]),
1677 );
1678 let mut overlay = EvmOverlay::new(snapshot, None);
1679
1680 // A snapshot-provided hash resolves for real: no flag.
1681 assert_eq!(overlay.block_hash(5).unwrap(), known);
1682 assert!(!overlay.blockhash_zero_fallback());
1683
1684 // An untracked number falls back to ZERO and is flagged so the
1685 // freshness validator can fail closed.
1686 assert_eq!(overlay.block_hash(6).unwrap(), B256::ZERO);
1687 assert!(overlay.blockhash_zero_fallback());
1688
1689 // The flag is per-simulation state: reset clears it.
1690 overlay.reset();
1691 assert!(!overlay.blockhash_zero_fallback());
1692 }
1693
1694 #[test]
1695 fn test_overlay_basic_from_snapshot() {
1696 let mut accounts = HashMap::new();
1697 let info = AccountInfo {
1698 balance: U256::from(1000),
1699 nonce: 1,
1700 code_hash: B256::ZERO,
1701 code: None,
1702 account_id: None,
1703 };
1704 let addr = Address::repeat_byte(0x01);
1705 accounts.insert(addr, info);
1706
1707 let snapshot = snap(accounts, HashMap::new(), HashMap::new(), HashMap::new());
1708
1709 let mut overlay = EvmOverlay::new(snapshot, None);
1710 let result = overlay.basic(addr).unwrap();
1711 assert!(result.is_some());
1712 assert_eq!(result.unwrap().balance, U256::from(1000));
1713 }
1714
1715 #[test]
1716 fn test_overlay_storage_from_snapshot() {
1717 let addr = Address::repeat_byte(0x01);
1718 let slot = U256::from(42);
1719 let value = U256::from(999);
1720
1721 let mut storage = HashMap::new();
1722 let mut account_storage = HashMap::new();
1723 account_storage.insert(slot, value);
1724 storage.insert(addr, account_storage);
1725
1726 let snapshot = snap(HashMap::new(), storage, HashMap::new(), HashMap::new());
1727
1728 let mut overlay = EvmOverlay::new(snapshot, None);
1729 let result = overlay.storage(addr, slot).unwrap();
1730 assert_eq!(result, value);
1731 }
1732
1733 #[test]
1734 fn test_overlay_dirty_overrides_snapshot() {
1735 let addr = Address::repeat_byte(0x01);
1736 let slot = U256::from(42);
1737
1738 let mut storage = HashMap::new();
1739 let mut account_storage = HashMap::new();
1740 account_storage.insert(slot, U256::from(100));
1741 storage.insert(addr, account_storage);
1742
1743 let snapshot = snap(HashMap::new(), storage, HashMap::new(), HashMap::new());
1744
1745 let mut overlay = EvmOverlay::new(snapshot, None);
1746
1747 // Write to dirty layer
1748 overlay
1749 .dirty_storage
1750 .entry(addr)
1751 .or_default()
1752 .insert(slot, U256::from(200));
1753
1754 // Should read dirty value, not snapshot
1755 let result = overlay.storage(addr, slot).unwrap();
1756 assert_eq!(result, U256::from(200));
1757 }
1758
1759 #[test]
1760 fn test_overlay_missing_returns_zero() {
1761 let snapshot = snap(
1762 HashMap::new(),
1763 HashMap::new(),
1764 HashMap::new(),
1765 HashMap::new(),
1766 );
1767
1768 let mut overlay = EvmOverlay::new(snapshot, None);
1769 let addr = Address::repeat_byte(0x99);
1770 let result = overlay.storage(addr, U256::from(1)).unwrap();
1771 assert_eq!(result, U256::ZERO);
1772
1773 let account = overlay.basic(addr).unwrap();
1774 assert!(account.is_none());
1775 }
1776
1777 #[test]
1778 fn test_overlay_code_by_hash_from_snapshot() {
1779 let code = Bytecode::new_raw(Bytes::from(vec![0x60, 0x00, 0x60, 0x00]));
1780 let hash = code.hash_slow();
1781
1782 let mut code_by_hash = HashMap::new();
1783 code_by_hash.insert(hash, code.clone());
1784
1785 let snapshot = snap(HashMap::new(), HashMap::new(), code_by_hash, HashMap::new());
1786
1787 let mut overlay = EvmOverlay::new(snapshot, None);
1788 let result = overlay.code_by_hash(hash).unwrap();
1789 assert_eq!(result.len(), 4);
1790 }
1791
1792 #[test]
1793 fn test_overlay_block_hash() {
1794 let mut block_hashes = HashMap::new();
1795 let hash = B256::repeat_byte(0xAB);
1796 block_hashes.insert(42u64, hash);
1797
1798 let snapshot = snap(HashMap::new(), HashMap::new(), HashMap::new(), block_hashes);
1799
1800 let mut overlay = EvmOverlay::new(snapshot, None);
1801 assert_eq!(overlay.block_hash(42).unwrap(), hash);
1802 assert_eq!(overlay.block_hash(99).unwrap(), B256::ZERO);
1803 }
1804}