Skip to main content

evm_fork_cache/cache/
mod.rs

1mod binary_state;
2mod bytecode;
3mod journal_access_list;
4mod metadata;
5pub mod overlay;
6pub mod slot_observations;
7pub mod snapshot;
8pub(crate) mod versioned;
9
10pub use binary_state::{load_binary_state, save_binary_state};
11pub use metadata::{CacheConfig, ImmutableDataCache};
12pub use overlay::EvmOverlay;
13pub use slot_observations::SlotObservationTracker;
14pub use snapshot::EvmSnapshot;
15
16use std::{
17    cell::RefCell,
18    collections::{HashMap, HashSet},
19    fs,
20    rc::Rc,
21    sync::{
22        Arc, Mutex,
23        atomic::{AtomicU8, Ordering},
24    },
25    time::{SystemTime, UNIX_EPOCH},
26};
27
28use alloy_consensus::BlockHeader;
29use alloy_eips::eip2930::AccessList;
30use alloy_eips::{BlockId, BlockNumberOrTag};
31use alloy_network::BlockResponse;
32use alloy_primitives::{Address, B256, Bytes, I256, Log, TxKind, U256, keccak256};
33use alloy_provider::{Provider, network::AnyNetwork};
34use alloy_rpc_types_eth::TransactionRequest;
35use alloy_sol_types::{SolCall, SolValue, sol};
36use anyhow::{Context as _, Result, anyhow};
37use foundry_fork_db::{BlockchainDb, SharedBackend, cache::BlockchainDbMeta};
38use revm::{
39    Context, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainBuilder, MainContext,
40    context::{BlockEnv, CfgEnv, Journal, LocalContext, TxEnv, result::ExecutionResult},
41    context_interface::JournalTr,
42    database::{AccountState, CacheDB},
43    primitives::hardfork::SpecId,
44    state::{Account, AccountInfo, Bytecode},
45};
46use tracing::{debug, instrument, trace, warn};
47
48use crate::access_set::StorageAccessList;
49use crate::errors::{SimError, SimulationError, SimulationResult};
50use crate::freshness::{SlotChange, SlotFetch, SlotOutcome};
51use crate::inspector::TransferInspector;
52use crate::state_update::{
53    AccountChange, AccountPatch, PurgeRecord, PurgeScope, SkippedAccountPatch, SkippedBalanceDelta,
54    SkippedDelta, SkippedMask, SlotDelta, StateDiff, StateUpdate,
55};
56
57use bytecode::BytecodeCache;
58use journal_access_list::{extract_access_list, merge_access_lists};
59
60/// Re-export AnyNetwork for callers that need to construct providers.
61pub use alloy_provider::network::AnyNetwork as AnyNetworkType;
62
63/// The database type used by the EVM cache.
64/// CacheDB wraps SharedBackend which lazily fetches data from RPC on-demand.
65pub type ForkCacheDB = CacheDB<SharedBackend>;
66
67/// Callback for making direct RPC `eth_call` requests, bypassing revm simulation.
68/// Used when batch-querying many contracts where revm's lazy storage fetching would
69/// be prohibitively slow (e.g. querying 500+ gauge contracts).
70pub type RpcCallFn = Arc<dyn Fn(Address, Bytes) -> Result<Bytes> + Send + Sync>;
71
72/// Callback for batch-fetching storage slots directly from RPC, bypassing SharedBackend.
73///
74/// Used by callers that need bulk storage reads without many individual channel
75/// round-trips through SharedBackend. Fires concurrent `eth_getStorageAt` calls
76/// directly via the provider and returns results for bulk injection into
77/// BlockchainDb.
78///
79/// The second argument pins the fetch to a specific block: `Some(block)` fetches
80/// at exactly that block, while `None` uses the fetcher's configured block (the
81/// cache's currently-pinned block). The freshness validator passes the block its
82/// snapshot was built from, so a concurrent [`EvmCache::set_block`] cannot make
83/// the deferred fetch read a *different* block than the snapshot it is compared
84/// against.
85///
86/// **Contract:** an implementation must return **exactly one** result tuple per
87/// requested `(address, slot)` (order does not matter). Callers — `verify_slots`,
88/// `reconcile_slots`, and the cold-start verify/probe paths — derive their
89/// per-slot outcomes from the returned tuples, so a fetcher that drops, dedups,
90/// reorders-and-truncates, or duplicates entries breaks the "one outcome per
91/// requested slot" guarantee those APIs document.
92pub type StorageBatchFetchFn = Arc<
93    dyn Fn(Vec<(Address, U256)>, Option<BlockId>) -> Vec<(Address, U256, Result<U256>)>
94        + Send
95        + Sync,
96>;
97
98/// Return a tokio runtime [`Handle`] suitable for `block_in_place` + `block_on`,
99/// or an error describing why one is unavailable.
100///
101/// The RPC-backed callbacks ([`RpcCallFn`], [`StorageBatchFetchFn`]) drive async
102/// work synchronously via `tokio::task::block_in_place`. That helper panics on a
103/// current-thread runtime, and `Handle::current()` panics when no runtime is
104/// present. To avoid panicking deep inside a callback, callers use this guard to
105/// degrade to a typed error instead.
106///
107/// Requires a **multi-thread** tokio runtime.
108pub(crate) fn block_in_place_handle() -> Result<tokio::runtime::Handle> {
109    match tokio::runtime::Handle::try_current() {
110        Ok(handle) => match handle.runtime_flavor() {
111            tokio::runtime::RuntimeFlavor::CurrentThread => Err(anyhow!(
112                "evm-fork-cache RPC operations require a multi-thread tokio runtime; \
113                 found a current-thread runtime (block_in_place is not supported there). \
114                 Build the runtime with `tokio::runtime::Builder::new_multi_thread()` \
115                 or annotate with `#[tokio::main(flavor = \"multi_thread\")]`"
116            )),
117            _ => Ok(handle),
118        },
119        Err(e) => Err(anyhow!(
120            "evm-fork-cache RPC operations require a running multi-thread tokio runtime: {e}"
121        )),
122    }
123}
124
125pub(crate) fn unix_timestamp_secs_saturating(time: SystemTime) -> u64 {
126    time.duration_since(UNIX_EPOCH)
127        .map(|duration| duration.as_secs())
128        .unwrap_or(0)
129}
130
131/// Read a storage slot from already-borrowed layers (`account_state`-aware),
132/// mirroring [`EvmCache::cached_storage_value`] but operating on a held backend
133/// storage guard rather than re-locking. Shared by the batched slot-run fast-path
134/// ([`EvmCache::apply_slot_run`]) so the same EVM-SLOAD semantics hold inside the
135/// held guard: the overlay slot wins; a `StorageCleared`/`NotExisting` overlay
136/// account reads a missing slot as ZERO (the backend is **not** consulted);
137/// otherwise it falls through to the backend.
138fn read_slot_account_state_aware<S1, S2>(
139    overlay: &std::collections::HashMap<Address, revm::database::DbAccount, S1>,
140    storage: &std::collections::HashMap<Address, foundry_fork_db::cache::StorageInfo, S2>,
141    address: Address,
142    slot: U256,
143) -> Option<U256>
144where
145    S1: std::hash::BuildHasher,
146    S2: std::hash::BuildHasher,
147{
148    if let Some(db_account) = overlay.get(&address) {
149        if let Some(value) = db_account.storage.get(&slot) {
150            return Some(*value);
151        }
152        if matches!(
153            db_account.account_state,
154            AccountState::StorageCleared | AccountState::NotExisting
155        ) {
156            return Some(U256::ZERO);
157        }
158    }
159    storage.get(&address).and_then(|s| s.get(&slot).copied())
160}
161
162/// Write a storage slot into already-borrowed layers, mirroring
163/// [`EvmCache::write_slot_through`] but operating on a held backend storage guard.
164/// Backend (layer 2) is always written; the overlay (layer 1) is written only if
165/// an overlay account already exists (never materialize a new overlay account).
166fn write_slot_into<S1, S2>(
167    overlay: &mut std::collections::HashMap<Address, revm::database::DbAccount, S1>,
168    storage: &mut std::collections::HashMap<Address, foundry_fork_db::cache::StorageInfo, S2>,
169    address: Address,
170    slot: U256,
171    value: U256,
172) where
173    S1: std::hash::BuildHasher,
174    S2: std::hash::BuildHasher + Default,
175{
176    storage.entry(address).or_default().insert(slot, value);
177    if let Some(db_account) = overlay.get_mut(&address) {
178        db_account.storage.insert(slot, value);
179    }
180}
181
182fn account_patch_is_empty(patch: &AccountPatch) -> bool {
183    patch.balance.is_none() && patch.nonce.is_none() && patch.code.is_none()
184}
185
186static CACHE_SPEED_MODE: AtomicU8 = AtomicU8::new(CacheSpeedMode::Slow as u8);
187
188/// Runtime tuning profile for cache-side batch storage fetches.
189///
190/// Selects the per-batch size and concurrency used by [`StorageBatchFetchFn`]:
191/// faster modes send larger batches with more in-flight HTTP requests, slower
192/// modes throttle to avoid RPC rate-limiting (e.g. HTTP 429 on Base). The
193/// selected mode is **process-global** state, set via [`set_cache_speed_mode`]
194/// and read via [`cache_speed_mode`]; it affects every cache in the process.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196#[repr(u8)]
197pub enum CacheSpeedMode {
198    /// Largest batches, highest concurrency — fastest, most likely to trip rate limits.
199    Fast = 0,
200    /// Moderate batch size and concurrency.
201    Normal = 1,
202    /// Conservative batch size and concurrency. The default.
203    Slow = 2,
204    /// Smallest batches, single in-flight request — slowest, gentlest on the RPC provider.
205    XSlow = 3,
206}
207
208impl CacheSpeedMode {
209    fn from_u8(value: u8) -> Self {
210        match value {
211            0 => Self::Fast,
212            1 => Self::Normal,
213            2 => Self::Slow,
214            3 => Self::XSlow,
215            _ => Self::Slow,
216        }
217    }
218}
219
220/// Set the process-global cache batch-fetch speed profile.
221///
222/// This mutates a single static shared by every cache in the process, so it
223/// affects all in-flight and future batch fetches, not just one [`EvmCache`].
224/// Read the current value with [`cache_speed_mode`].
225pub fn set_cache_speed_mode(mode: CacheSpeedMode) {
226    CACHE_SPEED_MODE.store(mode as u8, Ordering::Relaxed);
227}
228
229/// Return the current process-global cache batch-fetch speed profile.
230///
231/// Defaults to [`CacheSpeedMode::Slow`] until changed via
232/// [`set_cache_speed_mode`]. The value is shared across all caches in the
233/// process.
234pub fn cache_speed_mode() -> CacheSpeedMode {
235    CacheSpeedMode::from_u8(CACHE_SPEED_MODE.load(Ordering::Relaxed))
236}
237
238/// Behavior when overriding code at a target account that is not known to the cache/backend.
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum MissingTargetBehavior {
241    /// Return an error if the target account cannot be loaded.
242    Error,
243    /// Create a default account with the replacement code.
244    Create,
245}
246
247/// Per-call transaction-environment overrides for a simulation.
248///
249/// `Default` reproduces the read-only behavior of the plain `call_raw`
250/// (zero value, default gas/nonce). Use the `*_with` call variants to supply
251/// these — e.g. to simulate a payable function, a native-ETH transfer, or a
252/// gas-bounded call. Balance affordability checks are disabled in the
253/// simulator, so a non-zero `value` does not require the caller to be funded.
254#[derive(Debug, Clone, Default)]
255pub struct TxConfig {
256    /// Native value (wei) sent with the call. Set this to simulate a payable
257    /// function or a native-ETH transfer. Balance checks are disabled in the
258    /// simulator, so the caller need not be funded for a non-zero value.
259    pub value: U256,
260    /// Gas limit for the call. `None` uses revm's default. Set this to model a
261    /// gas-bounded call (e.g. to observe out-of-gas behavior).
262    pub gas_limit: Option<u64>,
263    /// Gas price (wei) for the call. `None` uses revm's default. Rarely needed
264    /// because base-fee checks are disabled in the simulator.
265    pub gas_price: Option<u128>,
266    /// Sender nonce. `None` lets the simulator pick; nonce checks are disabled,
267    /// so this is only worth setting when a contract reads the nonce explicitly.
268    pub nonce: Option<u64>,
269    /// EIP-2930 access list to pre-warm accounts and storage slots for this
270    /// call. Pre-warming changes EIP-2929 gas accounting; supply it when
271    /// reproducing the gas cost of a transaction that carried an access list.
272    pub access_list: Option<AccessList>,
273}
274
275/// Fluent builder for [`EvmCache`].
276///
277/// A readable alternative to the positional [`EvmCache::with_cache`]
278/// constructor. Defaults: latest block, no disk cache, [`SpecId::CANCUN`].
279///
280/// ```no_run
281/// # use std::sync::Arc;
282/// # use alloy_provider::{ProviderBuilder, network::AnyNetwork};
283/// # use revm::primitives::hardfork::SpecId;
284/// # use evm_fork_cache::cache::EvmCache;
285/// # async fn example() -> anyhow::Result<()> {
286/// let provider = ProviderBuilder::new()
287///     .network::<AnyNetwork>()
288///     .connect_http("https://example-rpc.invalid".parse()?);
289/// let cache = EvmCache::builder(Arc::new(provider))
290///     .latest_block()
291///     .spec(SpecId::CANCUN)
292///     .build()
293///     .await;
294/// # let _ = cache;
295/// # Ok(())
296/// # }
297/// ```
298pub struct EvmCacheBuilder<P> {
299    provider: Arc<P>,
300    block: BlockId,
301    cache_config: Option<CacheConfig>,
302    spec_id: SpecId,
303    shared_memory_capacity: SharedMemoryCapacity,
304    chain_id: Option<u64>,
305}
306
307impl<P> EvmCacheBuilder<P>
308where
309    P: Provider<AnyNetwork> + 'static,
310{
311    /// Start a builder over the given provider.
312    pub fn new(provider: Arc<P>) -> Self {
313        Self {
314            provider,
315            block: BlockId::latest(),
316            cache_config: None,
317            spec_id: SpecId::CANCUN,
318            shared_memory_capacity: SharedMemoryCapacity::default(),
319            chain_id: None,
320        }
321    }
322
323    /// Pin simulations and RPC fetches to a specific block.
324    ///
325    /// Use this to fork at a fixed height for reproducible simulation. Without
326    /// a call to [`block`](Self::block) or [`latest_block`](Self::latest_block)
327    /// the builder defaults to the latest block at [`build`](Self::build) time.
328    pub fn block(mut self, block: BlockId) -> Self {
329        self.block = block;
330        self
331    }
332
333    /// Pin to the latest block.
334    ///
335    /// The height is resolved when [`build`](Self::build) fetches the block
336    /// header, so the cache forks at whatever was latest at construction. Use
337    /// [`block`](Self::block) instead to pin a fixed, reproducible height.
338    pub fn latest_block(mut self) -> Self {
339        self.block = BlockId::latest();
340        self
341    }
342
343    /// Set the EVM hardfork spec (must match the chain's execution layer).
344    pub fn spec(mut self, spec_id: SpecId) -> Self {
345        self.spec_id = spec_id;
346        self
347    }
348
349    /// Set the chain ID reported to simulations via the `CHAINID` opcode.
350    ///
351    /// **Recommended.** This is the explicit, authoritative way to set the chain
352    /// ID. If left unset, [`build`](Self::build) infers it from the provider
353    /// (`eth_chainId`), falling back to `1` (Ethereum mainnet) only if that query
354    /// fails. A disk [`cache_config`](Self::cache_config) also carries a
355    /// `chain_id` (which additionally namespaces the on-disk cache directory);
356    /// when both are set, the value passed here wins for the `CHAINID` opcode, so
357    /// keep them consistent.
358    pub fn chain_id(mut self, chain_id: u64) -> Self {
359        self.chain_id = Some(chain_id);
360        self
361    }
362
363    /// Enable disk-backed caching with the given configuration.
364    ///
365    /// Supplying a [`CacheConfig`] turns on persistence of EVM state, bytecodes,
366    /// and immutable data under the configured chain directory; the cache is
367    /// loaded on [`build`](Self::build) and flushed on drop. Omit it for a
368    /// purely in-memory cache backed solely by RPC.
369    pub fn cache_config(mut self, cache_config: CacheConfig) -> Self {
370        self.cache_config = Some(cache_config);
371        self
372    }
373
374    /// Set how much EVM shared memory to pre-allocate per simulation context.
375    ///
376    /// Defaults to [`SharedMemoryCapacity::Fixed`] with `64 * 1024` bytes
377    /// (65,536 bytes).
378    /// Use `Fixed(n)` to pin a size, or [`SharedMemoryCapacity::Auto`] to size it
379    /// from the chain state loaded at [`build`](Self::build) time (e.g. a bincode
380    /// state file supplied via [`cache_config`](Self::cache_config)). See
381    /// [`SharedMemoryCapacity`] for the trade-offs.
382    pub fn shared_memory_capacity(mut self, capacity: SharedMemoryCapacity) -> Self {
383        self.shared_memory_capacity = capacity;
384        self
385    }
386
387    /// Build the [`EvmCache`], fetching the pinned block's header for context.
388    ///
389    /// If a chain ID was not set via [`chain_id`](Self::chain_id), it is inferred
390    /// from the provider (`eth_chainId`); see [`chain_id`](Self::chain_id) for the
391    /// full resolution order.
392    pub async fn build(self) -> EvmCache {
393        let explicit_chain_id = self.chain_id;
394        let mut cache = EvmCache::with_cache_capacity(
395            self.provider,
396            self.block,
397            self.cache_config,
398            self.spec_id,
399            self.shared_memory_capacity,
400        )
401        .await;
402        // An explicit builder value is authoritative for the `CHAINID` opcode and
403        // overrides both the inferred value and any `cache_config` chain id.
404        if let Some(chain_id) = explicit_chain_id {
405            cache.set_chain_id(chain_id);
406        }
407        cache
408    }
409}
410
411type CacheEvm<'a> = revm::MainnetEvm<
412    Context<BlockEnv, TxEnv, CfgEnv, &'a mut ForkCacheDB, Journal<&'a mut ForkCacheDB>, ()>,
413>;
414type InspectorCacheEvm<'a, INSP> = revm::MainnetEvm<
415    Context<BlockEnv, TxEnv, CfgEnv, &'a mut ForkCacheDB, Journal<&'a mut ForkCacheDB>, ()>,
416    INSP,
417>;
418
419/// Default initial capacity for the EVM shared-memory (working-memory) buffer.
420/// 64 KiB (65,536 bytes), chosen from profiling a state-heavy workload (16x the
421/// revm default of 4 KiB) so simulations rarely reallocate. Exposed for tuning via
422/// [`SharedMemoryCapacity`].
423const DEFAULT_SHARED_MEMORY_CAPACITY: usize = 64 * 1024;
424
425/// How much EVM shared memory (per-context working memory) to pre-allocate for
426/// simulations.
427///
428/// revm grows its shared memory on demand during execution; pre-allocating just
429/// avoids repeated reallocations when simulations touch a lot of memory — the
430/// original motivation was a state-heavy workload where resizing was hot. The
431/// trade-off cuts both ways: a wide parallel fan-out of *small* simulations pays
432/// this much memory per overlay, so general users may want a smaller `Fixed` size,
433/// while state-heavy users can raise it or let it auto-size from the loaded state.
434///
435/// The default is `Fixed(64 * 1024)` (65,536 bytes). Configure it on
436/// [`EvmCacheBuilder::shared_memory_capacity`].
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub enum SharedMemoryCapacity {
439    /// Pre-allocate exactly this many bytes. The [`Default`] is
440    /// `Fixed(64 * 1024)`.
441    Fixed(usize),
442    /// Size the buffer from the amount of chain state loaded into the cache at
443    /// construction (e.g. from a bincode state file via
444    /// [`CacheConfig`]/[`EvmCacheBuilder::cache_config`]), clamped to a sane
445    /// floor/ceiling. Falls back to the floor when nothing is loaded.
446    ///
447    /// This is a heuristic proxy — persisted state size loosely correlates with the
448    /// working-set size of simulations over it, not an exact peak-memory model. Use
449    /// `Fixed` when you have profiled your workload.
450    Auto,
451}
452
453impl Default for SharedMemoryCapacity {
454    fn default() -> Self {
455        Self::Fixed(DEFAULT_SHARED_MEMORY_CAPACITY)
456    }
457}
458
459impl SharedMemoryCapacity {
460    /// Floor for [`Auto`](Self::Auto) (and the default fixed size): 64 KiB
461    /// (65,536 bytes).
462    pub const MIN_AUTO: usize = DEFAULT_SHARED_MEMORY_CAPACITY;
463    /// Ceiling for [`Auto`](Self::Auto): 4 MiB. A simulation that needs more than
464    /// this still works — revm grows the buffer past it on demand.
465    pub const MAX_AUTO: usize = 4 * 1024 * 1024;
466    /// Heuristic proxy: bytes of pre-allocated working memory per loaded storage
467    /// slot. Tune if profiling warrants.
468    const AUTO_BYTES_PER_SLOT: usize = 16;
469
470    /// Resolve to a concrete byte capacity. `loaded_slots` is the number of layer-2
471    /// storage slots present in the cache at construction (0 when nothing is
472    /// loaded); it is consulted only for [`Auto`](Self::Auto).
473    pub(crate) fn resolve(self, loaded_slots: usize) -> usize {
474        match self {
475            Self::Fixed(bytes) => bytes,
476            Self::Auto => loaded_slots
477                .saturating_mul(Self::AUTO_BYTES_PER_SLOT)
478                .clamp(Self::MIN_AUTO, Self::MAX_AUTO),
479        }
480    }
481}
482
483/// EVM cache with lazy-loading RPC backend.
484///
485/// Uses `foundry-fork-db` for intelligent caching and request deduplication.
486/// Storage and account data is fetched on-demand when accessed during EVM execution,
487/// eliminating the need for expensive access list prefetching.
488pub struct EvmCache {
489    backend: SharedBackend,
490    blockchain_db: BlockchainDb,
491    db: ForkCacheDB,
492    token_decimals: HashMap<Address, u8>,
493    block: BlockId,
494    cache_config: Option<CacheConfig>,
495    /// Cache for immutable on-chain data (token decimals).
496    immutable_cache: ImmutableDataCache,
497    /// Optional timestamp override for simulating future blocks.
498    /// When set, EVM simulations use this timestamp instead of the current system time.
499    timestamp_override: Option<u64>,
500    /// Chain ID for EVM simulation (e.g. 42161 for Arbitrum, 1 for Ethereum).
501    chain_id: u64,
502    /// Block number for EVM simulations (NUMBER opcode).
503    /// Fetched from block header during construction. Without this, revm defaults to 0
504    /// which causes contracts that read block.number to execute different code paths.
505    block_number: Option<u64>,
506    /// Base fee per gas for EVM simulations (BASEFEE opcode).
507    /// Fetched from block header during construction.
508    basefee: Option<u64>,
509    /// Block beneficiary for EVM simulations (COINBASE opcode).
510    /// Fetched from the block header; commonly read by MEV/builder tip logic.
511    coinbase: Option<Address>,
512    /// `prevrandao` for EVM simulations (PREVRANDAO opcode), i.e. the header's
513    /// mix hash post-merge. Drives on-chain randomness.
514    prevrandao: Option<B256>,
515    /// Block gas limit for EVM simulations (GASLIMIT opcode).
516    block_gas_limit: Option<u64>,
517    /// Shared memory buffer reused across EVM simulations.
518    /// This avoids repeated allocations and allows measuring peak memory usage.
519    shared_memory_buffer: Rc<RefCell<Vec<u8>>>,
520    /// Optional callback for direct RPC `eth_call` (bypasses revm simulation).
521    /// Set during construction from the provider. Useful for batch operations
522    /// where revm's lazy storage fetching would be too slow.
523    rpc_caller: Option<RpcCallFn>,
524    /// Optional batch storage fetcher that bypasses SharedBackend.
525    /// Captures a provider clone and fires concurrent `eth_getStorageAt` calls directly.
526    storage_batch_fetcher: Option<StorageBatchFetchFn>,
527    /// Shared block ID for the batch storage fetcher closure.
528    /// Updated by `set_block()` so batch fetches always use the current block.
529    batch_block_id: Arc<Mutex<BlockId>>,
530    /// Best-known ERC20 `balanceOf` mapping slot per token contract.
531    ///
532    /// Used by `set_erc20_balance_with_slot_scan` to avoid re-scanning slots
533    /// repeatedly for the same token.
534    erc20_balance_slots: HashMap<Address, U256>,
535    /// EVM hardfork spec for simulations. Must match the chain's current execution
536    /// layer hardfork for accurate gas accounting. Configured per-chain via `evm_spec`
537    /// in `chains.toml`.
538    spec_id: SpecId,
539    /// Memoized, `Arc`-shared flatten of the cold layer-2 index, reused across
540    /// successive [`create_snapshot`](Self::create_snapshot) calls (Pillar A).
541    /// `None` until the first snapshot. Rebuilt copy-on-write by
542    /// [`refresh_base`](Self::refresh_base); never mutated in place once shared.
543    /// Not part of any public API and not serialized.
544    base: Option<Arc<snapshot::BaseState>>,
545    /// Layer-2 addresses changed since `base` was built, folded into the next base
546    /// rebuild. Populated by the base-invalidation sites (write-through, batch
547    /// injects, layer-2 seeding, purges). Not serialized.
548    base_dirty: HashSet<Address>,
549    /// When set, the next [`refresh_base`](Self::refresh_base) rebuilds the base
550    /// from scratch. Set by [`set_block`](Self::set_block) /
551    /// [`repin_to_block`](Self::repin_to_block), which replace layer 2 wholesale.
552    /// Not serialized.
553    base_full_rebuild: bool,
554    /// Per-account layer-2 slot count at the last base build, used by
555    /// [`refresh_base`](Self::refresh_base)'s `O(accounts)` length-scan to detect
556    /// uncontrolled lazy-fetch growth that bypasses the write funnel. Not
557    /// serialized.
558    base_storage_lens: HashMap<Address, usize>,
559    /// Resolved per-context EVM shared-memory pre-allocation (bytes), from the
560    /// [`SharedMemoryCapacity`] at construction (resolving `Auto` against the loaded
561    /// state). Propagated to each [`EvmSnapshot`] so snapshot-backed overlays
562    /// pre-allocate the same amount. See
563    /// [`shared_memory_capacity`](Self::shared_memory_capacity).
564    shared_memory_capacity: usize,
565}
566
567/// Outcome of a balance-delta-tracking simulation.
568///
569/// Produced by [`EvmCache::simulate_call_with_balance_deltas`] and
570/// [`EvmCache::simulate_with_transfer_tracking`]: a successful call together
571/// with the per-token balance changes it caused, its emitted logs, the touched
572/// access list, and its raw return data.
573/// Execution outcome of a simulated call.
574///
575/// Lets a caller distinguish a successful call — even one that emitted no logs,
576/// such as a view call — from a revert or a halt, without guessing from `logs`
577/// or `output`. Revert payloads live in [`CallSimulationResult::output`] and can
578/// be decoded with [`RevertDecoder`](crate::errors::RevertDecoder); only `Halt`
579/// carries extra data here, since its reason has nowhere else to live.
580#[derive(Clone, Debug, PartialEq, Eq)]
581pub enum SimStatus {
582    /// The call returned successfully.
583    Success,
584    /// The call reverted; the revert payload (if any) is in `output`.
585    Revert,
586    /// The call halted (e.g. out of gas, invalid opcode).
587    Halt {
588        /// Debug-formatted halt reason.
589        reason: String,
590    },
591}
592
593#[derive(Clone, Debug)]
594#[non_exhaustive]
595pub struct CallSimulationResult {
596    /// Whether the call succeeded, reverted, or halted.
597    pub status: SimStatus,
598    /// Gas consumed by the (successful) call.
599    pub gas_used: u64,
600    /// Net change in `owner`'s balance per tracked token, as a **signed**
601    /// [`I256`] (`post - pre`): positive means the call increased the balance,
602    /// negative means it decreased it. Tokens not seen by the call may be
603    /// absent or zero.
604    pub token_deltas: HashMap<Address, I256>,
605    /// Logs emitted by the call (in emission order).
606    pub logs: Vec<Log>,
607    /// EIP-2930 access list of all accounts and storage slots touched during simulation.
608    /// Extracted from the EVM journaled state after execution.
609    pub access_list: AccessList,
610    /// Raw return data of the call.
611    ///
612    /// `Success` carries the returned bytes, `Revert` the revert payload, and
613    /// `Halt` an empty slice. This makes a corrected view-call result observable:
614    /// when a re-run reads a changed slot, the new return value differs here even
615    /// if both runs succeed.
616    pub output: Bytes,
617}
618
619sol!(
620    #[sol(rpc)]
621    contract IERC20 {
622        function balanceOf(address target) returns (uint256);
623        function decimals() returns (uint8);
624        function allowance(address owner, address spender) returns (uint256);
625    }
626);
627
628/// Parse an EVM hardfork spec name (e.g. from TOML config) into a revm [`SpecId`].
629///
630/// Accepts revm's canonical names (e.g. `"Cancun"`, `"Shanghai"`, `"Prague"`)
631/// case-insensitively. Falls back to [`SpecId::CANCUN`] for unrecognized values.
632pub fn parse_evm_spec(spec: &str) -> SpecId {
633    // SpecId::from_str expects title-case (e.g. "Cancun"), so normalize the input.
634    let mut chars = spec.chars();
635    let title_case: String = match chars.next() {
636        Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
637        None => String::new(),
638    };
639    title_case.parse::<SpecId>().unwrap_or_else(|_| {
640        warn!(spec, "Unknown EVM spec, defaulting to Cancun");
641        SpecId::CANCUN
642    })
643}
644
645impl EvmCache {
646    /// Start a fluent [`EvmCacheBuilder`] over the given provider.
647    ///
648    /// Preferred over the positional [`with_cache`](Self::with_cache) /
649    /// [`new`](Self::new) constructors for readability.
650    pub fn builder<P>(provider: Arc<P>) -> EvmCacheBuilder<P>
651    where
652        P: Provider<AnyNetwork> + 'static,
653    {
654        EvmCacheBuilder::new(provider)
655    }
656
657    /// Create a new EvmCache with a SharedBackend that lazily fetches from RPC.
658    ///
659    /// The backend spawns a background handler task that manages RPC requests
660    /// and deduplicates concurrent requests for the same data.
661    ///
662    /// # Runtime requirement
663    /// RPC-backed operation requires a **multi-thread** tokio runtime
664    /// (`#[tokio::main(flavor = "multi_thread")]` or
665    /// `tokio::runtime::Builder::new_multi_thread()`). The direct RPC callbacks
666    /// (`eth_call` and batch `eth_getStorageAt`) drive async work synchronously
667    /// via `tokio::task::block_in_place`, which is unsupported on a
668    /// current-thread runtime. On a current-thread runtime those callbacks
669    /// degrade to typed errors rather than panicking.
670    pub async fn new<P>(provider: Arc<P>) -> Self
671    where
672        P: Provider<AnyNetwork> + 'static,
673    {
674        Self::at_block(provider, BlockId::latest()).await
675    }
676
677    /// Create a new EvmCache pinned to an explicit block.
678    ///
679    /// Prefer this over [`new`](Self::new) when reproducibility matters and the
680    /// caller has already chosen the fork block.
681    pub async fn at_block<P>(provider: Arc<P>, block: BlockId) -> Self
682    where
683        P: Provider<AnyNetwork> + 'static,
684    {
685        Self::with_cache(provider, block, None, SpecId::CANCUN).await
686    }
687
688    /// Create a new EvmCache with disk-based caching.
689    ///
690    /// This enables several caching features:
691    /// 1. Unified EVM state: Accounts + storage loaded from `evm_state.bin` (bincode)
692    /// 2. Bytecode caching: Contract bytecodes from `bytecodes.bin`
693    /// 3. Immutable data: Token decimals
694    ///
695    /// # Runtime requirement
696    /// RPC-backed operation requires a **multi-thread** tokio runtime
697    /// (`#[tokio::main(flavor = "multi_thread")]` or
698    /// `tokio::runtime::Builder::new_multi_thread()`). The direct RPC callbacks
699    /// (`eth_call` and batch `eth_getStorageAt`) drive async work synchronously
700    /// via `tokio::task::block_in_place`, which is unsupported on a
701    /// current-thread runtime. On a current-thread runtime those callbacks
702    /// degrade to typed errors rather than panicking.
703    pub async fn with_cache<P>(
704        provider: Arc<P>,
705        block: BlockId,
706        cache_config: Option<CacheConfig>,
707        spec_id: SpecId,
708    ) -> Self
709    where
710        P: Provider<AnyNetwork> + 'static,
711    {
712        Self::with_cache_capacity(
713            provider,
714            block,
715            cache_config,
716            spec_id,
717            SharedMemoryCapacity::default(),
718        )
719        .await
720    }
721
722    /// Like [`with_cache`](Self::with_cache) but takes an explicit
723    /// [`SharedMemoryCapacity`] controlling per-context EVM working-memory
724    /// pre-allocation. This is what [`EvmCacheBuilder::build`] calls; prefer the
725    /// builder. With [`SharedMemoryCapacity::Auto`] the buffer is sized from the
726    /// layer-2 storage loaded at construction (e.g. a bincode state file).
727    pub async fn with_cache_capacity<P>(
728        provider: Arc<P>,
729        block: BlockId,
730        cache_config: Option<CacheConfig>,
731        spec_id: SpecId,
732        shared_memory_capacity: SharedMemoryCapacity,
733    ) -> Self
734    where
735        P: Provider<AnyNetwork> + 'static,
736    {
737        let block_id = block;
738
739        // Fetch the pinned block header for accurate block context (NUMBER,
740        // BASEFEE, COINBASE, PREVRANDAO, GASLIMIT opcodes). Without this, revm
741        // defaults to 0/default values, causing contracts that read block
742        // context to execute different code paths. Use the concrete BlockId the
743        // cache is pinned to so hash pins do not accidentally inherit latest
744        // header context.
745        let (block_number, basefee, coinbase, prevrandao, block_gas_limit) =
746            match provider.get_block(block_id).await {
747                Ok(Some(blk)) => {
748                    let h = blk.header();
749                    (
750                        Some(h.number()),
751                        h.base_fee_per_gas(),
752                        Some(h.beneficiary()),
753                        h.mix_hash(),
754                        Some(h.gas_limit()),
755                    )
756                }
757                Ok(None) => {
758                    debug!("Block header not found for block context initialization");
759                    (None, None, None, None, None)
760                }
761                Err(e) => {
762                    debug!(error = %e, "Failed to fetch block header for block context");
763                    (None, None, None, None, None)
764                }
765            };
766
767        // Ensure cache directory exists
768        if let Some(cfg) = &cache_config {
769            let _ = fs::create_dir_all(cfg.chain_dir());
770        }
771
772        // Try to load EVM state from binary cache (bincode format)
773        let blockchain_db = if let Some(cfg) = &cache_config {
774            let binary_path = cfg.binary_state_cache_path();
775
776            if binary_path.exists() {
777                let meta = BlockchainDbMeta::default();
778                let db = BlockchainDb::new(meta, None);
779                if binary_state::load_binary_state(&db, &binary_path) {
780                    db
781                } else {
782                    let meta = BlockchainDbMeta::default();
783                    BlockchainDb::new(meta, None)
784                }
785            } else {
786                let meta = BlockchainDbMeta::default();
787                BlockchainDb::new(meta, None)
788            }
789        } else {
790            let meta = BlockchainDbMeta::default();
791            BlockchainDb::new(meta, None)
792        };
793
794        // Filter storage by maintain list (if configured)
795        if let Some(cfg) = &cache_config {
796            let has_filter = !cfg.maintain_addresses.is_empty() || !cfg.maintain_slots.is_empty();
797            if has_filter {
798                let mut storage = blockchain_db.storage().write();
799                let before_contracts = storage.len();
800                let before_slots: usize = storage.values().map(|s| s.len()).sum();
801
802                // Remove addresses not in any maintain list
803                let addrs_to_remove: Vec<Address> = storage
804                    .keys()
805                    .filter(|addr| {
806                        !cfg.maintain_addresses.contains(*addr)
807                            && !cfg.maintain_slots.contains_key(*addr)
808                    })
809                    .copied()
810                    .collect();
811                for addr in &addrs_to_remove {
812                    storage.remove(addr);
813                }
814
815                // For maintain_slots addresses: keep only the specified slots
816                for (addr, allowed_slots) in &cfg.maintain_slots {
817                    if let Some(addr_storage) = storage.get_mut(addr) {
818                        addr_storage.retain(|slot, _| allowed_slots.contains(slot));
819                    }
820                }
821
822                let after_contracts = storage.len();
823                let after_slots: usize = storage.values().map(|s| s.len()).sum();
824                drop(storage);
825
826                debug!(
827                    contracts_removed = before_contracts.saturating_sub(after_contracts),
828                    slots_removed = before_slots.saturating_sub(after_slots),
829                    contracts_kept = after_contracts,
830                    slots_kept = after_slots,
831                    "Filtered cached storage by maintain list"
832                );
833            }
834        }
835
836        // Seed bytecodes from the bytecodes.bin cache.
837        // The binary EVM state cache stores accounts without bytecode,
838        // so this is always needed when a cache config is present.
839        if let Some(cfg) = &cache_config {
840            let bytecode_path = cfg.bytecode_cache_path();
841            if let Some(bytecode_cache) = BytecodeCache::load(&bytecode_path) {
842                let loaded_count = Self::seed_bytecodes_from_cache(&blockchain_db, &bytecode_cache);
843                if loaded_count > 0 {
844                    debug!(
845                        count = loaded_count,
846                        path = ?bytecode_path,
847                        "Loaded contract bytecodes from cache"
848                    );
849                }
850            }
851        }
852
853        // Load immutable data cache (token decimals).
854        // This is still needed for validation and metadata lookups
855        let immutable_cache = cache_config
856            .as_ref()
857            .and_then(|cfg| {
858                let path = cfg.immutable_cache_path();
859                ImmutableDataCache::load(&path).inspect(|cache| {
860                    debug!(
861                        token_decimals = cache.token_decimals.len(),
862                        path = ?path,
863                        "Loaded immutable data from cache"
864                    );
865                })
866            })
867            .unwrap_or_default();
868
869        // Pre-populate in-memory token decimals from immutable cache
870        let token_decimals = immutable_cache.token_decimals.clone();
871
872        // Create an RPC callback for direct eth_call before moving provider into backend.
873        // This bypasses revm simulation for batch queries where lazy storage fetching is too slow.
874        let provider_for_rpc = provider.clone();
875        let rpc_caller: RpcCallFn = Arc::new(move |to: Address, calldata: Bytes| {
876            // Guard against panicking inside `block_in_place` on a current-thread
877            // runtime (or when no runtime is present): degrade to a typed error.
878            let handle = block_in_place_handle()?;
879            tokio::task::block_in_place(|| {
880                handle.block_on(async {
881                    let tx = TransactionRequest::default()
882                        .to(to)
883                        .input(alloy_primitives::Bytes::from(calldata.to_vec()).into());
884                    provider_for_rpc
885                        .call(tx.into())
886                        .await
887                        .map_err(|e| anyhow!("{}", e))
888                })
889            })
890        });
891
892        // Create a batch storage fetcher that bypasses SharedBackend for bulk prefetch.
893        // Uses JSON-RPC batch requests to send multiple eth_getStorageAt calls in a
894        // single HTTP request, dramatically reducing round-trip overhead.
895        let provider_for_batch = provider.clone();
896        let batch_block_id = Arc::new(Mutex::new(block_id));
897        let batch_block_ref = batch_block_id.clone();
898        let storage_batch_fetcher: StorageBatchFetchFn = Arc::new(
899            move |requests: Vec<(Address, U256)>, block: Option<BlockId>| {
900                use futures::stream::{self, StreamExt};
901                // Max items per JSON-RPC batch. RPC providers typically limit batch
902                // size to ~1000 items. Reduced from 200 to avoid 429s on Base.
903                let batch_size: usize = match cache_speed_mode() {
904                    CacheSpeedMode::Fast => 150,
905                    CacheSpeedMode::Normal => 100,
906                    CacheSpeedMode::Slow => 75,
907                    CacheSpeedMode::XSlow => 25,
908                };
909                // Max concurrent HTTP batch requests. Each batch contains batch_size
910                // individual eth_getStorageAt calls. Limiting concurrency prevents
911                // thundering herd when prefetching thousands of storage slots.
912                let max_concurrent: usize = match cache_speed_mode() {
913                    CacheSpeedMode::Fast => 8,
914                    CacheSpeedMode::Normal => 6,
915                    CacheSpeedMode::Slow => 4,
916                    CacheSpeedMode::XSlow => 1,
917                };
918
919                // Guard against panicking inside `block_in_place` on a
920                // current-thread runtime (or when no runtime is present): return
921                // an `Err` result for every requested slot instead.
922                let handle = match block_in_place_handle() {
923                    Ok(handle) => handle,
924                    Err(e) => {
925                        let msg = e.to_string();
926                        return requests
927                            .into_iter()
928                            .map(|(addr, slot)| (addr, slot, Err(anyhow!("{}", msg))))
929                            .collect();
930                    }
931                };
932                // Pin to the explicitly-requested block when given, else the
933                // cache's currently-pinned block. Capturing the block at the call
934                // site is what lets the deferred freshness validator fetch at the
935                // snapshot's block despite a later `set_block`.
936                let current_block = block.unwrap_or_else(|| *batch_block_ref.lock().unwrap());
937                tokio::task::block_in_place(|| {
938                    handle.block_on(async {
939                        let mut results = Vec::with_capacity(requests.len());
940
941                        // Build and send JSON-RPC batches (each batch = one HTTP request)
942                        let batch_futs: Vec<_> = requests
943                            .chunks(batch_size)
944                            .map(|chunk| {
945                                let client = provider_for_batch.client();
946                                let mut batch = alloy_rpc_client::BatchRequest::new(client);
947                                let mut waiters = Vec::with_capacity(chunk.len());
948
949                                for &(addr, slot) in chunk {
950                                    let params = (addr, slot, current_block);
951                                    match batch.add_call::<_, U256>("eth_getStorageAt", &params) {
952                                        Ok(waiter) => waiters.push((addr, slot, Some(waiter))),
953                                        Err(e) => {
954                                            // Serialization error — rare, treat as failure
955                                            tracing::warn!(
956                                                ?addr,
957                                                ?slot,
958                                                "batch request serialization failed: {}",
959                                                e
960                                            );
961                                            waiters.push((addr, slot, None));
962                                        }
963                                    }
964                                }
965
966                                async move {
967                                    // Send the batch as a single HTTP request
968                                    let send_result = batch.send().await;
969                                    let mut chunk_results = Vec::with_capacity(waiters.len());
970
971                                    for (addr, slot, waiter) in waiters {
972                                        if let Some(waiter) = waiter {
973                                            if send_result.is_ok() {
974                                                match waiter.await {
975                                                    Ok(value) => {
976                                                        chunk_results.push((addr, slot, Ok(value)));
977                                                    }
978                                                    Err(e) => {
979                                                        chunk_results.push((
980                                                            addr,
981                                                            slot,
982                                                            Err(anyhow!("{}", e)),
983                                                        ));
984                                                    }
985                                                }
986                                            } else {
987                                                chunk_results.push((
988                                                    addr,
989                                                    slot,
990                                                    Err(anyhow!("batch send failed")),
991                                                ));
992                                            }
993                                        } else {
994                                            chunk_results.push((
995                                                addr,
996                                                slot,
997                                                Err(anyhow!("serialization failed")),
998                                            ));
999                                        }
1000                                    }
1001                                    chunk_results
1002                                }
1003                            })
1004                            .collect();
1005
1006                        // Fire batches with bounded concurrency (`max_concurrent`) to avoid
1007                        // a thundering herd; per-batch size is the speed-mode `batch_size`
1008                        // chosen above, so throughput scales without overwhelming RPC providers.
1009                        let all_batch_results: Vec<Vec<_>> = stream::iter(batch_futs)
1010                            .buffer_unordered(max_concurrent)
1011                            .collect()
1012                            .await;
1013                        for batch_results in all_batch_results {
1014                            results.extend(batch_results);
1015                        }
1016                        results
1017                    })
1018                })
1019            },
1020        );
1021
1022        // Resolve the chain ID reported to simulations (the `CHAINID` opcode). A
1023        // disk `CacheConfig` is authoritative (its `chain_id` also namespaces the
1024        // on-disk cache directory); otherwise infer it from the provider via
1025        // `eth_chainId`, falling back to 1 (Ethereum mainnet) only if that query
1026        // fails. Resolved before `provider` is moved into the backend below.
1027        // Prefer setting it explicitly through `EvmCacheBuilder::chain_id`.
1028        let chain_id = match cache_config.as_ref() {
1029            Some(cfg) => cfg.chain_id,
1030            None => match provider.get_chain_id().await {
1031                Ok(id) => id,
1032                Err(e) => {
1033                    debug!(
1034                        error = %e,
1035                        "Failed to infer chain ID from provider; defaulting to 1 (Ethereum mainnet). Set it explicitly via EvmCacheBuilder::chain_id."
1036                    );
1037                    1
1038                }
1039            },
1040        };
1041
1042        // Spawn the backend handler on a background task
1043        let backend =
1044            SharedBackend::spawn_backend(provider, blockchain_db.clone(), Some(block_id)).await;
1045
1046        let db = CacheDB::new(backend.clone());
1047
1048        // Resolve the shared-memory pre-allocation. For `Auto` we size from the
1049        // amount of layer-2 chain state actually loaded (post-filter), so a large
1050        // bincode state file yields a larger buffer; `Fixed` ignores the count.
1051        let loaded_slots = match shared_memory_capacity {
1052            SharedMemoryCapacity::Auto => blockchain_db
1053                .storage()
1054                .read()
1055                .values()
1056                .map(|s| s.len())
1057                .sum(),
1058            SharedMemoryCapacity::Fixed(_) => 0,
1059        };
1060        let shared_memory_capacity = shared_memory_capacity.resolve(loaded_slots);
1061
1062        Self {
1063            backend,
1064            blockchain_db,
1065            db,
1066            token_decimals,
1067            block,
1068            cache_config,
1069            immutable_cache,
1070            timestamp_override: None,
1071            chain_id,
1072            block_number,
1073            basefee,
1074            coinbase,
1075            prevrandao,
1076            block_gas_limit,
1077            shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(shared_memory_capacity))),
1078            rpc_caller: Some(rpc_caller),
1079            storage_batch_fetcher: Some(storage_batch_fetcher),
1080            batch_block_id,
1081            erc20_balance_slots: HashMap::new(),
1082            spec_id,
1083            base: None,
1084            base_dirty: HashSet::new(),
1085            base_full_rebuild: false,
1086            base_storage_lens: HashMap::new(),
1087            shared_memory_capacity,
1088        }
1089    }
1090
1091    /// Seed contract bytecodes into the BlockchainDb from a bytecode cache.
1092    ///
1093    /// This allows subsequent EVM executions to use cached bytecode instead of
1094    /// fetching from RPC. Storage slots will still be fetched fresh since they
1095    /// may have changed between blocks.
1096    fn seed_bytecodes_from_cache(db: &BlockchainDb, cache: &BytecodeCache) -> usize {
1097        let mut count = 0;
1098        for (addr, entry) in &cache.contracts {
1099            if entry.bytecode.is_empty() {
1100                continue;
1101            }
1102
1103            // Create bytecode and compute hash
1104            let bytecode = Bytecode::new_raw(Bytes::from(entry.bytecode.clone()));
1105            let code_hash: B256 = bytecode.hash_slow();
1106
1107            // Create account info with bytecode but zeroed balance/nonce
1108            // The balance/nonce will be fetched from RPC if needed during execution
1109            let info = AccountInfo {
1110                balance: U256::ZERO,
1111                nonce: 0,
1112                code_hash,
1113                code: Some(bytecode),
1114                account_id: None,
1115            };
1116
1117            db.db().do_insert_account(*addr, info);
1118            count += 1;
1119        }
1120        count
1121    }
1122
1123    /// Create a new EvmCache from an existing SharedBackend.
1124    ///
1125    /// Useful when you want to share a backend between multiple caches
1126    /// (e.g. parallel simulation threads).
1127    ///
1128    /// **Shared pinned block.** A `SharedBackend` owns a single pinned fork
1129    /// height. Calling [`set_block`](Self::set_block) / `repin_to_block` on *any*
1130    /// cache built from the same backend re-pins the RPC fork height for **all**
1131    /// of them. Sibling caches sharing one backend should agree on a block and not
1132    /// re-pin independently; build separate backends if they must fork at
1133    /// different heights.
1134    pub fn from_backend(
1135        backend: SharedBackend,
1136        blockchain_db: BlockchainDb,
1137        block: BlockId,
1138        chain_id: u64,
1139        block_number: Option<u64>,
1140        basefee: Option<u64>,
1141        spec_id: SpecId,
1142    ) -> Self {
1143        let db = CacheDB::new(backend.clone());
1144        Self {
1145            backend,
1146            blockchain_db,
1147            db,
1148            token_decimals: HashMap::new(),
1149            block,
1150            cache_config: None,
1151            immutable_cache: ImmutableDataCache::default(),
1152            timestamp_override: None,
1153            chain_id,
1154            block_number,
1155            basefee,
1156            coinbase: None,
1157            prevrandao: None,
1158            block_gas_limit: None,
1159            shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(
1160                DEFAULT_SHARED_MEMORY_CAPACITY,
1161            ))),
1162            rpc_caller: None,
1163            storage_batch_fetcher: None,
1164            batch_block_id: Arc::new(Mutex::new(block)),
1165            erc20_balance_slots: HashMap::new(),
1166            spec_id,
1167            base: None,
1168            base_dirty: HashSet::new(),
1169            base_full_rebuild: false,
1170            base_storage_lens: HashMap::new(),
1171            shared_memory_capacity: DEFAULT_SHARED_MEMORY_CAPACITY,
1172        }
1173    }
1174
1175    /// Flush the cache state to disk.
1176    ///
1177    /// This persists:
1178    /// 1. Unified EVM state (accounts + storage) to `evm_state.bin` (bincode)
1179    /// 2. Contract bytecodes to `bytecodes.bin`
1180    /// 3. Immutable data (token decimals) to `immutable_data.bin`
1181    ///
1182    /// Call this after loading hot contract state and running simulations to
1183    /// speed up subsequent runs.
1184    /// The cache is also automatically flushed when the EvmCache is dropped.
1185    pub fn flush(&self) -> Result<()> {
1186        if let Some(cfg) = &self.cache_config {
1187            // Save EVM state to binary cache (bincode format)
1188            let binary_path = cfg.binary_state_cache_path();
1189            binary_state::save_binary_state(&self.blockchain_db, &binary_path)
1190                .with_context(|| format!("failed to save binary state cache to {binary_path:?}"))?;
1191
1192            // Save bytecode cache
1193            let bytecode_path = cfg.bytecode_cache_path();
1194            let mut bytecode_cache = BytecodeCache::load(&bytecode_path).unwrap_or_default();
1195            bytecode_cache.merge_from_db(&self.blockchain_db);
1196            bytecode_cache
1197                .save(&bytecode_path)
1198                .with_context(|| format!("failed to save bytecode cache to {bytecode_path:?}"))?;
1199            debug!(
1200                count = bytecode_cache.contracts.len(),
1201                path = ?bytecode_path,
1202                "Updated bytecode cache (binary format)"
1203            );
1204
1205            // Save the immutable data cache
1206            let immutable_path = cfg.immutable_cache_path();
1207            self.immutable_cache
1208                .save(&immutable_path)
1209                .with_context(|| {
1210                    format!("failed to save immutable data cache to {immutable_path:?}")
1211                })?;
1212            debug!(
1213                token_decimals = self.immutable_cache.token_decimals.len(),
1214                path = ?immutable_path,
1215                "Updated immutable data cache"
1216            );
1217        }
1218        Ok(())
1219    }
1220
1221    /// Get the cache configuration, if any.
1222    ///
1223    /// Returns `None` when the cache is purely in-memory (no disk persistence),
1224    /// i.e. constructed without a [`CacheConfig`] or via
1225    /// [`from_backend`](Self::from_backend).
1226    pub fn cache_config(&self) -> Option<&CacheConfig> {
1227        self.cache_config.as_ref()
1228    }
1229
1230    /// Run a synchronous direct mutation against the underlying [`BlockchainDb`]
1231    /// and invalidate the memoized snapshot base afterwards.
1232    ///
1233    /// This is the preferred escape hatch for unavoidable layer-2 map writes such
1234    /// as `accounts().write().insert(...)` or `storage().write().insert(...)`.
1235    /// The closure still bypasses the CacheDB overlay and the normal write funnel,
1236    /// so use higher-level mutators when they can express the change. Unlike
1237    /// [`unchecked_blockchain_db`](Self::unchecked_blockchain_db), this wrapper
1238    /// keeps the copy-on-write snapshot base honest automatically after in-place
1239    /// overwrites whose map cardinality does not change.
1240    pub fn with_blockchain_db_mut<R>(&mut self, f: impl FnOnce(&BlockchainDb) -> R) -> R {
1241        let result = f(&self.blockchain_db);
1242        self.invalidate_base();
1243        result
1244    }
1245
1246    /// Get an unchecked reference to the underlying [`BlockchainDb`] (the layer-2
1247    /// backend store of accounts, storage, and bytecodes).
1248    ///
1249    /// This exposes an internal store and bypasses the cache's two-layer
1250    /// consistency model: reads here see only the backend layer, not the CacheDB
1251    /// overlay, and any writes performed through it skip the overlay. Prefer
1252    /// higher-level accessors or [`with_blockchain_db_mut`](Self::with_blockchain_db_mut)
1253    /// for direct synchronous writes.
1254    ///
1255    /// # Snapshot base
1256    /// Writing layer 2 directly through this unchecked handle also bypasses the
1257    /// memoized copy-on-write snapshot base (Pillar A). The next
1258    /// [`create_snapshot`](Self::create_snapshot) only performs a count/absence
1259    /// growth scan over layer 2, which catches lazy RPC-populated accounts/slots
1260    /// because that path only appends at a fixed block. It does **not** catch
1261    /// direct in-place changes where cardinality is unchanged: overwriting an
1262    /// existing storage slot, or changing an existing account's info/code/balance
1263    /// without adding a new account, can leave a stale snapshot base. After such a
1264    /// direct write, call
1265    /// [`invalidate_snapshot_base`](Self::invalidate_snapshot_base) (or re-pin via
1266    /// [`set_block`](Self::set_block)) before the next snapshot. Writes via the
1267    /// crate's own mutators (`inject_storage_batch`, `apply_update`, the `inject_*`
1268    /// helpers, the purges) keep the base honest automatically.
1269    pub fn unchecked_blockchain_db(&self) -> &BlockchainDb {
1270        &self.blockchain_db
1271    }
1272
1273    /// Get an unchecked reference to the underlying [`SharedBackend`] (the lazy
1274    /// RPC-backed fetcher shared across clones).
1275    ///
1276    /// This exposes an internal handle and bypasses the cache's two-layer consistency
1277    /// model: it reads/fetches directly without consulting the CacheDB overlay.
1278    /// Prefer the higher-level accessors; use with care.
1279    ///
1280    /// # Snapshot base
1281    /// Lazy RPC fetches through this backend only append missing accounts/slots at
1282    /// the pinned block, so the snapshot growth scan catches them without an
1283    /// explicit invalidation. Direct `SharedBackend::insert_or_update_storage` /
1284    /// `insert_or_update_address` calls are different: they enqueue a background
1285    /// handler request that can rewrite layer-2 entries **in place**, leaving the
1286    /// memoized copy-on-write base stale at an unchanged slot/account count.
1287    ///
1288    /// If you use those helpers directly, first synchronize with the backend
1289    /// handler by reading back the updated account/slot through `SharedBackend`
1290    /// (for example via `basic_ref` / `storage_ref`), then call
1291    /// [`invalidate_snapshot_base`](Self::invalidate_snapshot_base) before the next
1292    /// [`create_snapshot`](Self::create_snapshot). Calling
1293    /// `invalidate_snapshot_base` immediately after `insert_or_update_*` is not, by
1294    /// itself, a guarantee that the queued update has been applied before the next
1295    /// snapshot.
1296    pub fn unchecked_backend(&self) -> &SharedBackend {
1297        &self.backend
1298    }
1299
1300    /// Get a mutable reference to the underlying [`ForkCacheDB`] (the layer-1
1301    /// CacheDB overlay).
1302    ///
1303    /// This exposes an internal and bypasses the cache's two-layer consistency
1304    /// model: writes made here land only in the overlay and are not mirrored
1305    /// into the BlockchainDb backend, so parallel tasks sharing the backend
1306    /// will not see them. Prefer the higher-level mutators; use with care.
1307    pub fn db_mut(&mut self) -> &mut ForkCacheDB {
1308        &mut self.db
1309    }
1310
1311    /// Make a direct RPC `eth_call` to the node, bypassing revm simulation.
1312    ///
1313    /// This is much faster than `call_raw` for batch operations because the RPC
1314    /// node has all state in memory and doesn't need lazy storage fetching.
1315    /// Returns `None` if no RPC caller is available (e.g. `from_backend` constructor).
1316    ///
1317    /// # Panics
1318    /// Must be called from within a **multi-thread** tokio runtime: the callback
1319    /// drives the async `eth_call` to completion via
1320    /// `tokio::task::block_in_place`. On a current-thread runtime (or with no
1321    /// runtime), the callback degrades to an `Err` rather than panicking, but
1322    /// `block_in_place` itself will panic if invoked from a non-worker thread of
1323    /// a multi-thread runtime.
1324    pub fn rpc_call(&self, to: Address, calldata: Bytes) -> Option<Result<Bytes>> {
1325        self.rpc_caller
1326            .as_ref()
1327            .map(|caller| (caller)(to, calldata))
1328    }
1329
1330    /// Get the batch storage fetcher, if available.
1331    ///
1332    /// Returns `None` when constructed via `from_backend` (no provider available).
1333    ///
1334    /// # Panics
1335    /// The returned [`StorageBatchFetchFn`] must be invoked from within a
1336    /// **multi-thread** tokio runtime: it drives concurrent `eth_getStorageAt`
1337    /// calls to completion via `tokio::task::block_in_place`. On a
1338    /// current-thread runtime (or with no runtime) it degrades to an `Err`
1339    /// result for every requested slot rather than panicking, but
1340    /// `block_in_place` itself will panic if invoked from a non-worker thread of
1341    /// a multi-thread runtime.
1342    pub fn storage_batch_fetcher(&self) -> Option<&StorageBatchFetchFn> {
1343        self.storage_batch_fetcher.as_ref()
1344    }
1345
1346    /// Inject batch-fetched storage values directly into BlockchainDb (layer 2).
1347    ///
1348    /// This bypasses SharedBackend and makes values available for subsequent
1349    /// `storage_ref()` calls and EVM SLOADs. Used after `StorageBatchFetchFn`
1350    /// returns results to populate the cache in bulk.
1351    ///
1352    /// Takes `&mut self` (as of Pillar A) so it can mark each touched address dirty
1353    /// for the memoized copy-on-write base; the write itself is still a direct
1354    /// layer-2 backend write. Overwriting an existing slot at an unchanged slot
1355    /// count is invalidated here too, since the `refresh_base` growth scan only
1356    /// catches length changes.
1357    pub fn inject_storage_batch(&mut self, results: &[(Address, U256, U256)]) {
1358        {
1359            let mut storage = self.blockchain_db.storage().write();
1360            for &(addr, slot, value) in results {
1361                storage.entry(addr).or_default().insert(slot, value);
1362            }
1363        }
1364        for &(addr, _, _) in results {
1365            self.mark_base_dirty(addr);
1366        }
1367    }
1368
1369    /// Inject freshly-fetched storage values, healing **both** cache layers.
1370    ///
1371    /// Like [`inject_storage_batch`](Self::inject_storage_batch) this writes each
1372    /// value into the BlockchainDb backend (layer 2). Additionally, for any
1373    /// address that *already* has a CacheDB overlay entry (layer 1), it writes
1374    /// the slot into that overlay too.
1375    ///
1376    /// This matters because both [`create_snapshot`](Self::create_snapshot) and
1377    /// the synchronous EVM SLOAD path let the overlay win over the backend. A
1378    /// correction written only to layer 2 would be shadowed by a stale layer-1
1379    /// slot, so the cache could never converge — the freshness validator would
1380    /// re-detect the same change and re-correct it every cycle. Writing through
1381    /// the overlay keeps the layer that wins authoritative.
1382    ///
1383    /// It deliberately does **not** create a new overlay account for an address
1384    /// that has none: such a slot is layer-2-only (e.g. cold prefetch), where
1385    /// the backend write is already authoritative and materializing an overlay
1386    /// entry would pollute layer 1 and could shadow later RPC reads.
1387    pub fn inject_storage_batch_fresh(&mut self, results: &[(Address, U256, U256)]) {
1388        // Thin wrapper over the unified write primitive (the F1 fix now lives in
1389        // `apply_slot`). Each tuple becomes a write-through `StateUpdate::Slot`;
1390        // the returned diff is discarded to preserve this method's `-> ()` API.
1391        let updates: Vec<StateUpdate> = results
1392            .iter()
1393            .map(|&(addr, slot, value)| StateUpdate::slot(addr, slot, value))
1394            .collect();
1395        let _ = self.apply_updates(&updates);
1396    }
1397
1398    /// Apply a single targeted [`StateUpdate`], returning a [`StateDiff`] of what
1399    /// actually changed.
1400    ///
1401    /// This is the single primitive that writes the state-update vocabulary
1402    /// across both cache layers with one consistent, documented policy. It is
1403    /// **synchronous and infallible** — a write, not a fetch, so it never touches
1404    /// RPC and never errors. See the [`state_update`](crate::state_update) module
1405    /// for the dual-layer write-through policy and the diff semantics.
1406    ///
1407    /// - [`StateUpdate::Slot`] — write `value` into the backend (layer 2) always,
1408    ///   and into the overlay (layer 1) only if an overlay account already
1409    ///   exists. Records a [`SlotChange`] only when the value actually changes
1410    ///   (`old.unwrap_or(ZERO) != value`).
1411    /// - [`StateUpdate::SlotDelta`] — *relative*, cold-aware. If the slot has a
1412    ///   cached value, write the saturating delta through the same path and record
1413    ///   a [`SlotChange`] iff it changed; if the slot is cold (absent from both
1414    ///   layers), apply nothing and surface a `SkippedDelta` in `diff.skipped`.
1415    /// - [`StateUpdate::BalanceDelta`] — *relative*, cold-aware native-balance
1416    ///   update. If the account is present in either layer, apply the saturating
1417    ///   delta to its balance (nonce/code preserved) write-through and record an
1418    ///   [`AccountChange`] iff it changed; if the account is cold (absent from both
1419    ///   layers), apply nothing and surface a [`SkippedBalanceDelta`] in
1420    ///   `diff.skipped_balances` (no default account is materialized).
1421    /// - [`StateUpdate::Account`] — load the current `AccountInfo` from the cached
1422    ///   layers (no RPC), apply each `Some` patch field (recomputing the code hash
1423    ///   when `code` is set), then write through with the same layer policy.
1424    ///   Records an [`AccountChange`] with `Some((old, new))` only for fields
1425    ///   that changed. If the account is cold (absent from both layers), apply
1426    ///   nothing and surface a [`SkippedAccountPatch`] in
1427    ///   `diff.skipped_accounts`.
1428    /// - [`StateUpdate::AccountUpsert`] — same patch semantics, but intentionally
1429    ///   materializes a cold/default account when absent from both layers.
1430    /// - [`StateUpdate::Purge`] — dispatch to the matching purge layer logic and
1431    ///   record a [`PurgeRecord`].
1432    ///
1433    /// # Warning — relative updates can be skipped
1434    ///
1435    /// A cold-aware update targeting a **cold** address is *dropped, not applied*
1436    /// unless it is an explicit [`StateUpdate::AccountUpsert`]. Because a skip
1437    /// produces no change, it is invisible to the changes-only
1438    /// [`StateDiff::is_empty`] / [`StateDiff::len`] success check, so after
1439    /// applying cold-aware updates the caller **must** inspect
1440    /// [`StateDiff::has_skipped`] (or the `skipped_*` fields) and fetch+seed the
1441    /// cold target.
1442    ///
1443    /// ```no_run
1444    /// # use alloy_primitives::{Address, U256};
1445    /// # use evm_fork_cache::StateUpdate;
1446    /// # fn example(cache: &mut evm_fork_cache::cache::EvmCache) {
1447    /// let contract = Address::repeat_byte(0x01);
1448    /// let diff = cache.apply_update(&StateUpdate::slot(contract, U256::from(0), U256::from(42)));
1449    /// assert_eq!(diff.slots.len(), 1);
1450    /// # }
1451    /// ```
1452    pub fn apply_update(&mut self, update: &StateUpdate) -> StateDiff {
1453        let mut diff = StateDiff::default();
1454        match update {
1455            StateUpdate::Slot {
1456                address,
1457                slot,
1458                value,
1459            } => {
1460                if let Some(change) = self.apply_slot(*address, *slot, *value) {
1461                    diff.slots.push(change);
1462                }
1463            }
1464            StateUpdate::SlotDelta {
1465                address,
1466                slot,
1467                delta,
1468            } => match self.cached_storage_value(*address, *slot) {
1469                // Hot slot: apply the saturating delta write-through. Build the
1470                // change from the value we already read (do not route through
1471                // `apply_slot`, which would re-read the same slot — §16.9.1).
1472                Some(current) => {
1473                    let new = delta.apply(current);
1474                    self.write_slot_through(*address, *slot, new);
1475                    if current != new {
1476                        diff.slots.push(SlotChange {
1477                            address: *address,
1478                            slot: *slot,
1479                            old: current,
1480                            new,
1481                        });
1482                    }
1483                }
1484                // Cold slot: applying `0 ± amount` would corrupt an unknown value,
1485                // so write nothing and surface the skip for the caller to seed.
1486                None => diff.skipped.push(SkippedDelta {
1487                    address: *address,
1488                    slot: *slot,
1489                    delta: *delta,
1490                }),
1491            },
1492            StateUpdate::SlotMasked {
1493                address,
1494                slot,
1495                mask,
1496                value,
1497            } => match self.cached_storage_value(*address, *slot) {
1498                // Hot slot: overwrite only the masked bits, preserving the rest.
1499                // Build the change from the value we already read (mirroring the
1500                // `SlotDelta` arm; do not re-read through `apply_slot`).
1501                Some(old) => {
1502                    let new = (old & !*mask) | (*value & *mask);
1503                    self.write_slot_through(*address, *slot, new);
1504                    if old != new {
1505                        diff.slots.push(SlotChange {
1506                            address: *address,
1507                            slot: *slot,
1508                            old,
1509                            new,
1510                        });
1511                    }
1512                }
1513                // Cold slot: the un-masked bits are unknown, so the result cannot
1514                // be computed; write nothing and surface the skip for re-seeding.
1515                None => diff.skipped_masks.push(SkippedMask {
1516                    address: *address,
1517                    slot: *slot,
1518                    mask: *mask,
1519                    value: *value,
1520                }),
1521            },
1522            StateUpdate::BalanceDelta { address, delta } => {
1523                match self.apply_balance_delta(*address, *delta) {
1524                    // Hot account: the saturating delta was applied.
1525                    Ok(Some(change)) => diff.accounts.push(change),
1526                    // Hot account but no change (e.g. Sub from 0, Add of 0).
1527                    Ok(None) => {}
1528                    // Cold account: surface the skip; nothing was materialized.
1529                    Err(skipped) => diff.skipped_balances.push(skipped),
1530                }
1531            }
1532            StateUpdate::Account { address, patch } => {
1533                match self.apply_account_patch(*address, patch, false) {
1534                    Ok(Some(change)) => diff.accounts.push(change),
1535                    Ok(None) => {}
1536                    Err(skipped) => diff.skipped_accounts.push(skipped),
1537                }
1538            }
1539            StateUpdate::AccountUpsert { address, patch } => {
1540                if let Some(change) = self
1541                    .apply_account_patch(*address, patch, true)
1542                    .expect("AccountUpsert never skips cold account patches")
1543                {
1544                    diff.accounts.push(change);
1545                }
1546            }
1547            StateUpdate::Purge { address, scope } => {
1548                diff.purged.push(self.apply_purge(*address, scope));
1549            }
1550        }
1551        diff
1552    }
1553
1554    /// Apply a batch of [`StateUpdate`]s left-to-right, merging each per-update
1555    /// [`StateDiff`].
1556    ///
1557    /// Later updates observe the effect of earlier ones: two `Slot` writes to the
1558    /// same key record `old → a` then `a → b`. Like
1559    /// [`apply_update`](Self::apply_update) this is synchronous and infallible.
1560    ///
1561    /// # Performance — batched single-lock fast-path
1562    ///
1563    /// Consecutive `Slot`/`SlotDelta` writes are processed holding the backend
1564    /// storage write-guard **once** for the run (the overlay map is lock-free), so
1565    /// a bulk slot seed pays one lock acquisition instead of one read + one write
1566    /// lock per slot. Apply order is preserved: when an `Account`/`BalanceDelta`/
1567    /// `Purge` update is reached the guard is dropped first (those take the
1568    /// `accounts()` / `storage()` locks themselves — holding the storage
1569    /// write-guard across them would deadlock the non-reentrant `RwLock`), the
1570    /// update is processed via [`apply_update`](Self::apply_update), then the guard
1571    /// is lazily re-acquired on the next slot run. The result is byte-identical to
1572    /// folding [`apply_update`](Self::apply_update) over the batch.
1573    ///
1574    /// # Warning — relative updates can be skipped
1575    ///
1576    /// See [`apply_update`](Self::apply_update): a cold relative update is dropped,
1577    /// not applied, and is invisible to [`StateDiff::is_empty`] /
1578    /// [`StateDiff::len`]. After a batch with relative updates, check
1579    /// [`StateDiff::has_skipped`].
1580    pub fn apply_updates(&mut self, updates: &[StateUpdate]) -> StateDiff {
1581        let mut diff = StateDiff::default();
1582        let mut i = 0;
1583        while i < updates.len() {
1584            match &updates[i] {
1585                // A run of consecutive slot writes: process them under a single
1586                // held storage write-guard, then advance past the run.
1587                StateUpdate::Slot { .. } | StateUpdate::SlotDelta { .. } => {
1588                    let run_end = updates[i..]
1589                        .iter()
1590                        .position(|u| {
1591                            !matches!(u, StateUpdate::Slot { .. } | StateUpdate::SlotDelta { .. })
1592                        })
1593                        .map(|off| i + off)
1594                        .unwrap_or(updates.len());
1595                    self.apply_slot_run(&updates[i..run_end], &mut diff);
1596                    i = run_end;
1597                }
1598                // Account / BalanceDelta / Purge: no held guard (they take their
1599                // own locks), so route through the single-update primitive.
1600                _ => {
1601                    diff.merge(self.apply_update(&updates[i]));
1602                    i += 1;
1603                }
1604            }
1605        }
1606        diff
1607    }
1608
1609    /// Apply a run of consecutive `Slot`/`SlotDelta` updates under one held backend
1610    /// storage write-guard (§16.9.2), merging each change into `diff`.
1611    ///
1612    /// The backend storage guard is acquired once for the whole run; overlay access
1613    /// is lock-free (`self.db.cache.accounts`). The old-value read stays
1614    /// `account_state`-aware (matching [`cached_storage_value`](Self::cached_storage_value)):
1615    /// for an overlay account whose slot is absent, a `StorageCleared`/`NotExisting`
1616    /// state reads ZERO and the backend is **not** consulted. Behavior is identical
1617    /// to applying each update via [`apply_update`](Self::apply_update); the
1618    /// `apply_updates_batched_equals_sequential` test pins this.
1619    fn apply_slot_run(&mut self, run: &[StateUpdate], diff: &mut StateDiff) {
1620        // Borrow the two layers as disjoint fields: the backend storage guard
1621        // (layer 2) held for the whole run, and the overlay accounts map (layer 1,
1622        // lock-free). Base invalidation is deferred until after the guard is
1623        // dropped (it needs `&mut self`): collect the layer-2 addresses written
1624        // here and mark them dirty below.
1625        let mut dirtied: Vec<Address> = Vec::new();
1626        let overlay = &mut self.db.cache.accounts;
1627        let mut storage = self.blockchain_db.storage().write();
1628
1629        for update in run {
1630            // Resolve `(address, slot, old, new)` for the write; a cold SlotDelta
1631            // is skipped here (write nothing). `old` is the `account_state`-aware
1632            // read (overlay ▸ cleared-as-ZERO ▸ backend), reused for both the write
1633            // gate and the change record so each slot is read at most once.
1634            let (address, slot, old, new) = match update {
1635                StateUpdate::Slot {
1636                    address,
1637                    slot,
1638                    value,
1639                } => {
1640                    let old = read_slot_account_state_aware(overlay, &storage, *address, *slot)
1641                        .unwrap_or(U256::ZERO);
1642                    (*address, *slot, old, *value)
1643                }
1644                StateUpdate::SlotDelta {
1645                    address,
1646                    slot,
1647                    delta,
1648                } => match read_slot_account_state_aware(overlay, &storage, *address, *slot) {
1649                    // Hot: apply the saturating delta to the value already read.
1650                    Some(current) => (*address, *slot, current, delta.apply(current)),
1651                    // Cold: skip and surface (write nothing).
1652                    None => {
1653                        diff.skipped.push(SkippedDelta {
1654                            address: *address,
1655                            slot: *slot,
1656                            delta: *delta,
1657                        });
1658                        continue;
1659                    }
1660                },
1661                // The caller only ever hands this method slot updates.
1662                _ => unreachable!("apply_slot_run only processes Slot/SlotDelta"),
1663            };
1664
1665            write_slot_into(overlay, &mut storage, address, slot, new);
1666            // Layer 2 was written for this address → it must be re-folded into the
1667            // memoized base. Mirrors `write_slot_through`'s `mark_base_dirty`.
1668            dirtied.push(address);
1669            if old != new {
1670                diff.slots.push(SlotChange {
1671                    address,
1672                    slot,
1673                    old,
1674                    new,
1675                });
1676            }
1677        }
1678
1679        // Drop the storage write-guard before taking `&mut self` for invalidation.
1680        drop(storage);
1681        for address in dirtied {
1682            self.mark_base_dirty(address);
1683        }
1684    }
1685
1686    /// Write-through a single storage slot (§5.1). Returns a [`SlotChange`] iff
1687    /// the slot's value actually changes.
1688    fn apply_slot(&mut self, address: Address, slot: U256, value: U256) -> Option<SlotChange> {
1689        // Old value: overlay ▸ backend ▸ None (treated as ZERO).
1690        let old = self
1691            .cached_storage_value(address, slot)
1692            .unwrap_or(U256::ZERO);
1693
1694        self.write_slot_through(address, slot, value);
1695
1696        // Record only an actual change.
1697        (old != value).then_some(SlotChange {
1698            address,
1699            slot,
1700            old,
1701            new: value,
1702        })
1703    }
1704
1705    /// The single dual-layer slot write path (§5.1), shared by [`apply_slot`],
1706    /// the [`StateUpdate::SlotDelta`] handler, and [`modify_slot`](Self::modify_slot).
1707    ///
1708    /// Backend (layer 2) is always written; the overlay (layer 1) is written only
1709    /// if an overlay account already exists. A new overlay account is never
1710    /// materialized: that preserves the layer-2-only invariant (a fresh
1711    /// `StorageCleared` overlay account would read missing slots as ZERO and could
1712    /// shadow later RPC reads), and an absent overlay entry falls through to the
1713    /// backend on reads so the backend write is authoritative.
1714    fn write_slot_through(&mut self, address: Address, slot: U256, value: U256) {
1715        // Backend (layer 2): always write.
1716        {
1717            let mut storage = self.blockchain_db.storage().write();
1718            storage.entry(address).or_default().insert(slot, value);
1719        }
1720
1721        // Overlay (layer 1): write only if an overlay account already exists.
1722        if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
1723            db_account.storage.insert(slot, value);
1724        }
1725
1726        // Layer 2 changed → invalidate the memoized base for this address (D2:
1727        // over-invalidation when also shadowed by layer 1 is safe).
1728        self.mark_base_dirty(address);
1729    }
1730
1731    /// Read-modify-write one storage slot through a caller-supplied transform.
1732    ///
1733    /// The general closure escape hatch behind [`StateUpdate::SlotDelta`] (the
1734    /// data-level form flows through [`apply_update`](Self::apply_update); this is
1735    /// for arbitrary transforms). `f` is called with the current cached value
1736    /// (overlay ▸ backend ▸ `None` when the slot is cold) and decides the new
1737    /// value:
1738    ///
1739    /// - `Some(new)` writes `new` through both layers (the same write path as
1740    ///   [`StateUpdate::Slot`]) and returns a [`SlotChange`] iff it changed
1741    ///   (`old.unwrap_or(ZERO) != new`);
1742    /// - `None` writes nothing and returns `None`.
1743    ///
1744    /// The caller owns the cold/overflow policy. To skip cold slots (the
1745    /// cold-aware read-modify-write rule), map through the `Option`:
1746    /// `|cur| cur.map(|v| v.saturating_add(amount))` leaves a cold slot untouched.
1747    /// To write an absolute value regardless, ignore the argument: `|_| Some(v)`.
1748    ///
1749    /// ```no_run
1750    /// # use alloy_primitives::{Address, U256};
1751    /// # fn example(cache: &mut evm_fork_cache::cache::EvmCache) {
1752    /// let token = Address::repeat_byte(0x01);
1753    /// let slot = U256::from(0);
1754    /// // Saturating +100, but only if the slot is already hot.
1755    /// let change = cache.modify_slot(token, slot, |cur| cur.map(|v| v.saturating_add(U256::from(100))));
1756    /// # let _ = change;
1757    /// # }
1758    /// ```
1759    pub fn modify_slot(
1760        &mut self,
1761        address: Address,
1762        slot: U256,
1763        f: impl FnOnce(Option<U256>) -> Option<U256>,
1764    ) -> Option<SlotChange> {
1765        let current = self.cached_storage_value(address, slot);
1766        let new = f(current)?;
1767
1768        self.write_slot_through(address, slot, new);
1769
1770        let old = current.unwrap_or(U256::ZERO);
1771        (old != new).then_some(SlotChange {
1772            address,
1773            slot,
1774            old,
1775            new,
1776        })
1777    }
1778
1779    /// Read-modify-write an account's native balance through a caller-supplied
1780    /// transform.
1781    ///
1782    /// The closure analog of [`StateUpdate::BalanceDelta`] (the data-level form
1783    /// flows through [`apply_update`](Self::apply_update); this is for arbitrary
1784    /// transforms). `f` is called with the account's current native balance
1785    /// (overlay ▸ backend ▸ `None` when the account is absent from **both**
1786    /// layers) and decides the new balance:
1787    ///
1788    /// - `Some(new)` writes `new` through both layers — backend always, overlay
1789    ///   only if an overlay account already exists — preserving the account's
1790    ///   nonce and code, and returns an [`AccountChange`] (balance only) iff the
1791    ///   balance changed;
1792    /// - `None` writes nothing (no account is materialized) and returns `None`.
1793    ///
1794    /// "Cold" for a balance is the account being absent from both layers — or
1795    /// present in the overlay as revm `NotExisting` (absent to the EVM), which the
1796    /// internal account read also treats as cold, mirroring `DbAccount::info()`.
1797    /// To skip cold accounts, map through the `Option`:
1798    /// `|cur| cur.map(|v| v.saturating_add(amount))`.
1799    ///
1800    /// ```no_run
1801    /// # use alloy_primitives::{Address, U256};
1802    /// # fn example(cache: &mut evm_fork_cache::cache::EvmCache) {
1803    /// let acct = Address::repeat_byte(0x01);
1804    /// // Saturating +100, but only if the account's balance is already known.
1805    /// let change = cache.modify_account_balance(acct, |cur| cur.map(|v| v.saturating_add(U256::from(100))));
1806    /// # let _ = change;
1807    /// # }
1808    /// ```
1809    pub fn modify_account_balance(
1810        &mut self,
1811        address: Address,
1812        f: impl FnOnce(Option<U256>) -> Option<U256>,
1813    ) -> Option<AccountChange> {
1814        // Load the full info from the cached layers only (overlay ▸ backend); the
1815        // account is "cold" when absent from both.
1816        let base = self.loaded_account_info(address);
1817        let current_balance = base.as_ref().map(|info| info.balance);
1818        let new_balance = f(current_balance)?;
1819
1820        // The closure asked to write `new_balance`. Materialize from the loaded
1821        // base (or a default if the caller chose to write a cold account).
1822        let mut info = base.unwrap_or_default();
1823        let old_balance = info.balance;
1824        info.balance = new_balance;
1825        self.write_account_info_through(address, info);
1826
1827        (old_balance != new_balance).then_some(AccountChange {
1828            address,
1829            balance: Some((old_balance, new_balance)),
1830            nonce: None,
1831            code_hash: None,
1832        })
1833    }
1834
1835    /// Apply a relative (saturating) [`SlotDelta`] to an account's native balance
1836    /// (§16.5). Cold-aware:
1837    ///
1838    /// - `Ok(Some(change))` — present account, balance changed;
1839    /// - `Ok(None)` — present account, balance unchanged (e.g. `Sub` from 0);
1840    /// - `Err(skipped)` — cold account (absent from both layers): nothing applied,
1841    ///   nothing materialized.
1842    fn apply_balance_delta(
1843        &mut self,
1844        address: Address,
1845        delta: SlotDelta,
1846    ) -> std::result::Result<Option<AccountChange>, SkippedBalanceDelta> {
1847        let Some(mut info) = self.loaded_account_info(address) else {
1848            // Cold: applying a delta against an unknown balance would corrupt it,
1849            // and materializing a default account would mask the real on-chain one.
1850            return Err(SkippedBalanceDelta { address, delta });
1851        };
1852
1853        let old_balance = info.balance;
1854        let new_balance = delta.apply(old_balance);
1855        info.balance = new_balance;
1856        self.write_account_info_through(address, info);
1857
1858        Ok((old_balance != new_balance).then_some(AccountChange {
1859            address,
1860            balance: Some((old_balance, new_balance)),
1861            nonce: None,
1862            code_hash: None,
1863        }))
1864    }
1865
1866    /// Load an account's `AccountInfo` from the cached layers only (overlay ▸
1867    /// backend), without touching RPC. `None` when the account is absent from
1868    /// both layers.
1869    fn loaded_account_info(&self, address: Address) -> Option<AccountInfo> {
1870        let mut info = if let Some(a) = self.db.cache.accounts.get(&address) {
1871            // Mirror revm `DbAccount::info()` / `basic_ref`: a NotExisting overlay
1872            // account is absent to the EVM (returns None) and does NOT fall through
1873            // to the backend. Without this, a relative balance update / partial
1874            // patch would compute against a stale `info` the EVM never sees.
1875            if matches!(a.account_state, AccountState::NotExisting) {
1876                return None;
1877            }
1878            a.info.clone()
1879        } else {
1880            self.blockchain_db
1881                .accounts()
1882                .read()
1883                .get(&address)
1884                .cloned()?
1885        };
1886        // Normalize like revm `insert_contract`: a ZERO code_hash denotes empty
1887        // code -> KECCAK_EMPTY. Done at load time so a patch's `old_code_hash`
1888        // matches what `write_account_info_through` stores (a self-consistent diff,
1889        // no phantom/under-reported code_hash change).
1890        if info.code_hash == B256::ZERO {
1891            info.code_hash = revm::primitives::KECCAK_EMPTY;
1892        }
1893        Some(info)
1894    }
1895
1896    /// Write an `AccountInfo` through both layers, mirroring the slot policy:
1897    /// backend (layer 2) always; overlay (layer 1) only if an overlay account
1898    /// already exists (never materialize a new overlay account).
1899    fn write_account_info_through(&mut self, address: Address, mut info: AccountInfo) {
1900        // Normalize the code hash the way revm's `insert_contract` (applied on the
1901        // overlay write below) does, so both layers store an identical hash: a ZERO
1902        // code_hash denotes empty code → KECCAK_EMPTY. Otherwise the overlay would
1903        // hold KECCAK_EMPTY while the backend kept ZERO for the same account.
1904        if info.code_hash == B256::ZERO {
1905            info.code_hash = revm::primitives::KECCAK_EMPTY;
1906        }
1907        let overlay_present = self.db.cache.accounts.contains_key(&address);
1908        {
1909            let mut accounts = self.blockchain_db.accounts().write();
1910            accounts.insert(address, info.clone());
1911        }
1912        if overlay_present {
1913            self.db.insert_account_info(address, info);
1914        }
1915        // Layer-2 account info changed → invalidate the memoized base for this
1916        // address (D2: over-invalidation when also in layer 1 is safe).
1917        self.mark_base_dirty(address);
1918    }
1919
1920    /// Apply a partial [`AccountPatch`] write-through (§5.2). Returns an
1921    /// [`AccountChange`] iff any field actually changes.
1922    fn apply_account_patch(
1923        &mut self,
1924        address: Address,
1925        patch: &AccountPatch,
1926        allow_cold_upsert: bool,
1927    ) -> std::result::Result<Option<AccountChange>, SkippedAccountPatch> {
1928        // 1. Current info from the cached layers only (overlay ▸ backend). No RPC:
1929        //    apply is a write, not a fetch. A partial patch on a cold account is
1930        //    skipped unless the caller explicitly chose AccountUpsert.
1931        let mut info = match self.loaded_account_info(address) {
1932            Some(info) => info,
1933            None if account_patch_is_empty(patch) => return Ok(None),
1934            None if allow_cold_upsert => AccountInfo::default(),
1935            None => {
1936                return Err(SkippedAccountPatch {
1937                    address,
1938                    patch: patch.clone(),
1939                });
1940            }
1941        };
1942
1943        let old_balance = info.balance;
1944        let old_nonce = info.nonce;
1945        let old_code_hash = info.code_hash;
1946
1947        // 2. Apply each `Some` field.
1948        if let Some(balance) = patch.balance {
1949            info.balance = balance;
1950        }
1951        if let Some(nonce) = patch.nonce {
1952            info.nonce = nonce;
1953        }
1954        if let Some(code) = &patch.code {
1955            let bytecode = Bytecode::new_raw(code.clone());
1956            info.code_hash = bytecode.hash_slow();
1957            info.code = Some(bytecode);
1958        }
1959
1960        // 3. Compute the change first. A no-op patch (every field equals the
1961        //    loaded base) must NOT write either layer — otherwise an all-`None`
1962        //    patch on an absent address would insert `AccountInfo::default()` into
1963        //    the shared backend (masking a future RPC fetch) while returning an
1964        //    empty diff. Only a real field change materializes anything.
1965        let change = AccountChange {
1966            address,
1967            balance: (old_balance != info.balance).then_some((old_balance, info.balance)),
1968            nonce: (old_nonce != info.nonce).then_some((old_nonce, info.nonce)),
1969            code_hash: (old_code_hash != info.code_hash).then_some((old_code_hash, info.code_hash)),
1970        };
1971        if change.balance.is_none() && change.nonce.is_none() && change.code_hash.is_none() {
1972            return Ok(None);
1973        }
1974
1975        // 4. Write-through, mirroring the slot policy: backend always; overlay
1976        //    only if an overlay account already exists (do not materialize one).
1977        self.write_account_info_through(address, info);
1978
1979        Ok(Some(change))
1980    }
1981
1982    /// Dispatch a [`PurgeScope`] to the matching layer logic (§5.3), returning a
1983    /// [`PurgeRecord`] of what was removed from each layer.
1984    fn apply_purge(&mut self, address: Address, scope: &PurgeScope) -> PurgeRecord {
1985        match scope {
1986            PurgeScope::Account => {
1987                let (slots_removed, account_removed) = self.purge_account_inner(address);
1988                PurgeRecord {
1989                    address,
1990                    scope: PurgeScope::Account,
1991                    slots_removed,
1992                    account_removed,
1993                }
1994            }
1995            PurgeScope::AllStorage => {
1996                let slots_removed = self.purge_contract_storage_inner(address);
1997                PurgeRecord {
1998                    address,
1999                    scope: PurgeScope::AllStorage,
2000                    slots_removed,
2001                    account_removed: false,
2002                }
2003            }
2004            PurgeScope::Slots(slots) => {
2005                let slots_removed = self.purge_contract_slots_inner(address, slots);
2006                PurgeRecord {
2007                    address,
2008                    scope: PurgeScope::Slots(slots.clone()),
2009                    slots_removed,
2010                    account_removed: false,
2011                }
2012            }
2013        }
2014    }
2015
2016    /// Set (or replace) the batch storage fetcher.
2017    ///
2018    /// This is the seam the freshness controller and tests use to drive
2019    /// re-verification without a live provider: a stubbed
2020    /// [`StorageBatchFetchFn`] can be injected over a mocked-provider cache.
2021    pub fn set_storage_batch_fetcher(&mut self, f: StorageBatchFetchFn) {
2022        self.storage_batch_fetcher = Some(f);
2023    }
2024
2025    /// Return the currently-cached value for a storage slot, if any.
2026    ///
2027    /// Mirrors what the EVM would `SLOAD` from the cached layers (it never touches
2028    /// RPC, unlike [`read_storage_slot`](Self::read_storage_slot)):
2029    ///
2030    /// 1. The CacheDB overlay (layer 1) wins: if the overlay account holds the
2031    ///    slot, return it.
2032    /// 2. Match revm's `CacheDB::storage_ref`: if the overlay account exists but
2033    ///    does **not** hold the slot, and its `account_state` is `StorageCleared`
2034    ///    or `NotExisting`, the live EVM reads the slot as ZERO and never consults
2035    ///    the backend — so return `Some(U256::ZERO)`, **not** the (shadowed)
2036    ///    backend value. Returning the backend value here would let a
2037    ///    `SlotDelta`/`modify_slot` compute a delta against a base the EVM never
2038    ///    sees (silent corruption) and would mis-record `apply_slot`'s `old`.
2039    /// 3. Otherwise fall through to the BlockchainDb backend (layer 2); `None` when
2040    ///    neither layer has seen the slot.
2041    pub fn cached_storage_value(&self, address: Address, slot: U256) -> Option<U256> {
2042        if let Some(db_account) = self.db.cache.accounts.get(&address) {
2043            if let Some(value) = db_account.storage.get(&slot) {
2044                return Some(*value);
2045            }
2046            // A StorageCleared / NotExisting overlay account reads a missing slot
2047            // as ZERO and never consults the backend (matching the EVM SLOAD).
2048            if matches!(
2049                db_account.account_state,
2050                AccountState::StorageCleared | AccountState::NotExisting
2051            ) {
2052                return Some(U256::ZERO);
2053            }
2054        }
2055        let storage = self.blockchain_db.storage().read();
2056        storage.get(&address).and_then(|s| s.get(&slot).copied())
2057    }
2058
2059    /// Re-fetch the given slots via the batch fetcher, compare to the currently
2060    /// cached values, and inject the ones that changed.
2061    ///
2062    /// For each slot whose freshly-fetched value differs from the cached value,
2063    /// the fresh value is written into the cache via
2064    /// [`inject_storage_batch_fresh`](Self::inject_storage_batch_fresh) and a
2065    /// [`SlotChange`] is recorded. Slots that are unchanged, or that the fetcher
2066    /// fails to return, are left as-is. Returns the set of changed slots.
2067    ///
2068    /// Requires a batch fetcher (set at construction or via
2069    /// [`set_storage_batch_fetcher`](Self::set_storage_batch_fetcher)); errors if
2070    /// none is available. This is the synchronous main-thread primitive; the
2071    /// background validator performs the equivalent comparison against a snapshot.
2072    pub fn verify_slots(&mut self, slots: &[(Address, U256)]) -> Result<Vec<SlotChange>> {
2073        Ok(self.verify_slots_inner(slots)?.0)
2074    }
2075
2076    /// Shared implementation for [`verify_slots`](Self::verify_slots) and the
2077    /// pipeline's reconcile path. Returns `(changed, fetched_ok)` where
2078    /// `fetched_ok` is the number of requested slots the fetcher returned a value
2079    /// for (failed per-slot fetches are skipped, not errors). Errors only when no
2080    /// batch fetcher is configured.
2081    fn verify_slots_inner(
2082        &mut self,
2083        slots: &[(Address, U256)],
2084    ) -> Result<(Vec<SlotChange>, usize)> {
2085        let (changed, outcomes) = self.verify_slots_core(slots)?;
2086        let fetched_ok = outcomes
2087            .iter()
2088            .filter(|o| matches!(o.fetch, SlotFetch::Value(_) | SlotFetch::Zero))
2089            .count();
2090        Ok((changed, fetched_ok))
2091    }
2092
2093    /// Classify a single fetched slot value into a [`SlotFetch`].
2094    ///
2095    /// This is purely the *fetch* classification (`Value` / `Zero` /
2096    /// `FetchFailed`); it is independent of change detection, which compares the
2097    /// fetched value to the cached baseline separately. A non-zero `Ok` is
2098    /// [`SlotFetch::Value`], a genuine `Ok(0)` is [`SlotFetch::Zero`], and an
2099    /// `Err` is [`SlotFetch::FetchFailed`] carrying the error string.
2100    ///
2101    /// Shared with the cold-start probe phase
2102    /// ([`execute_cold_start_round`](Self::execute_cold_start_round)) so the
2103    /// single classification is reused rather than duplicated.
2104    pub(crate) fn classify(fetched: Result<U256>) -> SlotFetch {
2105        match fetched {
2106            Ok(v) if v != U256::ZERO => SlotFetch::Value(v),
2107            Ok(_) => SlotFetch::Zero,
2108            Err(e) => SlotFetch::FetchFailed {
2109                reason: e.to_string(),
2110            },
2111        }
2112    }
2113
2114    /// Core slot-verification loop shared by [`verify_slots_inner`](Self::verify_slots_inner)
2115    /// and [`verify_slots_with_outcomes`](Self::verify_slots_with_outcomes).
2116    ///
2117    /// Fetches every slot via the batch fetcher and, for each slot, performs two
2118    /// **independent** reads of the same fetched value:
2119    ///
2120    /// 1. *Fetch classification* — every slot (including failed ones) produces one
2121    ///    [`SlotOutcome`] via [`classify`](Self::classify): `Value` / `Zero` /
2122    ///    `FetchFailed`.
2123    /// 2. *Change detection* — a successfully-fetched value that differs from the
2124    ///    cached baseline (`old`, defaulting to `ZERO` for an unseen slot) is
2125    ///    injected via [`inject_storage_batch_fresh`](Self::inject_storage_batch_fresh)
2126    ///    and recorded as a [`SlotChange`].
2127    ///
2128    /// These two reads are deliberately not collapsed: a genuine `Ok(0)` on a slot
2129    /// whose cached value was also `0` yields [`SlotFetch::Zero`] **and** no
2130    /// `SlotChange`. The returned `outcomes` vec has exactly one entry per
2131    /// requested slot. An empty `slots` input short-circuits to empty results
2132    /// without requiring a fetcher; otherwise a missing fetcher is an error.
2133    fn verify_slots_core(
2134        &mut self,
2135        slots: &[(Address, U256)],
2136    ) -> Result<(Vec<SlotChange>, Vec<SlotOutcome>)> {
2137        if slots.is_empty() {
2138            return Ok((Vec::new(), Vec::new()));
2139        }
2140        let fetcher = self
2141            .storage_batch_fetcher
2142            .as_ref()
2143            .ok_or_else(|| anyhow!("verify_slots requires a storage batch fetcher"))?
2144            .clone();
2145
2146        // Snapshot the cached values before fetching so we compare against a
2147        // stable baseline.
2148        let cached: HashMap<(Address, U256), Option<U256>> = slots
2149            .iter()
2150            .map(|&(addr, slot)| ((addr, slot), self.cached_storage_value(addr, slot)))
2151            .collect();
2152
2153        let results = (fetcher)(slots.to_vec(), Some(self.block));
2154
2155        let mut changed = Vec::new();
2156        let mut outcomes = Vec::with_capacity(results.len());
2157        let mut to_inject = Vec::new();
2158        for (addr, slot, fetched) in results {
2159            // Read 1: classify the fetch outcome for every slot, failed or not.
2160            let fetch = Self::classify(match &fetched {
2161                Ok(v) => Ok(*v),
2162                Err(e) => Err(anyhow!("{e}")),
2163            });
2164            outcomes.push(SlotOutcome {
2165                address: addr,
2166                slot,
2167                fetch,
2168            });
2169
2170            // Read 2: change detection, independent of the classification above.
2171            let fresh = match fetched {
2172                Ok(value) => value,
2173                Err(e) => {
2174                    debug!(%addr, %slot, error = %e, "verify_slots: fetch failed, skipping slot");
2175                    continue;
2176                }
2177            };
2178            // A slot the cache never saw is treated as old = ZERO (the value a
2179            // sim would have read), so a non-zero fresh value counts as a change.
2180            let old = cached
2181                .get(&(addr, slot))
2182                .copied()
2183                .flatten()
2184                .unwrap_or(U256::ZERO);
2185            if fresh != old {
2186                to_inject.push((addr, slot, fresh));
2187                changed.push(SlotChange {
2188                    address: addr,
2189                    slot,
2190                    old,
2191                    new: fresh,
2192                });
2193            }
2194        }
2195
2196        if !to_inject.is_empty() {
2197            self.inject_storage_batch_fresh(&to_inject);
2198        }
2199        Ok((changed, outcomes))
2200    }
2201
2202    /// Like [`verify_slots`](Self::verify_slots), but additionally returns one
2203    /// [`SlotOutcome`] per requested slot (including slots the fetcher failed to
2204    /// return), classified as `Value` / `Zero` / `FetchFailed`.
2205    ///
2206    /// This is the per-slot surface the cold-start driver consumes: it
2207    /// distinguishes a genuine on-chain zero from a fetch failure for every slot,
2208    /// closing the archive-miss gap. It is a pure alias of
2209    /// [`verify_slots_core`](Self::verify_slots_core) and shares its injection
2210    /// behaviour with [`verify_slots`](Self::verify_slots).
2211    #[cfg(feature = "reactive")]
2212    pub(crate) fn verify_slots_with_outcomes(
2213        &mut self,
2214        slots: &[(Address, U256)],
2215    ) -> Result<(Vec<SlotChange>, Vec<SlotOutcome>)> {
2216        self.verify_slots_core(slots)
2217    }
2218
2219    /// Reconciliation re-read used by [`EventPipeline::reconcile`](crate::events::EventPipeline::reconcile).
2220    ///
2221    /// Like [`verify_slots`](Self::verify_slots) it fetches the requested slots,
2222    /// injects the ones that changed, and returns the changed set — but it is
2223    /// **honest about reachability**: it errors not only when no batch fetcher is
2224    /// configured, but also when a non-empty request could not fetch **any** slot
2225    /// (a total fetch failure — e.g. the default RPC fetcher invoked with no usable
2226    /// runtime, or an unreachable provider). Reconciliation that silently "verified
2227    /// nothing" would be a false all-clear, so it surfaces as an error for the
2228    /// caller to retry. A partially-successful fetch returns `Ok` with whatever
2229    /// changed.
2230    pub fn reconcile_slots(&mut self, slots: &[(Address, U256)]) -> Result<Vec<SlotChange>> {
2231        let (changed, fetched_ok) = self.verify_slots_inner(slots)?;
2232        if !slots.is_empty() && fetched_ok == 0 {
2233            return Err(anyhow!(
2234                "reconcile could not fetch any of the {} requested slot(s) \
2235                 (no usable storage fetcher / provider unreachable)",
2236                slots.len()
2237            ));
2238        }
2239        Ok(changed)
2240    }
2241
2242    /// Purge an account fully from both cache layers: its `AccountInfo`
2243    /// (balance/nonce/code hash) **and** all of its storage.
2244    ///
2245    /// Removes `addr` from the CacheDB overlay accounts map, the BlockchainDb
2246    /// accounts map, and the BlockchainDb storage map, so the next access
2247    /// re-fetches a clean account from RPC. This is the account-level
2248    /// counterpart to the storage-only [`purge_contract_storage`](Self::purge_contract_storage):
2249    /// use it when an address is fully volatile (no pinned slots) and even its
2250    /// balance/nonce/code can no longer be trusted.
2251    pub fn purge_account(&mut self, addr: Address) {
2252        // Thin wrapper over the unified purge primitive; the layer logic lives in
2253        // `purge_account_inner` (shared with `apply_update(Purge { Account })`).
2254        let _ = self.apply_update(&StateUpdate::purge(addr, PurgeScope::Account));
2255    }
2256
2257    /// Account-scope purge layer logic. Removes `addr` from the overlay accounts
2258    /// map, the backend accounts map, and the backend storage map. Returns
2259    /// `(backend_slots_removed, account_removed)` where `account_removed` is true
2260    /// if an account entry was removed from either account layer.
2261    fn purge_account_inner(&mut self, addr: Address) -> (usize, bool) {
2262        // Layer 1: CacheDB overlay (accounts + their storage live together).
2263        let overlay_removed = self.db.cache.accounts.remove(&addr).is_some();
2264
2265        // Layer 2: BlockchainDb accounts + storage maps.
2266        let backend_account_removed = self
2267            .blockchain_db
2268            .accounts()
2269            .write()
2270            .remove(&addr)
2271            .is_some();
2272        let backend_storage_removed = self.blockchain_db.storage().write().remove(&addr);
2273        let slots_removed = backend_storage_removed
2274            .map(|slots| slots.len())
2275            .unwrap_or(0);
2276
2277        let account_removed = overlay_removed || backend_account_removed;
2278        if account_removed || slots_removed > 0 {
2279            debug!(
2280                account = %addr,
2281                overlay_removed,
2282                backend_account_removed,
2283                backend_storage_slots = slots_removed,
2284                "purged account from both cache layers"
2285            );
2286        }
2287        // Layer 2 (account + storage) changed for this address → invalidate base.
2288        self.mark_base_dirty(addr);
2289        (slots_removed, account_removed)
2290    }
2291
2292    /// Get the chain ID used for EVM simulations (the `CHAINID` opcode).
2293    pub fn chain_id(&self) -> u64 {
2294        self.chain_id
2295    }
2296
2297    /// Set the chain ID reported to simulations via the `CHAINID` opcode.
2298    ///
2299    /// Prefer setting this at construction through
2300    /// [`EvmCacheBuilder::chain_id`]. This setter exists for cases where the
2301    /// chain ID must change after construction. It takes effect on the next
2302    /// [`create_snapshot`](Self::create_snapshot) / `build_evm`; existing
2303    /// snapshots and overlays keep the chain ID captured when they were created.
2304    pub fn set_chain_id(&mut self, chain_id: u64) {
2305        self.chain_id = chain_id;
2306    }
2307
2308    /// Take a low-level, same-thread snapshot of the CacheDB overlay for
2309    /// in-place restore.
2310    ///
2311    /// Clones the inner [`revm::database::Cache`] (the layer-1 overlay's
2312    /// accounts and storage) only — not the underlying database wrapper or the
2313    /// BlockchainDb backend. Pair with [`restore`](Self::restore) to roll the
2314    /// overlay back on the same `EvmCache` after speculative mutations (this is
2315    /// how the balance-slot scan probes and rewinds).
2316    ///
2317    /// For cross-thread fan-out use [`create_snapshot`](Self::create_snapshot)
2318    /// instead: it merges both layers into an `Arc<`[`EvmSnapshot`]`>` that is
2319    /// `Send + Sync` and can be shared with parallel simulators via
2320    /// [`EvmOverlay`].
2321    pub fn snapshot(&self) -> revm::database::Cache {
2322        self.db.cache.clone()
2323    }
2324
2325    /// Restore the CacheDB overlay from a snapshot taken with
2326    /// [`snapshot`](Self::snapshot).
2327    ///
2328    /// Overwrites the layer-1 overlay wholesale with `snapshot`, discarding any
2329    /// overlay mutations made since it was taken. The BlockchainDb backend is
2330    /// untouched. This is the in-place counterpart to the cross-thread
2331    /// [`create_snapshot`](Self::create_snapshot) / [`EvmOverlay`] path.
2332    pub fn restore(&mut self, snapshot: revm::database::Cache) {
2333        self.db.cache = snapshot;
2334    }
2335
2336    /// Create a new session for executing multiple operations.
2337    ///
2338    /// Changes made within the session are only committed to the underlying database
2339    /// when `session.commit()` is called. Dropping the session without calling commit
2340    /// discards all changes made during the session.
2341    pub fn session(&mut self) -> EvmSession<'_> {
2342        EvmSession {
2343            evm: self.build_evm(),
2344        }
2345    }
2346
2347    /// Create an immutable, `Send + Sync` snapshot of the current EVM state for
2348    /// cross-thread fan-out (the copy-on-write two-tier view, Pillar A).
2349    ///
2350    /// Rather than deep-copying both layers, this memoizes the cold layer-2
2351    /// (`BlockchainDb`) index as an `Arc`-shared base — reused as a cheap
2352    /// `Arc::clone` when layer 2 is unchanged, rebuilt copy-on-write only for the
2353    /// addresses that changed — and folds the hot layer-1 (`CacheDB` overlay)
2354    /// delta over it. Layer-1 values shadow the base on reads, reproducing the
2355    /// live cache's layered semantics; the resulting [`EvmSnapshot`] is shared
2356    /// across threads via `Arc`. Its cost tracks *changed* state, not *total*
2357    /// state. (The retained [`create_snapshot_deep_clone`](Self::create_snapshot_deep_clone)
2358    /// is the read-equivalent O(total) reference, kept for benchmarking/testing.)
2359    ///
2360    /// Takes `&mut self` because it refreshes and memoizes the base. For cheap
2361    /// same-thread save/restore of just the overlay, prefer
2362    /// [`snapshot`](Self::snapshot) / [`restore`](Self::restore) instead.
2363    pub fn create_snapshot(&mut self) -> Arc<snapshot::EvmSnapshot> {
2364        // 1. Refresh / memoize the cold layer-2 base, then take a cheap Arc handle
2365        //    (O(1) when layer 2 is unchanged since the last snapshot).
2366        self.refresh_base();
2367        let base = Arc::clone(self.base.as_ref().expect("refresh_base sets base"));
2368
2369        // 2. Fold layer 1 (the hot CacheDB overlay) into the snapshot's overlay
2370        //    maps + cleared/not-existing sets, applying the same classification as
2371        //    the legacy flatten (O(layer-1)).
2372        let mut overlay_accounts = HashMap::new();
2373        let mut overlay_storage = HashMap::new();
2374        let mut overlay_code_by_hash = HashMap::new();
2375        let mut storage_cleared = std::collections::HashSet::new();
2376        let mut accounts_not_existing = std::collections::HashSet::new();
2377        for (addr, db_account) in &self.db.cache.accounts {
2378            let not_existing = matches!(db_account.account_state, AccountState::NotExisting);
2379            let cleared =
2380                not_existing || matches!(db_account.account_state, AccountState::StorageCleared);
2381
2382            // Account info. Mirror revm `DbAccount::info()` / `loaded_account_info`:
2383            // a NotExisting overlay account is absent to the EVM (`basic` returns
2384            // None), so it must NOT contribute info/code to the overlay — and
2385            // `accounts_not_existing` makes the read short-circuit to None before
2386            // ever consulting the base.
2387            if not_existing {
2388                accounts_not_existing.insert(*addr);
2389            } else {
2390                if let Some(code) = &db_account.info.code {
2391                    overlay_code_by_hash.insert(db_account.info.code_hash, code.clone());
2392                }
2393                overlay_accounts.insert(*addr, db_account.info.clone());
2394            }
2395
2396            // Storage. A StorageCleared/NotExisting account's storage is locally
2397            // complete: the overlay holds ONLY its own slots (so a cleared account
2398            // ALWAYS gets an `overlay_storage` entry, possibly empty), an absent
2399            // slot reads ZERO via `storage_cleared`, and the base is never consulted
2400            // for it. A non-cleared overlay account contributes its slots; absent
2401            // slots fall through to the base on a read.
2402            if cleared {
2403                storage_cleared.insert(*addr);
2404                let account_storage: HashMap<U256, U256> =
2405                    db_account.storage.iter().map(|(k, v)| (*k, *v)).collect();
2406                overlay_storage.insert(*addr, account_storage);
2407            } else if !db_account.storage.is_empty() {
2408                let account_storage = overlay_storage.entry(*addr).or_default();
2409                for (slot, value) in &db_account.storage {
2410                    account_storage.insert(*slot, *value);
2411                }
2412            }
2413        }
2414
2415        Arc::new(snapshot::EvmSnapshot {
2416            base,
2417            overlay_accounts,
2418            overlay_storage,
2419            overlay_code_by_hash,
2420            storage_cleared,
2421            accounts_not_existing,
2422            block_hashes: HashMap::new(),
2423            block_number: self.block_number,
2424            basefee: self.basefee,
2425            coinbase: self.coinbase,
2426            prevrandao: self.prevrandao,
2427            gas_limit: self.block_gas_limit,
2428            chain_id: self.chain_id,
2429            timestamp: self.timestamp_override,
2430            spec_id: self.spec_id,
2431            shared_memory_capacity: self.shared_memory_capacity,
2432        })
2433    }
2434
2435    /// Force the next [`create_snapshot`](Self::create_snapshot) to rebuild the
2436    /// memoized copy-on-write base from scratch (Pillar A).
2437    ///
2438    /// The crate's own mutators keep the base honest automatically. This is the
2439    /// **escape-hatch re-honest hook**: call it after writing layer 2 directly
2440    /// through [`unchecked_blockchain_db`](Self::unchecked_blockchain_db) or
2441    /// [`unchecked_backend`](Self::unchecked_backend) — those bypass the write
2442    /// funnel, and in-place changes at unchanged cardinality are invisible to the
2443    /// snapshot growth scan.
2444    /// That includes overwriting an existing storage slot and changing an existing
2445    /// account's info/code/balance without adding a new account. Lazy RPC-populated
2446    /// data does not need this call because it only appends accounts/slots, which
2447    /// the growth scan catches.
2448    ///
2449    /// When using `SharedBackend::insert_or_update_*` through
2450    /// [`unchecked_backend`](Self::unchecked_backend), remember those helpers only
2451    /// enqueue a background update. Synchronize/read back the update through
2452    /// `SharedBackend` before the next snapshot; `invalidate_snapshot_base` alone
2453    /// is not a backend-handler synchronization point. Once the direct write is
2454    /// present, calling this before the next snapshot guarantees it reflects that
2455    /// write rather than a stale memoized value. Over-invalidation is always safe
2456    /// (Decision D2); the only cost is one full base rebuild on the next snapshot.
2457    pub fn invalidate_snapshot_base(&mut self) {
2458        self.invalidate_base();
2459    }
2460
2461    /// Refresh the memoized cold layer-2 [`BaseState`](snapshot::BaseState),
2462    /// reusing the previous `Arc` wherever layer 2 is unchanged (Pillar A).
2463    ///
2464    /// Called at the top of [`create_snapshot`](Self::create_snapshot). It never
2465    /// mutates an `Arc<BaseState>` that may already be shared with a live
2466    /// snapshot: on any change it builds a *new* `BaseState` that shares the `Arc`
2467    /// handles of unchanged accounts and rebuilds only the changed ones
2468    /// (copy-on-write).
2469    ///
2470    /// Algorithm (see `docs/phase-5-spec.md` §2.3):
2471    /// 1. **Full rebuild** when there is no base yet or `base_full_rebuild` is set
2472    ///    (`set_block` / re-pin replaced layer 2): flatten all of layer 2.
2473    /// 2. **Detect uncontrolled growth**: a lazy RPC fetch / prefetch can write
2474    ///    layer 2 from inside `foundry-fork-db`, bypassing our write funnel. An
2475    ///    `O(accounts)` length-scan over the current layer-2 storage/accounts marks
2476    ///    any address whose slot count differs from the recorded length, or any
2477    ///    account absent from the base, as dirty.
2478    /// 3. **Nothing dirty** → reuse the existing `Arc<BaseState>` unchanged (the
2479    ///    common hot-loop case; the base side of `create_snapshot` is then O(1)).
2480    /// 4. **Some addresses dirty** → build a new `BaseState` sharing the `Arc`s of
2481    ///    unchanged accounts and rebuilding only the dirty ones.
2482    fn refresh_base(&mut self) {
2483        // Case 1: full rebuild.
2484        if self.base.is_none() || self.base_full_rebuild {
2485            self.base = Some(Arc::new(self.build_base_full()));
2486            self.base_dirty.clear();
2487            self.base_full_rebuild = false;
2488            return;
2489        }
2490
2491        // Case 2: detect uncontrolled layer-2 growth via an O(accounts) length scan
2492        // (NOT an O(slots) value scan). Any address whose slot count changed, or any
2493        // account that newly appeared in layer 2, is folded into `base_dirty`.
2494        //
2495        // LOAD-BEARING INVARIANT: the count/absence scan is sufficient *only* because
2496        // the one uncontrolled layer-2 writer — the foundry-fork-db `SharedBackend`
2497        // lazy fetch — is append-only at a fixed block (its request handler answers an
2498        // already-cached account/slot from the store and only inserts on a miss; it
2499        // never overwrites an existing entry in place). So an uncontrolled fetch can
2500        // only add a new account (caught by the absence check) or a new slot (caught
2501        // by the count check). An in-place value overwrite at unchanged length is
2502        // invisible here; the controlled writers therefore call `mark_base_dirty`
2503        // explicitly, and a direct out-of-band write via `unchecked_blockchain_db()`/`unchecked_backend()`
2504        // must call `invalidate_snapshot_base`. If a future foundry-fork-db bump makes
2505        // the lazy path overwrite-in-place, this scan must gain a value/version check.
2506        {
2507            let db_storage = self.blockchain_db.storage().read();
2508            for (addr, slots) in db_storage.iter() {
2509                if self.base_storage_lens.get(addr).copied() != Some(slots.len()) {
2510                    self.base_dirty.insert(*addr);
2511                }
2512            }
2513            let db_accounts = self.blockchain_db.accounts().read();
2514            let base = self.base.as_ref().expect("base present in case 2/3/4");
2515            for addr in db_accounts.keys() {
2516                if !base.accounts.contains_key(addr) {
2517                    self.base_dirty.insert(*addr);
2518                }
2519            }
2520        }
2521
2522        // Case 3: nothing changed → reuse the existing Arc unchanged.
2523        if self.base_dirty.is_empty() {
2524            return;
2525        }
2526
2527        // Case 4: rebuild copy-on-write — clone the outer maps (Arc handles +
2528        // AccountInfo, no per-slot copy) and rebuild only the dirty addresses.
2529        let prev = self.base.as_ref().expect("base present in case 4");
2530        let mut accounts = prev.accounts.clone();
2531        let mut storage = prev.storage.clone();
2532
2533        let db_accounts = self.blockchain_db.accounts().read();
2534        let db_storage = self.blockchain_db.storage().read();
2535        for addr in self.base_dirty.iter().copied() {
2536            // Account info: refresh from the current layer-2 account, or drop it if
2537            // the account no longer exists in layer 2 (e.g. after a purge).
2538            match db_accounts.get(&addr) {
2539                Some(info) => {
2540                    accounts.insert(addr, info.clone());
2541                }
2542                None => {
2543                    accounts.remove(&addr);
2544                }
2545            }
2546
2547            // Storage: rebuild this account's Arc<HashMap> from the current layer-2
2548            // storage, or drop it if the account has no layer-2 storage anymore.
2549            match db_storage.get(&addr) {
2550                Some(slots) => {
2551                    let rebuilt: HashMap<U256, U256> =
2552                        slots.iter().map(|(k, v)| (*k, *v)).collect();
2553                    self.base_storage_lens.insert(addr, rebuilt.len());
2554                    storage.insert(addr, Arc::new(rebuilt));
2555                }
2556                None => {
2557                    storage.remove(&addr);
2558                    self.base_storage_lens.remove(&addr);
2559                }
2560            }
2561        }
2562        drop(db_accounts);
2563        drop(db_storage);
2564
2565        // Rebuild the code index from the refreshed accounts (NOT cloned from the
2566        // previous base): a purged or recoded dirty account must not leave a stale
2567        // `code_by_hash` entry, which would diverge from `create_snapshot_deep_clone`
2568        // on a direct `code_by_hash(old_hash)` lookup. Rebuilding from scratch also
2569        // handles shared code hashes correctly (a hash survives iff some present
2570        // account still carries it).
2571        let code_by_hash = Self::code_index(&accounts);
2572
2573        self.base = Some(Arc::new(snapshot::BaseState {
2574            accounts,
2575            storage,
2576            code_by_hash,
2577        }));
2578        self.base_dirty.clear();
2579    }
2580
2581    /// Build the bytecode-by-hash index from a set of (layer-2) accounts, matching
2582    /// the deep-clone reference: a hash is present iff some account carries that
2583    /// code inline. Rebuilt from scratch on every base (re)build so a purged or
2584    /// recoded account never leaves a stale entry — preserving read-equivalence
2585    /// with [`create_snapshot_deep_clone`](Self::create_snapshot_deep_clone).
2586    fn code_index(accounts: &HashMap<Address, AccountInfo>) -> HashMap<B256, Bytecode> {
2587        accounts
2588            .values()
2589            .filter_map(|info| {
2590                info.code
2591                    .as_ref()
2592                    .map(|code| (info.code_hash, code.clone()))
2593            })
2594            .collect()
2595    }
2596
2597    /// Build a fresh [`BaseState`](snapshot::BaseState) by flattening all of layer
2598    /// 2, recording `base_storage_lens`. Shared by `refresh_base`'s full-rebuild
2599    /// path and [`create_snapshot_deep_clone`](Self::create_snapshot_deep_clone).
2600    fn build_base_full(&mut self) -> snapshot::BaseState {
2601        let mut accounts = HashMap::new();
2602        {
2603            let db_accounts = self.blockchain_db.accounts().read();
2604            for (addr, info) in db_accounts.iter() {
2605                accounts.insert(*addr, info.clone());
2606            }
2607        }
2608        let code_by_hash = Self::code_index(&accounts);
2609        let mut storage = HashMap::new();
2610        self.base_storage_lens.clear();
2611        {
2612            let db_storage = self.blockchain_db.storage().read();
2613            for (addr, slots) in db_storage.iter() {
2614                let converted: HashMap<U256, U256> = slots.iter().map(|(k, v)| (*k, *v)).collect();
2615                self.base_storage_lens.insert(*addr, converted.len());
2616                storage.insert(*addr, Arc::new(converted));
2617            }
2618        }
2619        snapshot::BaseState {
2620            accounts,
2621            storage,
2622            code_by_hash,
2623        }
2624    }
2625
2626    /// The retained deep-clone snapshot — today's full flatten, kept reachable for
2627    /// A/B benchmarking and as the read-equivalence reference (Decision D3).
2628    ///
2629    /// Produces the same two-tier [`EvmSnapshot`](snapshot::EvmSnapshot) shape as
2630    /// [`create_snapshot`](Self::create_snapshot), but with `base` set to the
2631    /// fully-merged flatten of **both** layers and **empty** overlay maps (the
2632    /// cleared / not-existing sets still in place). It is read-indistinguishable
2633    /// from `create_snapshot` by construction (the `tests/cow_snapshot.rs`
2634    /// differential gate pins this), at the cost of an O(total state) deep copy
2635    /// every call — exactly the cost `create_snapshot` now amortizes away.
2636    ///
2637    /// Stays `&self`: it does not touch the memoized base.
2638    #[doc(hidden)]
2639    pub fn create_snapshot_deep_clone(&self) -> Arc<snapshot::EvmSnapshot> {
2640        let mut accounts = HashMap::new();
2641        let mut storage: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
2642        let mut code_by_hash = HashMap::new();
2643
2644        // 1. Load from BlockchainDb (persistent cache / Layer 2).
2645        {
2646            let db_accounts = self.blockchain_db.accounts().read();
2647            for (addr, info) in db_accounts.iter() {
2648                if let Some(code) = &info.code {
2649                    code_by_hash.insert(info.code_hash, code.clone());
2650                }
2651                accounts.insert(*addr, info.clone());
2652            }
2653        }
2654        {
2655            let db_storage = self.blockchain_db.storage().read();
2656            for (addr, slots) in db_storage.iter() {
2657                let converted: HashMap<U256, U256> = slots.iter().map(|(k, v)| (*k, *v)).collect();
2658                storage.insert(*addr, converted);
2659            }
2660        }
2661
2662        // 2. Overlay from CacheDB (Layer 1, takes precedence). Merge into the same
2663        //    flat maps, dropping shadowed entries, exactly as the original
2664        //    `create_snapshot` did. A cleared account's storage is routed into
2665        //    `overlay_storage` (not the base), because `EvmSnapshot::storage_value`
2666        //    only applies the cleared-as-ZERO rule for an address with an
2667        //    `overlay_storage` entry — so the cleared semantics must be expressed
2668        //    there for both snapshot constructors to read identically.
2669        let mut overlay_storage: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
2670        let mut storage_cleared = std::collections::HashSet::new();
2671        let mut accounts_not_existing = std::collections::HashSet::new();
2672        for (addr, db_account) in &self.db.cache.accounts {
2673            let not_existing = matches!(db_account.account_state, AccountState::NotExisting);
2674            let cleared =
2675                not_existing || matches!(db_account.account_state, AccountState::StorageCleared);
2676
2677            if not_existing {
2678                accounts_not_existing.insert(*addr);
2679                accounts.remove(addr);
2680            } else {
2681                if let Some(code) = &db_account.info.code {
2682                    code_by_hash.insert(db_account.info.code_hash, code.clone());
2683                }
2684                accounts.insert(*addr, db_account.info.clone());
2685            }
2686
2687            if cleared {
2688                // Cleared: storage is locally complete. Drop any shadowed base
2689                // slots and keep ONLY the overlay slots, in `overlay_storage`.
2690                storage_cleared.insert(*addr);
2691                storage.remove(addr);
2692                let account_storage: HashMap<U256, U256> =
2693                    db_account.storage.iter().map(|(k, v)| (*k, *v)).collect();
2694                overlay_storage.insert(*addr, account_storage);
2695            } else {
2696                // Non-cleared: overlay slots win over base; fold them into base.
2697                let account_storage = storage.entry(*addr).or_default();
2698                for (slot, value) in &db_account.storage {
2699                    account_storage.insert(*slot, *value);
2700                }
2701            }
2702        }
2703
2704        let base = snapshot::BaseState {
2705            accounts,
2706            storage: storage
2707                .into_iter()
2708                .map(|(addr, slots)| (addr, Arc::new(slots)))
2709                .collect(),
2710            code_by_hash,
2711        };
2712
2713        Arc::new(snapshot::EvmSnapshot {
2714            base: Arc::new(base),
2715            overlay_accounts: HashMap::new(),
2716            overlay_storage,
2717            overlay_code_by_hash: HashMap::new(),
2718            storage_cleared,
2719            accounts_not_existing,
2720            block_hashes: HashMap::new(),
2721            block_number: self.block_number,
2722            basefee: self.basefee,
2723            coinbase: self.coinbase,
2724            prevrandao: self.prevrandao,
2725            gas_limit: self.block_gas_limit,
2726            chain_id: self.chain_id,
2727            timestamp: self.timestamp_override,
2728            spec_id: self.spec_id,
2729            shared_memory_capacity: self.shared_memory_capacity,
2730        })
2731    }
2732
2733    /// Mark a layer-2 address dirty so the next [`refresh_base`](Self::refresh_base)
2734    /// re-folds it into the memoized base (Pillar A invalidation; see
2735    /// `docs/phase-5-spec.md` §3).
2736    ///
2737    /// Called from every site that can change a layer-2 value a snapshot read
2738    /// would surface (write-through, batch injects, layer-2 seeding, purges).
2739    /// Over-invalidation is safe (Decision D2): marking an address that is also
2740    /// shadowed by layer 1 just re-folds that one account.
2741    fn mark_base_dirty(&mut self, address: Address) {
2742        self.base_dirty.insert(address);
2743    }
2744
2745    /// Force a full rebuild of the memoized base on the next
2746    /// [`refresh_base`](Self::refresh_base) (Pillar A invalidation).
2747    ///
2748    /// Used by layer-2 changes too broad to enumerate per-address efficiently
2749    /// (multi-contract / full-storage purges, block re-pins). Coarser than
2750    /// [`mark_base_dirty`](Self::mark_base_dirty) but always correct.
2751    fn invalidate_base(&mut self) {
2752        self.base_full_rebuild = true;
2753    }
2754
2755    /// Update the block that RPC fetches are pinned to.
2756    ///
2757    /// This re-pins the SharedBackend and the batch storage fetcher to `block`,
2758    /// so subsequent RPC fetches read state at the new block.
2759    ///
2760    /// # Block-context contract
2761    /// To prevent the EVM block context from silently diverging from the pinned
2762    /// block, when `block` is a concrete `BlockId::Number(Number(n))` this also
2763    /// updates `block_number` (the `NUMBER` opcode) to `n`. For tag-based block
2764    /// ids (`latest`, `pending`, hashes, etc.), the height is not
2765    /// statically known, so `block_number` is cleared.
2766    ///
2767    /// `basefee` (the `BASEFEE` opcode) is **cleared on every block change** and
2768    /// on every non-concrete tag/hash pin call because deriving it requires
2769    /// fetching the block header, which this synchronous method cannot do. Callers
2770    /// that change blocks should refresh it via
2771    /// [`set_block_context`](Self::set_block_context) after fetching the new
2772    /// header. Prefer [`repin_to_block`](Self::repin_to_block) when re-pinning to
2773    /// a concrete height, since it keeps `block_number` and the pinned block in
2774    /// lockstep.
2775    pub fn set_block(&mut self, block: BlockId) {
2776        let changed = self.block != block;
2777        let concrete_number = match block {
2778            BlockId::Number(BlockNumberOrTag::Number(n)) => Some(n),
2779            _ => None,
2780        };
2781        if changed {
2782            self.block = block;
2783            // Re-pinning replaces layer 2 wholesale (state at a new block): the
2784            // memoized base must be rebuilt from scratch on the next snapshot.
2785            self.invalidate_base();
2786            let _ = self.backend.set_pinned_block(block);
2787            *self.batch_block_id.lock().unwrap() = block;
2788        }
2789        if changed || concrete_number.is_none() {
2790            self.basefee = None;
2791        }
2792
2793        // Keep the EVM `NUMBER` opcode aligned with the pin. Only a concrete
2794        // height is meaningful; tags and hashes clear it so a stale number from
2795        // an earlier concrete block cannot leak into simulation.
2796        self.block_number = concrete_number;
2797    }
2798
2799    /// Get the block that RPC fetches are currently pinned to.
2800    pub fn block(&self) -> BlockId {
2801        self.block
2802    }
2803
2804    /// Set a custom timestamp for EVM simulations.
2805    ///
2806    /// When set, all EVM executions will use this timestamp instead of the current
2807    /// system time. This is useful for simulating future blocks to predict when
2808    /// time-dependent opportunities (like yield farming rewards) become profitable.
2809    ///
2810    /// Pass `None` to use the current system time (default behavior).
2811    pub fn set_timestamp(&mut self, timestamp: Option<u64>) {
2812        self.timestamp_override = timestamp;
2813    }
2814
2815    /// Get the current timestamp override, if any.
2816    ///
2817    /// Returns `None` if the cache is using the current system time (default).
2818    pub fn timestamp(&self) -> Option<u64> {
2819        self.timestamp_override
2820    }
2821
2822    /// Get the block number used for EVM simulations (the `NUMBER` opcode).
2823    ///
2824    /// Fetched from the pinned block's header at construction. Concrete-number
2825    /// pins set it via [`set_block`](Self::set_block) /
2826    /// [`repin_to_block`](Self::repin_to_block); tag/hash pins clear it
2827    /// because their height is not statically known. `None` means revm falls back
2828    /// to `0`, which can steer contracts that branch on `block.number` down a
2829    /// different code path. Override directly via
2830    /// [`set_block_context`](Self::set_block_context).
2831    pub fn block_number(&self) -> Option<u64> {
2832        self.block_number
2833    }
2834
2835    /// Get the base fee per gas used for EVM simulations (the `BASEFEE` opcode).
2836    ///
2837    /// Fetched from the pinned block's header at construction. `None` means
2838    /// revm falls back to `0`. This is cleared by [`set_block`](Self::set_block)
2839    /// / [`repin_to_block`](Self::repin_to_block) when the pin changes, and by
2840    /// non-concrete tag/hash pin calls because those can drift without a
2841    /// concrete number in the API. Refresh it with
2842    /// [`set_block_context`](Self::set_block_context) after fetching a new header
2843    /// if `BASEFEE` accuracy matters.
2844    pub fn basefee(&self) -> Option<u64> {
2845        self.basefee
2846    }
2847
2848    /// Update the block context for EVM simulations.
2849    ///
2850    /// Call this when the simulation block changes (e.g. at the start of each
2851    /// search cycle) to keep NUMBER and BASEFEE opcodes accurate.
2852    pub fn set_block_context(&mut self, block_number: Option<u64>, basefee: Option<u64>) {
2853        self.block_number = block_number;
2854        self.basefee = basefee;
2855    }
2856
2857    /// Set the block base fee (the `BASEFEE` opcode) for subsequent simulations,
2858    /// propagated into the next [`create_snapshot`](Self::create_snapshot).
2859    ///
2860    /// Offline caches built over a mocked provider have no fetched block header,
2861    /// so the base fee is unset (and the `BASEFEE` opcode reads `0`). Use this to
2862    /// install one explicitly — it determines the priority fee
2863    /// (`gas_price − basefee`) credited to the beneficiary, and thus the
2864    /// `coinbase_payment` a [`simulate_bundle`](Self::simulate_bundle) reports.
2865    ///
2866    /// The cache stores the base fee as a `u64` (matching the block header and the
2867    /// `EvmSnapshot` field), so a `U256` larger than `u64::MAX` is saturated.
2868    pub fn set_basefee(&mut self, basefee: U256) {
2869        self.basefee = Some(basefee.saturating_to::<u64>());
2870    }
2871
2872    /// Override the block beneficiary (the `COINBASE` opcode) for subsequent
2873    /// simulations.
2874    ///
2875    /// Set this when simulating logic that reads `block.coinbase` (e.g.
2876    /// MEV/builder tip accounting). `None` lets revm use its default beneficiary.
2877    pub fn set_coinbase(&mut self, coinbase: Option<Address>) {
2878        self.coinbase = coinbase;
2879    }
2880
2881    /// Override `prevrandao` (the `PREVRANDAO` opcode, the post-merge header mix
2882    /// hash) for subsequent simulations.
2883    ///
2884    /// Set this when reproducing contracts that source on-chain randomness from
2885    /// `block.prevrandao`. `None` leaves revm's default in place.
2886    pub fn set_prevrandao(&mut self, prevrandao: Option<B256>) {
2887        self.prevrandao = prevrandao;
2888    }
2889
2890    /// Override the block gas limit (the `GASLIMIT` opcode) for subsequent
2891    /// simulations.
2892    ///
2893    /// Set this when simulating logic that reads `block.gaslimit`. `None` lets
2894    /// revm use its default.
2895    pub fn set_block_gas_limit(&mut self, gas_limit: Option<u64>) {
2896        self.block_gas_limit = gas_limit;
2897    }
2898
2899    /// Re-pin the cache to a specific block number.
2900    ///
2901    /// Updates the SharedBackend pinned block, the batch fetcher block, and the
2902    /// EVM block context (`NUMBER` opcode) in lockstep. The current `basefee` is
2903    /// cleared because it cannot be refreshed synchronously; callers should set it
2904    /// via [`set_block_context`](Self::set_block_context) after fetching the new
2905    /// block header if `BASEFEE` accuracy matters.
2906    pub fn repin_to_block(&mut self, block_number: u64) {
2907        let old_block = self.block;
2908        self.set_block(BlockId::Number(block_number.into()));
2909
2910        if let BlockId::Number(BlockNumberOrTag::Number(old_num)) = old_block {
2911            let drift = block_number.saturating_sub(old_num);
2912            if drift > 0 {
2913                debug!(
2914                    old_block = old_num,
2915                    new_block = block_number,
2916                    drift,
2917                    "Re-pinned cache to current block"
2918                );
2919            }
2920        }
2921    }
2922
2923    /// Ensure an account is loaded into the cache.
2924    ///
2925    /// With the lazy-loading backend, this is optional - accounts are fetched
2926    /// automatically when accessed. However, you can use this to pre-warm
2927    /// the cache for specific accounts.
2928    #[instrument(level = "trace", skip(self))]
2929    pub async fn ensure_account(&mut self, address: Address) -> Result<()> {
2930        if self.db.cache.accounts.contains_key(&address) {
2931            return Ok(());
2932        }
2933
2934        // Load account info via SharedBackend (fetches from RPC if not cached).
2935        // basic_ref populates BlockchainDb; we also insert into the CacheDB
2936        // overlay so the account is immediately available for direct reads.
2937        use revm::database_interface::DatabaseRef;
2938        let info = self
2939            .backend
2940            .basic_ref(address)
2941            .map_err(|e| anyhow!("Failed to fetch account: {:?}", e))?;
2942
2943        if let Some(info) = info {
2944            self.db.insert_account_info(address, info);
2945        }
2946
2947        Ok(())
2948    }
2949
2950    /// Read a single storage slot through the SharedBackend (BlockchainDb -> RPC fallback).
2951    ///
2952    /// After `purge_contract_slots` removes a slot from BlockchainDb, this method fetches
2953    /// fresh data from RPC and caches it in BlockchainDb. Subsequent EVM SLOADs find
2954    /// the value there without additional RPC calls.
2955    pub fn read_storage_slot(&mut self, address: Address, slot: U256) -> Result<U256> {
2956        use revm::database_interface::DatabaseRef;
2957        self.backend
2958            .storage_ref(address, slot)
2959            .map_err(|e| anyhow!("storage read failed for {address} slot {slot}: {e}"))
2960    }
2961
2962    /// Write a raw storage slot value directly into the CacheDB layer.
2963    ///
2964    /// Subsequent EVM SLOADs for this (address, slot) will read the injected value
2965    /// without any RPC call. Used for hot-state injection where we already know the
2966    /// current on-chain value from WebSocket events.
2967    pub fn insert_storage_slot(&mut self, address: Address, slot: U256, value: U256) -> Result<()> {
2968        self.db.insert_account_storage(address, slot, value)?;
2969        Ok(())
2970    }
2971
2972    /// Pre-seed known ERC20 `balanceOf` mapping base slots, keyed by token.
2973    ///
2974    /// Each `(token, slot)` records the storage slot of the token's
2975    /// `mapping(address => uint256) balances`, letting
2976    /// [`set_erc20_balance_with_slot_scan`](Self::set_erc20_balance_with_slot_scan)
2977    /// skip its `0..=max_slot` probing pass for that token and write the balance
2978    /// directly. Seeding a wrong slot is self-correcting: the scan verifies the
2979    /// write and falls back to a fresh probe (evicting the bad seed) if it
2980    /// fails. Later entries overwrite earlier ones for the same token.
2981    pub fn seed_erc20_balance_slots(&mut self, slots: impl IntoIterator<Item = (Address, U256)>) {
2982        for (token, slot) in slots {
2983            self.erc20_balance_slots.insert(token, slot);
2984        }
2985    }
2986
2987    /// Write a value into a Solidity `mapping(address => ...)` entry on
2988    /// `contract`, at the mapping declared at base slot `slot`.
2989    ///
2990    /// Computes the entry's storage key as
2991    /// `keccak256(abi.encode(slot_address, slot))` — Solidity's layout for an
2992    /// address-keyed mapping — and writes `value` there in the CacheDB overlay.
2993    /// Used to forge ERC20 balances and allowances without an on-chain transfer.
2994    ///
2995    /// # Errors
2996    /// Returns an error if the underlying CacheDB storage insert fails (e.g. the
2997    /// account cannot be loaded from the backend).
2998    pub fn insert_mapping_storage_slot(
2999        &mut self,
3000        contract: Address,
3001        slot: U256,
3002        slot_address: Address,
3003        value: U256,
3004    ) -> Result<()> {
3005        let hashed_balance_slot = keccak256((slot_address, slot).abi_encode());
3006        self.db
3007            .insert_account_storage(contract, hashed_balance_slot.into(), value)?;
3008        Ok(())
3009    }
3010
3011    /// Set an ERC20 balance by probing storage mapping slots until `balanceOf(owner)` reflects
3012    /// a probe value, then writing `amount` to the discovered slot.
3013    ///
3014    /// Returns `Ok(true)` if the balance was set and verified, `Ok(false)` if no slot in
3015    /// `0..=max_slot` matched, and `Err` on EVM/cache failures.
3016    pub fn set_erc20_balance_with_slot_scan(
3017        &mut self,
3018        token: Address,
3019        owner: Address,
3020        amount: U256,
3021        max_slot: u16,
3022    ) -> Result<bool> {
3023        if let Some(slot) = self.erc20_balance_slots.get(&token).copied() {
3024            self.insert_mapping_storage_slot(token, slot, owner, amount)?;
3025            if self.erc20_balance_of(token, owner)? == amount {
3026                return Ok(true);
3027            }
3028            self.erc20_balance_slots.remove(&token);
3029        }
3030
3031        let Some(discovered_slot) =
3032            self.discover_erc20_balance_slot_with_scan(token, owner, max_slot)?
3033        else {
3034            return Ok(false);
3035        };
3036
3037        self.insert_mapping_storage_slot(token, discovered_slot, owner, amount)?;
3038        let verified = self.erc20_balance_of(token, owner)? == amount;
3039        if verified {
3040            self.erc20_balance_slots.insert(token, discovered_slot);
3041        } else {
3042            self.erc20_balance_slots.remove(&token);
3043        }
3044        Ok(verified)
3045    }
3046
3047    fn discover_erc20_balance_slot_with_scan(
3048        &mut self,
3049        token: Address,
3050        owner: Address,
3051        max_slot: u16,
3052    ) -> Result<Option<U256>> {
3053        if let Some(slot) = self.erc20_balance_slots.get(&token).copied() {
3054            return Ok(Some(slot));
3055        }
3056
3057        let baseline_snapshot = self.snapshot();
3058        let baseline_balance = self.erc20_balance_of(token, owner)?;
3059
3060        // Choose a probe value distinct from baseline to avoid false positives.
3061        let mut probe = U256::from(0xDEAD_BEEF_u64);
3062        if probe == baseline_balance {
3063            probe = baseline_balance.saturating_add(U256::from(1u64));
3064        }
3065        if probe == baseline_balance {
3066            probe = U256::MAX;
3067        }
3068
3069        for slot_idx in 0..=max_slot {
3070            self.restore(baseline_snapshot.clone());
3071            let slot = U256::from(slot_idx);
3072            self.insert_mapping_storage_slot(token, slot, owner, probe)?;
3073            if self.erc20_balance_of(token, owner)? == probe {
3074                self.restore(baseline_snapshot);
3075                self.erc20_balance_slots.insert(token, slot);
3076                return Ok(Some(slot));
3077            }
3078        }
3079
3080        self.restore(baseline_snapshot);
3081        Ok(None)
3082    }
3083
3084    /// Execute a call with automatic account/storage fetching.
3085    ///
3086    /// Unlike the old implementation, this does NOT prefetch via access lists.
3087    /// The SharedBackend lazily fetches any missing data during execution.
3088    #[instrument(level = "debug", skip(self, calldata), fields(calldata_len = calldata.len()))]
3089    pub fn call(
3090        &mut self,
3091        from: Address,
3092        to: Address,
3093        calldata: Bytes,
3094        commit: bool,
3095    ) -> Result<ExecutionResult> {
3096        self.call_raw(from, to, calldata, commit)
3097    }
3098
3099    /// Execute a call without any prefetching.
3100    ///
3101    /// Data is fetched lazily by the SharedBackend as needed during execution.
3102    #[instrument(level = "debug", skip(self, calldata), fields(calldata_len = calldata.len()))]
3103    pub fn call_raw(
3104        &mut self,
3105        from: Address,
3106        to: Address,
3107        calldata: Bytes,
3108        commit: bool,
3109    ) -> Result<ExecutionResult> {
3110        self.call_raw_with(from, to, calldata, commit, &TxConfig::default())
3111    }
3112
3113    /// Execute a non-committing typed Solidity call from [`Address::ZERO`].
3114    ///
3115    /// This is the typed equivalent of encoding a [`SolCall`], passing it to
3116    /// [`call_raw`](Self::call_raw) with `commit = false`, and decoding the
3117    /// successful return data with [`SolCall::abi_decode_returns`].
3118    ///
3119    /// ```no_run
3120    /// # use alloy_primitives::Address;
3121    /// # use alloy_sol_types::sol;
3122    /// # use evm_fork_cache::cache::EvmCache;
3123    /// # fn example(cache: &mut EvmCache, token: Address, owner: Address) -> anyhow::Result<()> {
3124    /// sol! {
3125    ///     function balanceOf(address account) external view returns (uint256);
3126    /// }
3127    ///
3128    /// let balance = cache.call_sol(token, balanceOfCall { account: owner })?;
3129    /// # let _ = balance;
3130    /// # Ok(())
3131    /// # }
3132    /// ```
3133    pub fn call_sol<C>(&mut self, to: Address, call: C) -> Result<C::Return>
3134    where
3135        C: SolCall,
3136    {
3137        self.call_sol_from(Address::ZERO, to, call)
3138    }
3139
3140    /// Execute a non-committing typed Solidity call from an explicit sender.
3141    ///
3142    /// Uses the default [`TxConfig`], so native value, gas limit/price, nonce,
3143    /// and access list are left at the same defaults as [`call_raw`](Self::call_raw).
3144    pub fn call_sol_from<C>(&mut self, from: Address, to: Address, call: C) -> Result<C::Return>
3145    where
3146        C: SolCall,
3147    {
3148        self.call_sol_with_commit(from, to, call, &TxConfig::default(), false)
3149    }
3150
3151    /// Execute a non-committing typed Solidity call with explicit tx overrides.
3152    ///
3153    /// This is the typed equivalent of [`call_raw_with`](Self::call_raw_with)
3154    /// with `commit = false`.
3155    pub fn call_sol_with<C>(
3156        &mut self,
3157        from: Address,
3158        to: Address,
3159        call: C,
3160        tx: &TxConfig,
3161    ) -> Result<C::Return>
3162    where
3163        C: SolCall,
3164    {
3165        self.call_sol_with_commit(from, to, call, tx, false)
3166    }
3167
3168    /// Execute a typed Solidity call and commit its state changes.
3169    ///
3170    /// This is the typed equivalent of [`call_raw_with`](Self::call_raw_with)
3171    /// with `commit = true`; the call's state changes are persisted through the
3172    /// same path as the raw committing API before the return data is decoded.
3173    pub fn transact_sol<C>(
3174        &mut self,
3175        from: Address,
3176        to: Address,
3177        call: C,
3178        tx: &TxConfig,
3179    ) -> Result<C::Return>
3180    where
3181        C: SolCall,
3182    {
3183        self.call_sol_with_commit(from, to, call, tx, true)
3184    }
3185
3186    /// Execute a call with explicit transaction-environment overrides
3187    /// ([`TxConfig`]): native `value`, gas limit/price, nonce, and an input
3188    /// access list. This is the entry point for value-bearing and gas-bounded
3189    /// simulation; [`call_raw`](Self::call_raw) is the zero-value shorthand.
3190    #[instrument(level = "debug", skip(self, calldata, tx), fields(calldata_len = calldata.len()))]
3191    pub fn call_raw_with(
3192        &mut self,
3193        from: Address,
3194        to: Address,
3195        calldata: Bytes,
3196        commit: bool,
3197        tx: &TxConfig,
3198    ) -> Result<ExecutionResult> {
3199        let tx_env = Self::build_tx_env_with(from, to, calldata, tx)?;
3200        let mut evm = self.build_evm();
3201
3202        if commit {
3203            return evm
3204                .transact_commit(tx_env)
3205                .map_err(|e| anyhow!("Failed to transact: {:?}", e));
3206        }
3207
3208        let checkpoint = evm.journaled_state.checkpoint();
3209        let result = evm.transact_one(tx_env);
3210        evm.journaled_state.checkpoint_revert(checkpoint);
3211        result.map_err(|e| anyhow!("Failed to transact: {:?}", e))
3212    }
3213
3214    /// Execute a non-committing call and extract the access list of touched
3215    /// accounts and storage slots before reverting.
3216    ///
3217    /// Used for EIP-2929 marginal gas estimation in batched simulations.
3218    pub fn call_raw_with_access_list(
3219        &mut self,
3220        from: Address,
3221        to: Address,
3222        calldata: Bytes,
3223    ) -> Result<(ExecutionResult, StorageAccessList)> {
3224        let tx = Self::build_tx_env(from, to, calldata)?;
3225        let mut evm = self.build_evm();
3226
3227        let checkpoint = evm.journaled_state.checkpoint();
3228        match evm.transact_one(tx) {
3229            Ok(result) => {
3230                // Extract access list from journaled state before reverting. After
3231                // transact_one, journaled_state.state holds all touched accounts/slots.
3232                let mut access_list = StorageAccessList::default();
3233                for (address, account) in evm.journaled_state.state.iter() {
3234                    if account.is_touched() {
3235                        access_list.accounts.insert(*address);
3236                        for (slot_key, _) in account.storage.iter() {
3237                            access_list.slots.insert((*address, *slot_key));
3238                        }
3239                    }
3240                }
3241                evm.journaled_state.checkpoint_revert(checkpoint);
3242                Ok((result, access_list))
3243            }
3244            Err(e) => {
3245                // Revert the checkpoint even on a host/transact error so the EVM
3246                // journal is not left dirty (mirrors `call_raw`).
3247                evm.journaled_state.checkpoint_revert(checkpoint);
3248                Err(anyhow!("Failed to transact: {:?}", e))
3249            }
3250        }
3251    }
3252
3253    /// Execute a call and return its emitted logs and gas used.
3254    ///
3255    /// A thin wrapper over [`call`](Self::call) that requires success and
3256    /// discards the return data. When `commit` is true the call's state changes
3257    /// are persisted to the CacheDB overlay; otherwise they are reverted.
3258    ///
3259    /// # Errors
3260    /// Returns an error if the underlying transact fails, or if the call did not
3261    /// `Success` (i.e. it reverted or halted).
3262    pub fn call_logs(
3263        &mut self,
3264        from: Address,
3265        to: Address,
3266        calldata: Bytes,
3267        commit: bool,
3268    ) -> Result<(Vec<Log>, u64)> {
3269        let result = self.call(from, to, calldata, commit)?;
3270        if let ExecutionResult::Success { logs, gas_used, .. } = result {
3271            Ok((logs, gas_used))
3272        } else {
3273            Err(anyhow!("Failed to call: {:?}", result))
3274        }
3275    }
3276
3277    /// Read an ERC20 token balance by simulating a `balanceOf(owner)` call.
3278    ///
3279    /// Non-committing: the read is reverted, so it never mutates cache state.
3280    ///
3281    /// # Errors
3282    /// Returns an error if the simulated call fails or does not `Success` (e.g.
3283    /// `token` is not a contract or reverts), or if the returned data cannot be
3284    /// ABI-decoded as a `uint256`.
3285    pub fn erc20_balance_of(&mut self, token: Address, owner: Address) -> Result<U256> {
3286        let call = IERC20::balanceOfCall { target: owner };
3287        let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
3288
3289        match result {
3290            ExecutionResult::Success { output, .. } => {
3291                let out = output.into_data();
3292                let balance = IERC20::balanceOfCall::abi_decode_returns(&out)
3293                    .map_err(|e| anyhow!("Failed to decode balanceOf: {:?}", e))?;
3294                Ok(balance)
3295            }
3296            _ => Err(anyhow!("balanceOf call failed: {:?}", result)),
3297        }
3298    }
3299
3300    /// Read an ERC20 allowance by simulating an `allowance(owner, spender)` call.
3301    ///
3302    /// Non-committing: the read is reverted, so it never mutates cache state.
3303    ///
3304    /// # Errors
3305    /// Returns an error if the simulated call fails or does not `Success` (e.g.
3306    /// `token` is not a contract or reverts), or if the returned data cannot be
3307    /// ABI-decoded as a `uint256`.
3308    pub fn erc20_allowance(
3309        &mut self,
3310        token: Address,
3311        owner: Address,
3312        spender: Address,
3313    ) -> Result<U256> {
3314        let call = IERC20::allowanceCall { owner, spender };
3315        let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
3316
3317        match result {
3318            ExecutionResult::Success { output, .. } => {
3319                let out = output.into_data();
3320                let allowance = IERC20::allowanceCall::abi_decode_returns(&out)
3321                    .map_err(|e| anyhow!("Failed to decode allowance: {:?}", e))?;
3322                Ok(allowance)
3323            }
3324            _ => Err(anyhow!("allowance call failed: {:?}", result)),
3325        }
3326    }
3327
3328    /// Read an ERC20 token's decimals by simulating a `decimals()` call.
3329    ///
3330    /// Memoized: a hit in the in-memory token-decimals map returns immediately
3331    /// without simulating. On a miss the value is resolved by a non-committing
3332    /// `decimals()` call.
3333    ///
3334    /// # Side effects
3335    /// On a miss the resolved value is cached in **both** the in-memory
3336    /// token-decimals map (process lifetime) **and** the immutable data cache
3337    /// (so it is persisted to disk on the next [`flush`](Self::flush)).
3338    ///
3339    /// # Errors
3340    /// Returns an error if the simulated call fails or does not `Success` (e.g.
3341    /// `token` is not a contract or reverts), or if the returned data cannot be
3342    /// ABI-decoded as a `uint8`.
3343    pub fn erc20_decimals(&mut self, token: Address) -> Result<u8> {
3344        if let Some(decimals) = self.token_decimals.get(&token) {
3345            return Ok(*decimals);
3346        }
3347
3348        let call = IERC20::decimalsCall {};
3349        let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
3350
3351        match result {
3352            ExecutionResult::Success { output, .. } => {
3353                let out = output.into_data();
3354                let decimals = IERC20::decimalsCall::abi_decode_returns(&out)
3355                    .map_err(|e| anyhow!("Failed to decode decimals: {:?}", e))?;
3356                self.token_decimals.insert(token, decimals);
3357                // Also update immutable cache for persistence
3358                self.immutable_cache.set_token_decimals(token, decimals);
3359                Ok(decimals)
3360            }
3361            _ => Err(anyhow!("decimals call failed: {:?}", result)),
3362        }
3363    }
3364
3365    /// Get a reference to the immutable data cache (token decimals).
3366    pub fn immutable_cache(&self) -> &ImmutableDataCache {
3367        &self.immutable_cache
3368    }
3369
3370    /// Get a mutable reference to the immutable data cache.
3371    ///
3372    /// Use this to pre-populate token decimals that would otherwise be discovered
3373    /// lazily. Entries are persisted on the next [`flush`](Self::flush) (and on
3374    /// drop) when a [`CacheConfig`] is set.
3375    pub fn immutable_cache_mut(&mut self) -> &mut ImmutableDataCache {
3376        &mut self.immutable_cache
3377    }
3378
3379    /// Check if an address has storage slots pre-loaded in the BlockchainDb.
3380    ///
3381    /// This is useful to determine if we loaded the EVM state from the unified
3382    /// `evm_state.bin` cache and an address already has reusable storage.
3383    ///
3384    /// # Arguments
3385    /// * `address` - The contract address to check
3386    ///
3387    /// # Returns
3388    /// `true` if the address has any storage slots in the underlying BlockchainDb,
3389    /// `false` otherwise
3390    pub fn has_contract_storage(&self, address: Address) -> bool {
3391        let storage = self.blockchain_db.storage().read();
3392        storage
3393            .get(&address)
3394            .map(|slots| !slots.is_empty())
3395            .unwrap_or(false)
3396    }
3397
3398    /// Get the number of storage slots loaded for a contract address.
3399    ///
3400    /// Useful for debugging and logging to understand cache state.
3401    pub fn contract_storage_slot_count(&self, address: Address) -> usize {
3402        let storage = self.blockchain_db.storage().read();
3403        storage.get(&address).map(|slots| slots.len()).unwrap_or(0)
3404    }
3405
3406    /// Get memory statistics for the shared memory buffer used during EVM simulations.
3407    ///
3408    /// Returns a tuple of (current_capacity_bytes, current_length_bytes).
3409    ///
3410    /// The capacity represents the high-water mark of memory usage across all
3411    /// simulations since the buffer grows but doesn't shrink. The length is
3412    /// typically 0 between simulations (cleared after each use).
3413    ///
3414    /// # Use Case
3415    /// Call this after running a batch of simulations to understand memory usage
3416    /// and inform the optimal initial capacity for `SharedMemory`.
3417    ///
3418    /// # Example
3419    /// ```ignore
3420    /// let (capacity, _len) = cache.shared_memory_stats();
3421    /// println!("Peak memory usage: {} KB", capacity / 1024);
3422    /// ```
3423    pub fn shared_memory_stats(&self) -> (usize, usize) {
3424        let buffer = self.shared_memory_buffer.borrow();
3425        (buffer.capacity(), buffer.len())
3426    }
3427
3428    /// Log the current shared memory buffer statistics.
3429    ///
3430    /// Useful for profiling after running a batch of simulations.
3431    pub fn log_shared_memory_stats(&self) {
3432        let (capacity, len) = self.shared_memory_stats();
3433        debug!(
3434            capacity_bytes = capacity,
3435            capacity_kb = capacity / 1024,
3436            current_len = len,
3437            "Shared memory buffer stats (peak capacity across simulations)"
3438        );
3439    }
3440
3441    /// Pre-allocate the shared memory buffer to a specific capacity.
3442    ///
3443    /// Use this after measuring peak usage to avoid reallocation overhead
3444    /// during simulations. The buffer will grow beyond this if needed,
3445    /// but pre-sizing to the expected peak eliminates allocations.
3446    ///
3447    /// # Arguments
3448    /// * `capacity` - The capacity in bytes to reserve
3449    ///
3450    /// # Example
3451    /// ```ignore
3452    /// // After profiling shows peak usage is ~32KB
3453    /// cache.reserve_shared_memory(32 * 1024);
3454    /// ```
3455    pub fn reserve_shared_memory(&mut self, capacity: usize) {
3456        let mut buffer = self.shared_memory_buffer.borrow_mut();
3457        let current_capacity = buffer.capacity();
3458        if current_capacity < capacity {
3459            buffer.reserve(capacity - current_capacity);
3460            debug!(
3461                new_capacity = buffer.capacity(),
3462                requested = capacity,
3463                "Reserved shared memory buffer capacity"
3464            );
3465        }
3466        drop(buffer);
3467        // Record the high-water mark so snapshots taken afterwards propagate it to
3468        // their overlays (snapshots copy the capacity at creation time).
3469        self.shared_memory_capacity = self.shared_memory_capacity.max(capacity);
3470    }
3471
3472    /// The resolved per-context EVM shared-memory pre-allocation, in bytes.
3473    ///
3474    /// This is the [`SharedMemoryCapacity`] configured on the
3475    /// [`EvmCacheBuilder`] resolved to a concrete size (with
3476    /// [`SharedMemoryCapacity::Auto`] resolved against the state loaded at
3477    /// construction), raised by any later [`reserve_shared_memory`](Self::reserve_shared_memory).
3478    /// Each [`create_snapshot`](Self::create_snapshot) copies it onto the snapshot
3479    /// so snapshot-backed [`EvmOverlay`]s pre-allocate the same amount.
3480    pub fn shared_memory_capacity(&self) -> usize {
3481        self.shared_memory_capacity
3482    }
3483
3484    /// Purge all storage slots for a specific contract from both cache layers.
3485    ///
3486    /// This clears:
3487    /// 1. **CacheDB overlay** (`self.db.cache.accounts[addr].storage`) - the in-memory
3488    ///    layer that caches storage slots fetched during EVM execution. Without clearing
3489    ///    this layer, subsequent EVM calls return stale values even after the backend
3490    ///    is purged.
3491    /// 2. **BlockchainDb backend** (`self.blockchain_db.storage()`) - the persistent
3492    ///    layer that caches RPC responses and is loaded from `evm_state.bin`.
3493    ///
3494    /// After purging both layers, the next EVM read for this contract's storage will
3495    /// go all the way to the RPC for fresh data.
3496    pub fn purge_contract_storage(&mut self, address: Address) -> usize {
3497        // Thin wrapper over the unified purge primitive; returns the backend slot
3498        // count the `AllStorage` scope removed.
3499        self.apply_update(&StateUpdate::purge(address, PurgeScope::AllStorage))
3500            .purged
3501            .first()
3502            .map(|rec| rec.slots_removed)
3503            .unwrap_or(0)
3504    }
3505
3506    /// `AllStorage`-scope purge layer logic. Clears the overlay storage for
3507    /// `address` and removes its backend storage map. Returns the number of
3508    /// backend slots removed.
3509    fn purge_contract_storage_inner(&mut self, address: Address) -> usize {
3510        // Layer 1: Clear CacheDB overlay
3511        let cache_db_cleared = if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
3512            let count = db_account.storage.len();
3513            db_account.storage.clear();
3514            count
3515        } else {
3516            0
3517        };
3518
3519        // Layer 2: Clear BlockchainDb backend
3520        let backend_cleared = {
3521            let mut storage = self.blockchain_db.storage().write();
3522            if let Some(slots) = storage.remove(&address) {
3523                slots.len()
3524            } else {
3525                0
3526            }
3527        };
3528
3529        if cache_db_cleared > 0 || backend_cleared > 0 {
3530            debug!(
3531                contract = %address,
3532                cache_db_slots = cache_db_cleared,
3533                backend_slots = backend_cleared,
3534                "purged contract storage from both cache layers"
3535            );
3536        }
3537
3538        // Layer-2 storage for this address was removed → invalidate base.
3539        self.mark_base_dirty(address);
3540        backend_cleared
3541    }
3542
3543    /// Purge specific storage slots for a contract from both cache layers.
3544    ///
3545    /// Unlike `purge_contract_storage()` which removes ALL storage, this only removes
3546    /// the specified slots. This is useful when only a narrow subset of hot storage
3547    /// became stale and the rest of the contract's cached storage should be kept.
3548    ///
3549    /// Returns the number of slots removed from the BlockchainDb backend.
3550    pub fn purge_contract_slots(&mut self, address: Address, slots: &[U256]) -> usize {
3551        // Thin wrapper over the unified purge primitive; returns the backend slot
3552        // count the `Slots` scope removed.
3553        self.apply_update(&StateUpdate::purge(
3554            address,
3555            PurgeScope::Slots(slots.to_vec()),
3556        ))
3557        .purged
3558        .first()
3559        .map(|rec| rec.slots_removed)
3560        .unwrap_or(0)
3561    }
3562
3563    /// `Slots`-scope purge layer logic. Removes the listed slots from the overlay
3564    /// and the backend storage map. Returns the number of backend slots removed.
3565    fn purge_contract_slots_inner(&mut self, address: Address, slots: &[U256]) -> usize {
3566        let mut cache_db_removed = 0usize;
3567        let mut backend_removed = 0usize;
3568
3569        // Layer 1: Remove specific slots from CacheDB overlay
3570        if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
3571            for slot in slots {
3572                if db_account.storage.remove(slot).is_some() {
3573                    cache_db_removed += 1;
3574                }
3575            }
3576        }
3577
3578        // Layer 2: Remove specific slots from BlockchainDb backend
3579        {
3580            let mut storage = self.blockchain_db.storage().write();
3581            if let Some(address_storage) = storage.get_mut(&address) {
3582                for slot in slots {
3583                    if address_storage.remove(slot).is_some() {
3584                        backend_removed += 1;
3585                    }
3586                }
3587            }
3588        }
3589
3590        if cache_db_removed > 0 || backend_removed > 0 {
3591            trace!(
3592                contract = %address,
3593                requested = slots.len(),
3594                cache_db_removed,
3595                backend_removed,
3596                "selectively purged contract storage slots from both cache layers"
3597            );
3598        }
3599
3600        // Layer-2 storage for this address changed (slots dropped) → invalidate
3601        // base. The growth scan only catches length changes; mark explicitly.
3602        self.mark_base_dirty(address);
3603        backend_removed
3604    }
3605
3606    /// Purge storage slots for multiple contracts from both cache layers.
3607    ///
3608    /// See `purge_contract_storage()` for details on what each layer contains.
3609    pub fn purge_contracts_storage(
3610        &mut self,
3611        addresses: impl IntoIterator<Item = Address>,
3612    ) -> usize {
3613        let mut total_purged = 0usize;
3614
3615        for address in addresses {
3616            // Layer 1: Clear CacheDB overlay
3617            if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
3618                db_account.storage.clear();
3619            }
3620
3621            // Layer 2: Clear BlockchainDb backend
3622            let mut storage = self.blockchain_db.storage().write();
3623            if let Some(slots) = storage.remove(&address) {
3624                let count = slots.len();
3625                if count > 0 {
3626                    debug!(
3627                        contract = %address,
3628                        slots_removed = count,
3629                        "purged contract storage from both cache layers"
3630                    );
3631                }
3632                total_purged += count;
3633            }
3634        }
3635
3636        if total_purged > 0 {
3637            debug!(
3638                total_slots_purged = total_purged,
3639                "purged contract storage from both cache layers"
3640            );
3641        }
3642        // Multiple layer-2 contracts changed → full base rebuild (coarse but
3643        // correct; cheaper than enumerating each touched address here).
3644        self.invalidate_base();
3645        total_purged
3646    }
3647
3648    /// Purge ALL storage slots from both cache layers while preserving bytecodes.
3649    ///
3650    /// Use this for periodic full cache refresh (e.g., every 48 hours) to ensure
3651    /// any stale data like strategy swap paths, proxy implementations, reward rates,
3652    /// etc. are re-fetched from the actual on-chain state.
3653    ///
3654    /// This preserves:
3655    /// - Account info (nonce, balance, code hash)
3656    /// - Contract bytecodes (immutable)
3657    ///
3658    /// This purges:
3659    /// - All storage slots from CacheDB overlay (layer 1)
3660    /// - All storage slots from BlockchainDb backend (layer 2)
3661    ///
3662    /// # Returns
3663    /// The total number of storage slots that were removed from the BlockchainDb
3664    pub fn purge_all_storage(&mut self) -> usize {
3665        // Layer 1: Clear all storage in CacheDB overlay
3666        let mut cache_db_cleared = 0usize;
3667        for db_account in self.db.cache.accounts.values_mut() {
3668            cache_db_cleared += db_account.storage.len();
3669            db_account.storage.clear();
3670        }
3671
3672        // Layer 2: Clear BlockchainDb backend
3673        let (total_slots, contract_count) = {
3674            let mut storage = self.blockchain_db.storage().write();
3675            let total_slots: usize = storage.values().map(|s| s.len()).sum();
3676            let contract_count = storage.len();
3677            storage.clear();
3678            (total_slots, contract_count)
3679        };
3680
3681        if total_slots > 0 || cache_db_cleared > 0 {
3682            warn!(
3683                contracts_cleared = contract_count,
3684                backend_slots_purged = total_slots,
3685                cache_db_slots_purged = cache_db_cleared,
3686                "purged ALL storage from both cache layers (full refresh)"
3687            );
3688        }
3689        // All layer-2 storage was cleared → full base rebuild.
3690        self.invalidate_base();
3691        total_slots
3692    }
3693
3694    /// Enumerate all cached storage slots for a contract address.
3695    ///
3696    /// Returns the union of slot keys from both CacheDB overlay (layer 1) and
3697    /// BlockchainDb backend (layer 2). Used by the slot observation tracker to
3698    /// selectively purge only slots likely to have changed.
3699    pub fn enumerate_contract_slots(&self, address: Address) -> Vec<U256> {
3700        let mut slots: HashSet<U256> = HashSet::new();
3701
3702        // Layer 1: CacheDB overlay
3703        if let Some(db_account) = self.db.cache.accounts.get(&address) {
3704            slots.extend(db_account.storage.keys().copied());
3705        }
3706
3707        // Layer 2: BlockchainDb backend
3708        let storage = self.blockchain_db.storage().read();
3709        if let Some(backend_slots) = storage.get(&address) {
3710            slots.extend(backend_slots.keys().copied());
3711        }
3712
3713        slots.into_iter().collect()
3714    }
3715
3716    /// Return all contract addresses that have cached storage in either layer.
3717    ///
3718    /// Used by the observation-aware full purge to enumerate what needs checking.
3719    pub fn all_cached_contract_addresses(&self) -> Vec<Address> {
3720        let mut addrs: HashSet<Address> = HashSet::new();
3721
3722        // Layer 1: CacheDB overlay
3723        for (addr, account) in &self.db.cache.accounts {
3724            if !account.storage.is_empty() {
3725                addrs.insert(*addr);
3726            }
3727        }
3728
3729        // Layer 2: BlockchainDb backend
3730        let storage = self.blockchain_db.storage().read();
3731        for addr in storage.keys() {
3732            addrs.insert(*addr);
3733        }
3734
3735        addrs.into_iter().collect()
3736    }
3737
3738    /// Get the number of storage slots in the CacheDB overlay for a contract.
3739    ///
3740    /// This is useful for diagnostics: if a contract has slots in the CacheDB
3741    /// overlay, they will be served on EVM reads without going to the backend.
3742    pub fn cache_db_storage_slot_count(&self, address: Address) -> usize {
3743        self.db
3744            .cache
3745            .accounts
3746            .get(&address)
3747            .map(|a| a.storage.len())
3748            .unwrap_or(0)
3749    }
3750
3751    /// Simulate a call and compute `owner`'s net balance change for each token
3752    /// in `tokens` by reading `balanceOf(owner)` immediately before and after.
3753    ///
3754    /// Each delta is the signed `post - pre` difference (see
3755    /// [`CallSimulationResult::token_deltas`]). When `commit` is true the call's
3756    /// state changes are persisted to the CacheDB overlay; otherwise they are
3757    /// reverted. Unlike
3758    /// [`simulate_with_transfer_tracking`](Self::simulate_with_transfer_tracking),
3759    /// this measures deltas via pre/post balance reads (not transfer-event
3760    /// inspection). The returned [`access_list`](CallSimulationResult::access_list)
3761    /// includes the accounts and slots touched by the pre/post `balanceOf` reads
3762    /// and the simulated call.
3763    ///
3764    /// # Errors
3765    /// Returns an error if building the tx env fails, if a pre/post
3766    /// `balanceOf` read fails, or if the call does not `Success` (i.e. it
3767    /// reverted or halted). On error the simulation is reverted.
3768    pub fn simulate_call_with_balance_deltas(
3769        &mut self,
3770        from: Address,
3771        to: Address,
3772        calldata: Bytes,
3773        owner: Address,
3774        tokens: impl IntoIterator<Item = Address>,
3775        commit: bool,
3776    ) -> Result<CallSimulationResult> {
3777        let token_list: Vec<Address> = tokens.into_iter().collect();
3778
3779        let mut pre_balances = HashMap::with_capacity(token_list.len());
3780        let mut access_lists = Vec::with_capacity(token_list.len().saturating_mul(2) + 1);
3781        for token in &token_list {
3782            let mut evm = self.build_evm();
3783            let synthetic_beneficiary = Self::seed_synthetic_beneficiary(&mut evm);
3784            let (balance, access_list) =
3785                Self::erc20_balance_of_in_evm_isolated(&mut evm, from, *token, owner)?;
3786            Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3787            pre_balances.insert(*token, balance);
3788            access_lists.push(access_list);
3789        }
3790
3791        let tx = Self::build_tx_env(from, to, calldata)?;
3792        let mut evm = self.build_evm();
3793        let synthetic_beneficiary = Self::seed_synthetic_beneficiary(&mut evm);
3794        let target_checkpoint = evm.journaled_state.checkpoint();
3795        let result = evm
3796            .transact_one(tx)
3797            .map_err(|e| anyhow!("Failed to transact: {:?}", e))?;
3798        let (logs, gas_used, output) = match result {
3799            ExecutionResult::Success {
3800                logs,
3801                gas_used,
3802                output,
3803                ..
3804            } => (logs, gas_used, output.into_data()),
3805            _ => {
3806                evm.journaled_state.checkpoint_revert(target_checkpoint);
3807                Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3808                return Err(anyhow!("Failed to call: {:?}", result));
3809            }
3810        };
3811        access_lists.push(extract_access_list(&evm.journaled_state.state));
3812
3813        let mut token_deltas = HashMap::with_capacity(token_list.len());
3814        for token in &token_list {
3815            let (post, access_list) =
3816                match Self::erc20_balance_of_in_evm_isolated(&mut evm, from, *token, owner) {
3817                    Ok(result) => result,
3818                    Err(err) => {
3819                        evm.journaled_state.checkpoint_revert(target_checkpoint);
3820                        Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3821                        return Err(err);
3822                    }
3823                };
3824            let pre = pre_balances.get(token).copied().unwrap_or_default();
3825            token_deltas.insert(*token, I256::from_raw(post) - I256::from_raw(pre));
3826            access_lists.push(access_list);
3827        }
3828
3829        let access_list = merge_access_lists(access_lists);
3830        if commit {
3831            Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3832            evm.commit_inner();
3833        } else {
3834            evm.journaled_state.checkpoint_revert(target_checkpoint);
3835            Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3836        }
3837
3838        Ok(CallSimulationResult {
3839            status: SimStatus::Success,
3840            gas_used,
3841            token_deltas,
3842            logs,
3843            access_list,
3844            output,
3845        })
3846    }
3847
3848    /// Simulate a call and track token balance changes using a TransferInspector.
3849    ///
3850    /// This method uses EVM inspection to capture ERC20 Transfer events during execution,
3851    /// eliminating the need for manual balance reads before/after the transaction.
3852    ///
3853    /// Returns:
3854    /// - `Ok(CallSimulationResult)` on successful execution
3855    /// - `Err(SimError::Revert(_))` when the transaction reverts (graceful failure)
3856    /// - `Err(SimError::Other(_))` for unexpected errors (should be propagated)
3857    #[instrument(level = "debug", skip(self, calldata, tokens), fields(calldata_len = calldata.len()))]
3858    pub fn simulate_with_transfer_tracking(
3859        &mut self,
3860        from: Address,
3861        to: Address,
3862        calldata: Bytes,
3863        owner: Address,
3864        tokens: Option<impl IntoIterator<Item = Address>>,
3865        commit: bool,
3866    ) -> SimulationResult<CallSimulationResult> {
3867        let tx = Self::build_tx_env(from, to, calldata).map_err(SimError::Other)?;
3868        let inspector = TransferInspector::new();
3869        let mut evm = self.build_evm_with_inspector(inspector);
3870        let checkpoint = evm.journaled_state.checkpoint();
3871
3872        let result = evm
3873            .inspect_one_tx(tx)
3874            .map_err(|e| SimError::Other(anyhow!("Failed to transact: {:?}", e)));
3875
3876        match result {
3877            Ok(ExecutionResult::Success {
3878                logs,
3879                gas_used,
3880                output,
3881                ..
3882            }) => {
3883                // Compute balance deltas from captured transfers
3884                let token_deltas = if let Some(token_list) = tokens {
3885                    evm.inspector.balance_deltas_for_tokens(owner, token_list)
3886                } else {
3887                    evm.inspector.balance_deltas(owner)
3888                };
3889
3890                // Log shared memory buffer capacity for profiling
3891                let memory_capacity = evm.ctx.local.shared_memory_buffer.borrow().capacity();
3892                trace!(
3893                    memory_capacity_bytes = memory_capacity,
3894                    memory_capacity_kb = memory_capacity / 1024,
3895                    "EVM shared memory buffer capacity after simulation"
3896                );
3897
3898                // Extract EIP-2930 access list from journaled state before commit/revert.
3899                // After inspect_one_tx, state contains all touched accounts and storage slots.
3900                let access_list = extract_access_list(&evm.journaled_state.state);
3901
3902                if commit {
3903                    evm.commit_inner();
3904                } else {
3905                    evm.journaled_state.checkpoint_revert(checkpoint);
3906                }
3907
3908                Ok(CallSimulationResult {
3909                    status: SimStatus::Success,
3910                    gas_used,
3911                    token_deltas,
3912                    logs,
3913                    access_list,
3914                    output: output.into_data(),
3915                })
3916            }
3917            Ok(ExecutionResult::Revert { gas_used, output }) => {
3918                evm.journaled_state.checkpoint_revert(checkpoint);
3919                Err(SimulationError::from_revert(gas_used, output).into())
3920            }
3921            Ok(ExecutionResult::Halt { reason, gas_used }) => {
3922                evm.journaled_state.checkpoint_revert(checkpoint);
3923                Err(SimError::Halt {
3924                    reason: format!("{reason:?}"),
3925                    gas_used,
3926                })
3927            }
3928            Err(err) => {
3929                evm.journaled_state.checkpoint_revert(checkpoint);
3930                Err(err)
3931            }
3932        }
3933    }
3934
3935    /// Simulate a call with transfer tracking without any prefetching.
3936    ///
3937    /// This is identical to `simulate_with_transfer_tracking` since we no longer
3938    /// do access list prefetching. Kept for API compatibility.
3939    pub fn simulate_with_transfer_tracking_raw(
3940        &mut self,
3941        from: Address,
3942        to: Address,
3943        calldata: Bytes,
3944        owner: Address,
3945        tokens: Option<impl IntoIterator<Item = Address>>,
3946        commit: bool,
3947    ) -> SimulationResult<CallSimulationResult> {
3948        self.simulate_with_transfer_tracking(from, to, calldata, owner, tokens, commit)
3949    }
3950
3951    /// Simulate an ordered transaction **bundle** over cumulative block state,
3952    /// with a revert policy and coinbase/miner-payment accounting (Phase 6
3953    /// Track A+B).
3954    ///
3955    /// This is a convenience wrapper: it snapshots the cache and runs the bundle
3956    /// on a fresh transient [`EvmOverlay`] via
3957    /// [`EvmOverlay::simulate_bundle`](crate::cache::EvmOverlay::simulate_bundle),
3958    /// which carries the full semantics (ordered cumulative state, the
3959    /// [`RevertPolicy`](crate::bundle::RevertPolicy), and coinbase accounting).
3960    ///
3961    /// The cache itself is **never** mutated — even when `opts.commit` is `true`.
3962    /// `commit` controls only whether the bundle's cumulative state is folded
3963    /// into the transient overlay (and is therefore moot here, since that overlay
3964    /// is dropped when this call returns). Snapshot the cache yourself and drive
3965    /// [`EvmOverlay::simulate_bundle`] directly when you need the committed
3966    /// overlay state to outlive the call (e.g. to chain a follow-up read).
3967    ///
3968    /// # Errors
3969    ///
3970    /// Returns [`SimError`] if a transaction environment cannot be built or revm
3971    /// fails to transact. A transaction reverting is reported through the
3972    /// per-transaction outcome and the revert policy, not as an error.
3973    pub fn simulate_bundle(
3974        &mut self,
3975        txs: &[crate::bundle::BundleTx],
3976        opts: &crate::bundle::BundleOptions,
3977    ) -> SimulationResult<crate::bundle::BundleResult> {
3978        let snapshot = self.create_snapshot();
3979        let mut overlay = EvmOverlay::new(snapshot, None);
3980        overlay.simulate_bundle(txs, opts)
3981    }
3982
3983    /// Deploy a contract via CREATE transaction and return the deployed address.
3984    ///
3985    /// The `creation_code` should include the init code with ABI-encoded constructor
3986    /// arguments appended. Nonce checks are disabled, so any `from` address works.
3987    ///
3988    /// Note: This commits the deployment to the CacheDB. Use a throw-away deployer
3989    /// address (e.g., `Address::ZERO`) to avoid side effects on real accounts.
3990    ///
3991    /// # Errors
3992    /// Returns an error if the CREATE tx env cannot be built, if the deployment
3993    /// reverts or halts, or if it succeeds but the EVM returns no contract
3994    /// address.
3995    pub fn deploy_contract(&mut self, from: Address, creation_code: Bytes) -> Result<Address> {
3996        let tx = TxEnv::builder()
3997            .caller(from)
3998            .kind(TxKind::Create)
3999            .data(creation_code)
4000            .value(U256::ZERO)
4001            .build()
4002            .map_err(|e| anyhow!("Failed to build CREATE tx: {:?}", e))?;
4003
4004        // Use a relaxed contract size limit for deployment. Arbitrum supports
4005        // larger contracts than the EIP-170 24576-byte limit via ArbOS.
4006        let mut evm = self.build_evm();
4007        evm.cfg.limit_contract_code_size = Some(usize::MAX);
4008        let result = evm
4009            .transact_commit(tx)
4010            .map_err(|e| anyhow!("Contract deployment failed: {:?}", e))?;
4011
4012        match result {
4013            ExecutionResult::Success { output, .. } => output
4014                .address()
4015                .copied()
4016                .ok_or_else(|| anyhow!("Contract deployment succeeded but no address returned")),
4017            ExecutionResult::Revert { output, .. } => Err(anyhow!(
4018                "Contract deployment reverted: 0x{}",
4019                alloy_primitives::hex::encode(&output)
4020            )),
4021            ExecutionResult::Halt { reason, .. } => {
4022                Err(anyhow!("Contract deployment halted: {:?}", reason))
4023            }
4024        }
4025    }
4026
4027    /// Override the bytecode at `target` address with bytecode from `source` address.
4028    ///
4029    /// Copies only non-empty runtime code and code_hash; storage, balance, and nonce
4030    /// at `target` remain unchanged. `target` must already have non-empty runtime
4031    /// bytecode. Both the CacheDB overlay and BlockchainDb backend are updated,
4032    /// ensuring the override is visible to parallel EVM tasks sharing the same backend.
4033    ///
4034    /// # Errors
4035    /// Returns an error if `source` has no cached bytecode or its code is empty,
4036    /// if `target` cannot be loaded (it must already exist on the backend), or
4037    /// if `target` has no existing runtime bytecode to override. For synthetic
4038    /// `target` addresses that may not exist, use
4039    /// [`override_or_create_account_code`](Self::override_or_create_account_code).
4040    pub fn override_account_code(&mut self, source: Address, target: Address) -> Result<()> {
4041        self.override_account_code_with_missing_target(source, target, MissingTargetBehavior::Error)
4042    }
4043
4044    /// Override the bytecode at `target`, creating a default target account when absent.
4045    ///
4046    /// Use this for synthetic addresses in local simulations. For live forked
4047    /// accounts where storage/balance/nonce must be preserved, prefer
4048    /// [`Self::override_account_code`].
4049    pub fn override_or_create_account_code(
4050        &mut self,
4051        source: Address,
4052        target: Address,
4053    ) -> Result<()> {
4054        self.override_account_code_with_missing_target(
4055            source,
4056            target,
4057            MissingTargetBehavior::Create,
4058        )
4059    }
4060
4061    /// Override code at `target`, with explicit behavior for missing target accounts.
4062    ///
4063    /// This is intentionally **not** folded onto
4064    /// [`apply_update`](Self::apply_update)'s `Account` code patch: it copies code
4065    /// from a `source` account, preserves the target's existing balance/nonce/
4066    /// storage, and **unconditionally materializes** the target in the CacheDB
4067    /// overlay (the primary read path for EVM execution, required for the
4068    /// `Create` synthetic-target case). The generic primitive writes the overlay
4069    /// only when an account is already present, so the two are not
4070    /// behavior-equivalent. For a plain code overwrite that follows the
4071    /// dual-layer write-through policy, use
4072    /// `apply_update(StateUpdate::Account { patch: AccountPatch::default().code(..) })`.
4073    pub fn override_account_code_with_missing_target(
4074        &mut self,
4075        source: Address,
4076        target: Address,
4077        missing_target: MissingTargetBehavior,
4078    ) -> Result<()> {
4079        // Read deployed bytecode from source (in CacheDB overlay after deploy_contract)
4080        let source_code = self
4081            .db
4082            .cache
4083            .accounts
4084            .get(&source)
4085            .and_then(|a| a.info.code.clone())
4086            .ok_or_else(|| anyhow!("No bytecode found at source address {}", source))?;
4087        Self::ensure_runtime_code(source, Some(&source_code), "source")?;
4088
4089        let code_hash = source_code.hash_slow();
4090        debug!(
4091            source = %source,
4092            target = %target,
4093            code_size = source_code.len(),
4094            "Overriding account bytecode"
4095        );
4096
4097        let mut target_info = self.target_account_info(target, missing_target)?;
4098
4099        if matches!(missing_target, MissingTargetBehavior::Error) {
4100            Self::ensure_runtime_code(target, target_info.code.as_ref(), "target")?;
4101        }
4102
4103        target_info.code = Some(source_code);
4104        target_info.code_hash = code_hash;
4105
4106        // Update CacheDB overlay (primary read path for EVM execution).
4107        self.db.insert_account_info(target, target_info.clone());
4108
4109        // Update BlockchainDb backend (shared with parallel tasks)
4110        {
4111            let mut accounts = self.blockchain_db.accounts().write();
4112            accounts.insert(target, target_info);
4113        }
4114
4115        // Layer 2 changed → invalidate the memoized base for `target`. The layer-1
4116        // `insert_account_info` above currently shadows it on every snapshot read,
4117        // but we dirty unconditionally for uniformity with every other layer-2 write
4118        // site (D2), so base correctness never relies on that shadowing invariant.
4119        self.mark_base_dirty(target);
4120
4121        Ok(())
4122    }
4123
4124    pub(crate) fn require_contract_target(&self, target: Address) -> Result<()> {
4125        let target_info = self.target_account_info(target, MissingTargetBehavior::Error)?;
4126        Self::ensure_runtime_code(target, target_info.code.as_ref(), "target")
4127    }
4128
4129    fn target_account_info(
4130        &self,
4131        target: Address,
4132        missing_target: MissingTargetBehavior,
4133    ) -> Result<AccountInfo> {
4134        if let Some(account) = self.db.cache.accounts.get(&target) {
4135            // A NotExisting overlay account is absent to the EVM (revm
4136            // `DbAccount::info()` returns None); treat it as a missing target
4137            // rather than returning its stale/default info.
4138            if !matches!(account.account_state, AccountState::NotExisting) {
4139                return Ok(account.info.clone());
4140            }
4141        }
4142
4143        match missing_target {
4144            MissingTargetBehavior::Create => Ok(AccountInfo::default()),
4145            MissingTargetBehavior::Error => {
4146                use revm::database_interface::DatabaseRef;
4147                self.backend
4148                    .basic_ref(target)
4149                    .map_err(|e| anyhow!("Failed to fetch target account {}: {:?}", target, e))?
4150                    .ok_or_else(|| {
4151                        anyhow!(
4152                            "Target account {} not found; use override_or_create_account_code for synthetic targets",
4153                            target
4154                        )
4155                    })
4156            }
4157        }
4158    }
4159
4160    fn ensure_runtime_code(address: Address, code: Option<&Bytecode>, role: &str) -> Result<()> {
4161        if code.is_some_and(|code| !code.is_empty()) {
4162            return Ok(());
4163        }
4164
4165        Err(anyhow!(
4166            "{} account {} has no runtime bytecode",
4167            role,
4168            address
4169        ))
4170    }
4171}
4172
4173/// Read-only state view for the event pipeline (Pillar B.2): a decoder reads the
4174/// current cached value of a slot through [`cached_storage_value`](EvmCache::cached_storage_value),
4175/// which never touches RPC and is `account_state`-aware (a cold slot reads
4176/// `None`).
4177impl crate::events::StateView for EvmCache {
4178    fn storage(&self, address: Address, slot: U256) -> Option<U256> {
4179        self.cached_storage_value(address, slot)
4180    }
4181}
4182
4183impl EvmCache {
4184    /// Create a LocalContext that reuses the shared memory buffer.
4185    ///
4186    /// The buffer is cleared (length set to 0) but capacity is preserved,
4187    /// avoiding repeated allocations across simulations.
4188    fn make_local_context(&self) -> LocalContext {
4189        // Clear the buffer but preserve capacity. `Vec::clear` sets the length
4190        // to 0 without releasing the allocation, so the buffer is reused across
4191        // simulations.
4192        self.shared_memory_buffer.borrow_mut().clear();
4193
4194        LocalContext {
4195            shared_memory_buffer: self.shared_memory_buffer.clone(),
4196            precompile_error_message: None,
4197        }
4198    }
4199
4200    fn build_evm(&mut self) -> CacheEvm<'_> {
4201        let local = self.make_local_context();
4202        let chain_id = self.chain_id;
4203        let mut evm = Context::mainnet()
4204            .with_db(&mut self.db)
4205            .with_local(local)
4206            .modify_cfg_chained(|cfg| {
4207                cfg.disable_nonce_check = true;
4208                cfg.disable_eip3607 = true;
4209                cfg.disable_base_fee = true;
4210                cfg.disable_balance_check = true;
4211                cfg.chain_id = chain_id;
4212                cfg.limit_contract_code_size = None;
4213                cfg.tx_chain_id_check = false;
4214                cfg.spec = self.spec_id;
4215            })
4216            .build_mainnet();
4217
4218        let timestamp = self
4219            .timestamp_override
4220            .unwrap_or_else(|| unix_timestamp_secs_saturating(SystemTime::now()));
4221        evm.block.timestamp = U256::from(timestamp);
4222        if let Some(number) = self.block_number {
4223            evm.block.number = U256::from(number);
4224        }
4225        if let Some(basefee) = self.basefee {
4226            evm.block.basefee = basefee;
4227        }
4228        if let Some(coinbase) = self.coinbase {
4229            evm.block.beneficiary = coinbase;
4230        }
4231        if let Some(prevrandao) = self.prevrandao {
4232            evm.block.prevrandao = Some(prevrandao);
4233        }
4234        if let Some(gas_limit) = self.block_gas_limit {
4235            evm.block.gas_limit = gas_limit;
4236        }
4237        evm
4238    }
4239
4240    fn build_evm_with_inspector<INSP>(&mut self, inspector: INSP) -> InspectorCacheEvm<'_, INSP> {
4241        let local = self.make_local_context();
4242        let chain_id = self.chain_id;
4243        let mut evm = Context::mainnet()
4244            .with_db(&mut self.db)
4245            .with_local(local)
4246            .modify_cfg_chained(|cfg| {
4247                cfg.disable_nonce_check = true;
4248                cfg.disable_eip3607 = true;
4249                cfg.disable_base_fee = true;
4250                cfg.disable_balance_check = true;
4251                cfg.chain_id = chain_id;
4252                cfg.limit_contract_code_size = None;
4253                cfg.tx_chain_id_check = false;
4254                cfg.spec = self.spec_id;
4255            })
4256            .build_mainnet_with_inspector(inspector);
4257
4258        let timestamp = self
4259            .timestamp_override
4260            .unwrap_or_else(|| unix_timestamp_secs_saturating(SystemTime::now()));
4261        evm.block.timestamp = U256::from(timestamp);
4262        if let Some(number) = self.block_number {
4263            evm.block.number = U256::from(number);
4264        }
4265        if let Some(basefee) = self.basefee {
4266            evm.block.basefee = basefee;
4267        }
4268        if let Some(coinbase) = self.coinbase {
4269            evm.block.beneficiary = coinbase;
4270        }
4271        if let Some(prevrandao) = self.prevrandao {
4272            evm.block.prevrandao = Some(prevrandao);
4273        }
4274        if let Some(gas_limit) = self.block_gas_limit {
4275            evm.block.gas_limit = gas_limit;
4276        }
4277        evm
4278    }
4279
4280    fn build_tx_env(from: Address, to: Address, calldata: Bytes) -> Result<TxEnv> {
4281        Self::build_tx_env_with(from, to, calldata, &TxConfig::default())
4282    }
4283
4284    fn build_tx_env_with(
4285        from: Address,
4286        to: Address,
4287        calldata: Bytes,
4288        tx: &TxConfig,
4289    ) -> Result<TxEnv> {
4290        let mut builder = TxEnv::builder()
4291            .caller(from)
4292            .kind(TxKind::Call(to))
4293            .data(calldata)
4294            .value(tx.value);
4295        if let Some(gas_limit) = tx.gas_limit {
4296            builder = builder.gas_limit(gas_limit);
4297        }
4298        if let Some(gas_price) = tx.gas_price {
4299            builder = builder.gas_price(gas_price);
4300        }
4301        if let Some(nonce) = tx.nonce {
4302            builder = builder.nonce(nonce);
4303        }
4304        if let Some(access_list) = &tx.access_list {
4305            builder = builder.access_list(access_list.clone());
4306        }
4307        builder
4308            .build()
4309            .map_err(|e| anyhow!("Failed to build tx env: {:?}", e))
4310    }
4311
4312    fn call_sol_with_commit<C>(
4313        &mut self,
4314        from: Address,
4315        to: Address,
4316        call: C,
4317        tx: &TxConfig,
4318        commit: bool,
4319    ) -> Result<C::Return>
4320    where
4321        C: SolCall,
4322    {
4323        let calldata = Bytes::from(call.abi_encode());
4324        let result = self
4325            .call_raw_with(from, to, calldata, commit, tx)
4326            .with_context(|| {
4327                format!(
4328                    "failed to execute Solidity call {} from {from:?} to {to:?}",
4329                    C::SIGNATURE
4330                )
4331            })?;
4332        Self::decode_sol_call_result::<C>(from, to, result)
4333    }
4334
4335    fn decode_sol_call_result<C>(
4336        from: Address,
4337        to: Address,
4338        result: ExecutionResult,
4339    ) -> Result<C::Return>
4340    where
4341        C: SolCall,
4342    {
4343        match result {
4344            ExecutionResult::Success { output, .. } => {
4345                let output = output.into_data();
4346                C::abi_decode_returns(&output).map_err(|error| {
4347                    anyhow!(
4348                        "failed to decode Solidity call {} return data from {from:?} to {to:?}: output_len={}, error: {:?}",
4349                        C::SIGNATURE,
4350                        output.len(),
4351                        error
4352                    )
4353                })
4354            }
4355            other => Err(anyhow!(
4356                "Solidity call {} from {from:?} to {to:?} did not succeed: {:?}",
4357                C::SIGNATURE,
4358                other
4359            )),
4360        }
4361    }
4362
4363    fn erc20_balance_of_in_evm(
4364        evm: &mut CacheEvm<'_>,
4365        caller: Address,
4366        token: Address,
4367        owner: Address,
4368    ) -> Result<U256> {
4369        let call = IERC20::balanceOfCall { target: owner };
4370        let tx = Self::build_tx_env(caller, token, Bytes::from(call.abi_encode()))?;
4371        let result = evm
4372            .transact_one(tx)
4373            .map_err(|e| anyhow!("Failed to transact: {:?}", e))?;
4374
4375        match result {
4376            ExecutionResult::Success { output, .. } => {
4377                let out = output.into_data();
4378                let balance = IERC20::balanceOfCall::abi_decode_returns(&out)
4379                    .map_err(|e| anyhow!("Failed to decode balanceOf: {:?}", e))?;
4380                Ok(balance)
4381            }
4382            _ => Err(anyhow!("balanceOf call failed: {:?}", result)),
4383        }
4384    }
4385
4386    fn erc20_balance_of_in_evm_isolated(
4387        evm: &mut CacheEvm<'_>,
4388        caller: Address,
4389        token: Address,
4390        owner: Address,
4391    ) -> Result<(U256, AccessList)> {
4392        let state_before = evm.journaled_state.state.clone();
4393        let checkpoint = evm.journaled_state.checkpoint();
4394        let result = Self::erc20_balance_of_in_evm(evm, caller, token, owner);
4395        let access_list = extract_access_list(&evm.journaled_state.state);
4396        evm.journaled_state.checkpoint_revert(checkpoint);
4397        evm.journaled_state.state = state_before;
4398        result.map(|balance| (balance, access_list))
4399    }
4400
4401    fn seed_synthetic_beneficiary(evm: &mut CacheEvm<'_>) -> Option<Address> {
4402        let beneficiary = evm.block.beneficiary;
4403        if evm.journaled_state.state.contains_key(&beneficiary) {
4404            return None;
4405        }
4406        evm.journaled_state
4407            .state
4408            .insert(beneficiary, Account::from(AccountInfo::default()));
4409        Some(beneficiary)
4410    }
4411
4412    fn remove_synthetic_beneficiary(evm: &mut CacheEvm<'_>, beneficiary: Option<Address>) {
4413        if let Some(beneficiary) = beneficiary {
4414            evm.journaled_state.state.remove(&beneficiary);
4415        }
4416    }
4417}
4418
4419/// A session for executing multiple EVM operations without committing to the underlying DB.
4420///
4421/// Changes made within a session are tracked in the EVM's journaled state. Call `commit()` to
4422/// persist changes to the underlying database, or simply drop the session to discard
4423/// all changes.
4424///
4425/// Note: For snapshot/restore functionality across multiple transactions, use `EvmCache::snapshot()`
4426/// and `EvmCache::restore()` instead, as the EVM journal is cleared after each transaction.
4427pub struct EvmSession<'a> {
4428    evm: CacheEvm<'a>,
4429}
4430
4431impl<'a> EvmSession<'a> {
4432    /// Execute a call within the session.
4433    ///
4434    /// If `commit` is true, changes are persisted to the session's journaled state.
4435    /// If `commit` is false, the call is executed but its effects are immediately reverted.
4436    ///
4437    /// Note: Changes are not persisted to the underlying CacheDB until `commit()` is called
4438    /// on the session itself.
4439    pub fn call_raw(
4440        &mut self,
4441        from: Address,
4442        to: Address,
4443        calldata: Bytes,
4444        commit: bool,
4445    ) -> Result<ExecutionResult> {
4446        let tx = EvmCache::build_tx_env(from, to, calldata)?;
4447
4448        if commit {
4449            self.evm
4450                .transact_one(tx)
4451                .map_err(|e| anyhow!("Failed to transact: {:?}", e))
4452        } else {
4453            let checkpoint = self.evm.journaled_state.checkpoint();
4454            let result = self.evm.transact_one(tx);
4455            self.evm.journaled_state.checkpoint_revert(checkpoint);
4456            result.map_err(|e| anyhow!("Failed to transact: {:?}", e))
4457        }
4458    }
4459
4460    /// Commit all session changes to the underlying database.
4461    ///
4462    /// This persists all changes made during the session to the CacheDB.
4463    pub fn commit(mut self) {
4464        self.evm.commit_inner();
4465    }
4466
4467    /// Get access to the underlying EVM for advanced operations.
4468    ///
4469    /// This exposes revm internals and bypasses the cache's two-layer
4470    /// consistency model: state mutated directly through the journaled EVM
4471    /// lands in the session's journal, not the BlockchainDb backend, and is
4472    /// only flushed to the CacheDB overlay on [`commit`](Self::commit). Use
4473    /// with care.
4474    pub fn evm(&mut self) -> &mut CacheEvm<'a> {
4475        &mut self.evm
4476    }
4477}
4478
4479/// Automatically flush the cache to disk when the EvmCache is dropped.
4480impl Drop for EvmCache {
4481    fn drop(&mut self) {
4482        if self.cache_config.is_some() {
4483            debug!("Flushing EVM cache on drop");
4484            if let Err(e) = self.flush() {
4485                warn!(error = %e, "Failed to flush EVM cache on drop");
4486            }
4487        }
4488    }
4489}
4490
4491#[cfg(test)]
4492mod shared_memory_capacity_tests {
4493    use super::SharedMemoryCapacity as Cap;
4494
4495    #[test]
4496    fn default_is_fixed_64k() {
4497        assert_eq!(Cap::default(), Cap::Fixed(64 * 1024));
4498    }
4499
4500    #[test]
4501    fn fixed_ignores_loaded_slots() {
4502        assert_eq!(Cap::Fixed(8_192).resolve(10_000_000), 8_192);
4503        assert_eq!(Cap::Fixed(0).resolve(123), 0);
4504    }
4505
4506    #[test]
4507    fn auto_floors_clamps_and_scales() {
4508        // Nothing / little loaded → floor.
4509        assert_eq!(Cap::Auto.resolve(0), Cap::MIN_AUTO);
4510        assert_eq!(Cap::Auto.resolve(1_000), Cap::MIN_AUTO); // 16 KiB < 64 KiB floor
4511        // Linear region (16 bytes/slot).
4512        assert_eq!(Cap::Auto.resolve(10_000), 160_000);
4513        assert_eq!(Cap::Auto.resolve(100_000), 1_600_000);
4514        // Ceiling.
4515        assert_eq!(Cap::Auto.resolve(usize::MAX), Cap::MAX_AUTO);
4516        assert_eq!(Cap::Auto.resolve(262_144), Cap::MAX_AUTO); // 262_144 * 16 == 4 MiB
4517    }
4518}
4519
4520/// Tests that exercise the generic cache engine.
4521#[cfg(test)]
4522mod core_tests {
4523    use super::*;
4524
4525    #[test]
4526    fn test_address_to_u256_conversion() {
4527        // Test that address conversion preserves the address bytes correctly
4528        let addr = Address::repeat_byte(0xAB);
4529        let value = U256::from_be_slice(addr.as_slice());
4530
4531        // Address is 20 bytes, should be right-aligned in U256 (32 bytes)
4532        let bytes = value.to_be_bytes::<32>();
4533
4534        // First 12 bytes should be zero (padding)
4535        assert_eq!(&bytes[..12], &[0u8; 12]);
4536
4537        // Last 20 bytes should be the address
4538        assert_eq!(&bytes[12..], addr.as_slice());
4539    }
4540
4541    // ==================== block context tests ====================
4542
4543    #[test]
4544    fn new_defaults_to_latest_block_pin() {
4545        use alloy_provider::RootProvider;
4546        use alloy_rpc_client::RpcClient;
4547        use alloy_transport::mock::Asserter;
4548
4549        let asserter = Asserter::new();
4550        let client = RpcClient::mocked(asserter);
4551        let provider = RootProvider::<AnyNetwork>::new(client);
4552
4553        let rt = tokio::runtime::Builder::new_current_thread()
4554            .enable_all()
4555            .build()
4556            .unwrap();
4557
4558        let cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4559
4560        assert_eq!(
4561            cache.block(),
4562            BlockId::latest(),
4563            "a default cache must carry an explicit latest block pin, not None"
4564        );
4565    }
4566
4567    #[test]
4568    fn test_set_block_context_stores_values() {
4569        use alloy_provider::RootProvider;
4570        use alloy_rpc_client::RpcClient;
4571        use alloy_transport::mock::Asserter;
4572
4573        let asserter = Asserter::new();
4574        let client = RpcClient::mocked(asserter);
4575        let provider = RootProvider::<AnyNetwork>::new(client);
4576
4577        let rt = tokio::runtime::Builder::new_current_thread()
4578            .enable_all()
4579            .build()
4580            .unwrap();
4581
4582        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4583
4584        // Initially None
4585        assert_eq!(cache.block_number(), None);
4586        assert_eq!(cache.basefee(), None);
4587
4588        // Set values
4589        cache.set_block_context(Some(148_252_680), Some(50));
4590        assert_eq!(cache.block_number(), Some(148_252_680));
4591        assert_eq!(cache.basefee(), Some(50));
4592
4593        // Clear values
4594        cache.set_block_context(None, None);
4595        assert_eq!(cache.block_number(), None);
4596        assert_eq!(cache.basefee(), None);
4597    }
4598
4599    #[test]
4600    fn set_block_latest_clears_stale_block_context() {
4601        use alloy_provider::RootProvider;
4602        use alloy_rpc_client::RpcClient;
4603        use alloy_transport::mock::Asserter;
4604
4605        let asserter = Asserter::new();
4606        let client = RpcClient::mocked(asserter);
4607        let provider = RootProvider::<AnyNetwork>::new(client);
4608
4609        let rt = tokio::runtime::Builder::new_current_thread()
4610            .enable_all()
4611            .build()
4612            .unwrap();
4613
4614        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4615        cache.set_block_context(Some(148_252_680), Some(50));
4616
4617        cache.set_block(BlockId::latest());
4618
4619        assert_eq!(
4620            cache.block_number(),
4621            None,
4622            "tag pins must not retain a stale NUMBER context"
4623        );
4624        assert_eq!(
4625            cache.basefee(),
4626            None,
4627            "set_block cannot refresh BASEFEE synchronously, so it must clear stale values"
4628        );
4629    }
4630
4631    #[test]
4632    fn set_block_latest_clears_stale_context_even_when_pin_unchanged() {
4633        use alloy_provider::RootProvider;
4634        use alloy_rpc_client::RpcClient;
4635        use alloy_transport::mock::Asserter;
4636
4637        let asserter = Asserter::new();
4638        let client = RpcClient::mocked(asserter);
4639        let provider = RootProvider::<AnyNetwork>::new(client);
4640
4641        let rt = tokio::runtime::Builder::new_current_thread()
4642            .enable_all()
4643            .build()
4644            .unwrap();
4645
4646        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4647        cache.set_block_context(Some(148_252_680), Some(50));
4648
4649        cache.set_block(BlockId::latest());
4650
4651        assert_eq!(
4652            cache.block_number(),
4653            None,
4654            "latest pins must not retain a stale NUMBER context"
4655        );
4656        assert_eq!(
4657            cache.basefee(),
4658            None,
4659            "latest pins can drift like tags, so stale BASEFEE must be cleared"
4660        );
4661    }
4662
4663    #[test]
4664    fn set_block_number_sets_number_and_clears_stale_basefee() {
4665        use alloy_provider::RootProvider;
4666        use alloy_rpc_client::RpcClient;
4667        use alloy_transport::mock::Asserter;
4668
4669        let asserter = Asserter::new();
4670        let client = RpcClient::mocked(asserter);
4671        let provider = RootProvider::<AnyNetwork>::new(client);
4672
4673        let rt = tokio::runtime::Builder::new_current_thread()
4674            .enable_all()
4675            .build()
4676            .unwrap();
4677
4678        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4679        cache.set_block_context(Some(100), Some(50));
4680
4681        cache.set_block(BlockId::Number(BlockNumberOrTag::Number(200)));
4682
4683        assert_eq!(cache.block_number(), Some(200));
4684        assert_eq!(
4685            cache.basefee(),
4686            None,
4687            "set_block cannot refresh BASEFEE synchronously, so it must clear stale values"
4688        );
4689    }
4690
4691    #[test]
4692    fn repin_to_block_clears_stale_basefee() {
4693        use alloy_provider::RootProvider;
4694        use alloy_rpc_client::RpcClient;
4695        use alloy_transport::mock::Asserter;
4696
4697        let asserter = Asserter::new();
4698        let client = RpcClient::mocked(asserter);
4699        let provider = RootProvider::<AnyNetwork>::new(client);
4700
4701        let rt = tokio::runtime::Builder::new_current_thread()
4702            .enable_all()
4703            .build()
4704            .unwrap();
4705
4706        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4707        cache.set_block_context(Some(100), Some(50));
4708
4709        cache.repin_to_block(200);
4710
4711        assert_eq!(cache.block_number(), Some(200));
4712        assert_eq!(
4713            cache.basefee(),
4714            None,
4715            "repin_to_block must not carry stale BASEFEE across blocks"
4716        );
4717    }
4718
4719    #[test]
4720    fn test_build_evm_applies_block_context() {
4721        use alloy_provider::RootProvider;
4722        use alloy_rpc_client::RpcClient;
4723        use alloy_transport::mock::Asserter;
4724
4725        let asserter = Asserter::new();
4726        let client = RpcClient::mocked(asserter);
4727        let provider = RootProvider::<AnyNetwork>::new(client);
4728
4729        let rt = tokio::runtime::Builder::new_current_thread()
4730            .enable_all()
4731            .build()
4732            .unwrap();
4733
4734        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4735
4736        let block_num = 148_252_680u64;
4737        let basefee_val = 50u64;
4738        let coinbase = Address::repeat_byte(0xC0);
4739        let prevrandao = B256::repeat_byte(0x77);
4740        let gas_limit = 30_000_000u64;
4741        cache.set_block_context(Some(block_num), Some(basefee_val));
4742        cache.set_coinbase(Some(coinbase));
4743        cache.set_prevrandao(Some(prevrandao));
4744        cache.set_block_gas_limit(Some(gas_limit));
4745
4746        let evm = cache.build_evm();
4747        assert_eq!(evm.block.number, U256::from(block_num));
4748        assert_eq!(evm.block.basefee, basefee_val);
4749        assert_eq!(evm.block.beneficiary, coinbase);
4750        assert_eq!(evm.block.prevrandao, Some(prevrandao));
4751        assert_eq!(evm.block.gas_limit, gas_limit);
4752    }
4753
4754    #[test]
4755    fn test_from_backend_propagates_block_context() {
4756        use alloy_provider::RootProvider;
4757        use alloy_rpc_client::RpcClient;
4758        use alloy_transport::mock::Asserter;
4759
4760        let asserter = Asserter::new();
4761        let client = RpcClient::mocked(asserter);
4762        let provider = RootProvider::<AnyNetwork>::new(client);
4763
4764        let rt = tokio::runtime::Builder::new_current_thread()
4765            .enable_all()
4766            .build()
4767            .unwrap();
4768
4769        let parent = rt.block_on(EvmCache::new(Arc::new(provider)));
4770
4771        let block_num = Some(148_252_680u64);
4772        let basefee_val = Some(50u64);
4773        let child = EvmCache::from_backend(
4774            parent.unchecked_backend().clone(),
4775            parent.unchecked_blockchain_db().clone(),
4776            parent.block(),
4777            42161,
4778            block_num,
4779            basefee_val,
4780            SpecId::CANCUN,
4781        );
4782
4783        assert_eq!(child.block_number(), block_num);
4784        assert_eq!(child.basefee(), basefee_val);
4785    }
4786
4787    #[test]
4788    fn unix_timestamp_secs_saturating_handles_pre_epoch() {
4789        let before_epoch = std::time::UNIX_EPOCH - std::time::Duration::from_secs(5);
4790        assert_eq!(
4791            unix_timestamp_secs_saturating(before_epoch),
4792            0,
4793            "pre-epoch system times must saturate instead of panicking"
4794        );
4795    }
4796}