Skip to main content

evm_fork_cache/cache/
mod.rs

1//! The forked-EVM state cache: lazy RPC loading, a layered write funnel, and
2//! cheap copy-on-write snapshots.
3//!
4//! [`EvmCache`] is the core handle. It fronts a [`foundry_fork_db`]-backed fork
5//! database with a hot [`revm`] cache layer, lazily fetching account and storage
6//! state from a provider on the first miss and serving it locally thereafter.
7//! Targeted writes and purges ([`StateUpdate`], balance/code overrides, verified
8//! code seeds) flow through a single write
9//! funnel — never the RPC path — so event-driven state maintenance never round-trips.
10//! [`EvmCache::snapshot`] produces an immutable, `Arc`-shared, cross-thread
11//! [`EvmSnapshot`] (see [`snapshot`]) for parallel fan-out; [`overlay`]
12//! layers per-simulation state on top. See the crate-root docs for the full
13//! state stack and [`docs/INTERNALS.md`](https://github.com/KaiCode2/evm-fork-cache/blob/main/docs/INTERNALS.md)
14//! for the snapshot cost model.
15
16mod binary_state;
17mod bytecode;
18mod code_seeds;
19mod journal_access_list;
20mod metadata;
21pub mod overlay;
22pub mod slot_observations;
23pub mod snapshot;
24pub(crate) mod versioned;
25
26pub use binary_state::{load_binary_state, save_binary_state};
27pub use metadata::{CacheConfig, ImmutableDataCache};
28pub use overlay::EvmOverlay;
29pub use slot_observations::SlotObservationTracker;
30pub use snapshot::EvmSnapshot;
31
32use std::{
33    cell::RefCell,
34    collections::{HashMap, HashSet},
35    fs,
36    rc::Rc,
37    sync::Arc,
38    time::{SystemTime, UNIX_EPOCH},
39};
40
41use alloy_consensus::BlockHeader;
42use alloy_eips::eip2930::AccessList;
43use alloy_eips::{BlockId, BlockNumberOrTag};
44use alloy_network::BlockResponse;
45use alloy_primitives::{Address, B256, Bytes, I256, Log, TxKind, U256, keccak256};
46use alloy_provider::{Provider, network::AnyNetwork};
47use alloy_rpc_types_eth::TransactionRequest;
48use alloy_sol_types::{SolCall, SolValue, sol};
49use foundry_fork_db::{BlockchainDb, SharedBackend, cache::BlockchainDbMeta};
50use revm::{
51    Context, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainBuilder, MainContext,
52    context::{BlockEnv, CfgEnv, Journal, LocalContext, TxEnv, result::ExecutionResult},
53    context_interface::JournalTr,
54    database::{AccountState, CacheDB},
55    primitives::hardfork::SpecId,
56    state::{Account, AccountInfo, Bytecode},
57};
58use tracing::{debug, instrument, trace, warn};
59
60use crate::access_set::StorageAccessList;
61use crate::bulk_storage::AccountFieldsSample;
62use crate::errors::{
63    BlockContextError, CacheError, CacheResult as Result, RpcError, RuntimeError, SimError,
64    SimHostError, SimulationError, SimulationResult, StorageFetchError, StorageFetchResult,
65};
66use crate::freshness::{SlotChange, SlotFetch, SlotOutcome};
67use crate::inspector::TransferInspector;
68use crate::mapping_probe::{
69    HashSlotAccess, HashStorageProbe, SlotLayout, TrackedBalances, TrackedMapping,
70};
71use crate::state_update::{
72    AccountChange, AccountPatch, PurgeRecord, PurgeScope, SkippedAccountPatch, SkippedBalanceDelta,
73    SkippedDelta, SkippedMask, SlotDelta, StateDiff, StateUpdate,
74};
75
76use bytecode::BytecodeCache;
77use code_seeds::CodeSeedCache;
78pub use code_seeds::CodeSeedState;
79use journal_access_list::{extract_access_list, merge_access_lists};
80
81/// Re-export AnyNetwork for callers that need to construct providers.
82pub use alloy_provider::network::AnyNetwork as AnyNetworkType;
83
84/// The database type used by the EVM cache.
85/// CacheDB wraps SharedBackend which lazily fetches data from RPC on-demand.
86pub type ForkCacheDB = CacheDB<SharedBackend>;
87
88/// Callback for making direct RPC `eth_call` requests, bypassing revm simulation.
89/// Used when batch-querying many contracts where revm's lazy storage fetching would
90/// be prohibitively slow (e.g. querying 500+ gauge contracts).
91pub type RpcCallFn = Arc<dyn Fn(Address, Bytes) -> Result<Bytes, RpcError> + Send + Sync>;
92
93/// Callback for batch-fetching storage slots directly from RPC, bypassing SharedBackend.
94///
95/// Used by callers that need bulk storage reads without many individual channel
96/// round-trips through SharedBackend. Fires concurrent `eth_getStorageAt` calls
97/// directly via the provider and returns results for bulk injection into
98/// BlockchainDb.
99/// Users may replace the provider-backed implementation with their own fetcher via
100/// [`EvmCache::set_storage_batch_fetcher`].
101///
102/// The second argument pins the fetch to a specific block. Callers pass the
103/// cache's pinned block at the point they schedule the fetch; deferred callers
104/// such as the freshness validator pass the block their snapshot was built from,
105/// so a concurrent [`EvmCache::set_block`] cannot make the deferred fetch read a
106/// *different* block than the snapshot it is compared against.
107///
108/// **Contract:** an implementation must return **exactly one** result tuple per
109/// requested `(address, slot)` (order does not matter). Callers — `verify_slots`,
110/// `reconcile_slots`, and the cold-start verify/probe paths — derive their
111/// per-slot outcomes from the returned tuples, so a fetcher that drops, dedups,
112/// reorders-and-truncates, or duplicates entries breaks the "one outcome per
113/// requested slot" guarantee those APIs document.
114pub type StorageBatchFetchFn = Arc<
115    dyn Fn(Vec<(Address, U256)>, BlockId) -> Vec<(Address, U256, StorageFetchResult<U256>)>
116        + Send
117        + Sync,
118>;
119
120/// Account header + optional storage-proof slots from `eth_getProof`.
121/// `slots` is populated only for requested storage keys; an empty key list is a
122/// root-only probe (account fields + `storage_hash`, no slot payload).
123#[derive(Clone, Debug)]
124pub struct AccountProof {
125    /// Merkle root of the account's storage trie (`storageHash`).
126    pub storage_hash: B256,
127    /// Account balance.
128    pub balance: U256,
129    /// Account nonce.
130    pub nonce: u64,
131    /// Hash of the account's runtime code (`codeHash`).
132    pub code_hash: B256,
133    /// Proven `(slot, value)` pairs for the requested storage keys. Empty for a
134    /// root-only probe.
135    pub slots: Vec<(U256, U256)>,
136}
137
138/// Callback for fetching account headers (and optional storage-proof slots)
139/// directly from RPC via `eth_getProof`, mirroring [`StorageBatchFetchFn`].
140///
141/// Used by callers that need authoritative account fields (balance/nonce/code
142/// hash) plus the account's `storageHash`, e.g. account-target resyncs and
143/// account-level freshness. Each request is a `(address, keys)` pair; an empty
144/// `keys` list is a root-only probe.
145///
146/// The second argument pins the fetch to a specific block, matching
147/// [`StorageBatchFetchFn`]'s block semantics.
148///
149/// **Contract:** an implementation returns at most one result per requested
150/// address. An address present with `Ok(..)` succeeded; present with `Err(..)`
151/// failed; omitted entirely means the fetcher produced no result for it. Callers
152/// derive their per-address outcome from whether the address appears and, if so,
153/// whether it is `Ok`/`Err`.
154pub type AccountProofFetchFn = Arc<
155    dyn Fn(Vec<(Address, Vec<U256>)>, BlockId) -> Vec<(Address, StorageFetchResult<AccountProof>)>
156        + Send
157        + Sync,
158>;
159
160/// Callback fetching `(balance, EXTCODEHASH)` samples for many addresses at a
161/// pinned block — one bulk `eth_call` by default (the
162/// [`ACCOUNT_FIELDS_EXTRACTOR_CODE`](crate::bulk_storage::ACCOUNT_FIELDS_EXTRACTOR_CODE)
163/// program). Sync and type-erased with the same bridging rules as
164/// [`StorageBatchFetchFn`] (multi-thread tokio runtime required for the
165/// default provider-backed implementation).
166///
167/// **Contract:** the call is all-or-nothing — `Ok` carries one sample per
168/// requested address (an omitted address is treated by callers as
169/// unverifiable), `Err` means the whole fetch failed and nothing can be
170/// concluded about any address. Used by
171/// [`EvmCache::verify_code_seeds`](EvmCache::verify_code_seeds) and the
172/// cold-start `verify_code` phase.
173pub type AccountFieldsFetchFn = Arc<
174    dyn Fn(Vec<Address>, BlockId) -> StorageFetchResult<Vec<(Address, AccountFieldsSample)>>
175        + Send
176        + Sync,
177>;
178
179/// Final state changes observed from a block-level state-diff trace.
180#[derive(Clone, Debug, Default, PartialEq, Eq)]
181pub struct BlockStateDiff {
182    /// Accounts changed by the traced block.
183    pub accounts: Vec<BlockStateAccountDiff>,
184}
185
186/// Final account/storage values observed for one account in a block trace.
187#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct BlockStateAccountDiff {
189    /// Changed account address.
190    pub address: Address,
191    /// Final balance when the trace reports a balance change.
192    pub balance: Option<U256>,
193    /// Final nonce when the trace reports a nonce change.
194    pub nonce: Option<u64>,
195    /// Final runtime bytecode when the trace reports a code change.
196    pub code: Option<Bytes>,
197    /// Final storage-slot values reported for this account.
198    pub storage: Vec<BlockStateStorageDiff>,
199}
200
201/// Final value for one storage slot observed from a block trace.
202#[derive(Clone, Debug, PartialEq, Eq)]
203pub struct BlockStateStorageDiff {
204    /// Storage slot key.
205    pub slot: U256,
206    /// Final slot value after the block. Cleared slots are represented as zero.
207    pub value: U256,
208}
209
210/// Callback for fetching one block's state diff through debug/trace RPC.
211///
212/// The callback returns final post-block values for accounts/storage slots that
213/// changed in the block. Callers may resolve matching resync targets from this
214/// diff before falling back to point reads.
215pub type BlockStateDiffFetchFn =
216    Arc<dyn Fn(BlockId) -> StorageFetchResult<BlockStateDiff> + Send + Sync>;
217
218/// Return a tokio runtime [`Handle`] suitable for `block_in_place` + `block_on`,
219/// or an error describing why one is unavailable.
220///
221/// The RPC-backed callbacks ([`RpcCallFn`], [`StorageBatchFetchFn`]) drive async
222/// work synchronously via `tokio::task::block_in_place`. That helper panics on a
223/// current-thread runtime, and `Handle::current()` panics when no runtime is
224/// present. To avoid panicking deep inside a callback, callers use this guard to
225/// degrade to a typed error instead.
226///
227/// Requires a **multi-thread** tokio runtime.
228pub(crate) fn block_in_place_handle() -> Result<tokio::runtime::Handle, RuntimeError> {
229    match tokio::runtime::Handle::try_current() {
230        Ok(handle) => match handle.runtime_flavor() {
231            tokio::runtime::RuntimeFlavor::CurrentThread => Err(RuntimeError::CurrentThreadRuntime),
232            _ => Ok(handle),
233        },
234        Err(e) => Err(RuntimeError::MissingRuntime {
235            details: e.to_string(),
236        }),
237    }
238}
239
240fn trace_rpc_method_and_params(block: BlockId) -> (&'static str, serde_json::Value) {
241    let tracer = serde_json::json!({
242        "tracer": "prestateTracer",
243        "tracerConfig": {
244            "diffMode": true,
245        },
246    });
247    match block {
248        BlockId::Hash(hash) => (
249            "debug_traceBlockByHash",
250            serde_json::json!([hash.block_hash, tracer]),
251        ),
252        BlockId::Number(number) => (
253            "debug_traceBlockByNumber",
254            serde_json::json!([block_number_or_tag_param(number), tracer]),
255        ),
256    }
257}
258
259fn block_number_or_tag_param(number: BlockNumberOrTag) -> serde_json::Value {
260    match number {
261        BlockNumberOrTag::Number(number) => serde_json::json!(format!("{number:#x}")),
262        BlockNumberOrTag::Latest => serde_json::json!("latest"),
263        BlockNumberOrTag::Finalized => serde_json::json!("finalized"),
264        BlockNumberOrTag::Safe => serde_json::json!("safe"),
265        BlockNumberOrTag::Earliest => serde_json::json!("earliest"),
266        BlockNumberOrTag::Pending => serde_json::json!("pending"),
267    }
268}
269
270fn parse_block_state_diff_trace(value: &serde_json::Value) -> Result<BlockStateDiff> {
271    let mut accounts: HashMap<Address, BlockStateAccountDiff> = HashMap::new();
272    match value {
273        serde_json::Value::Array(traces) => {
274            for trace in traces {
275                merge_trace_diff(trace, &mut accounts)?;
276            }
277        }
278        trace => merge_trace_diff(trace, &mut accounts)?,
279    }
280
281    let mut accounts: Vec<_> = accounts.into_values().collect();
282    accounts.sort_by_key(|account| account.address);
283    for account in &mut accounts {
284        account.storage.sort_by_key(|slot| slot.slot);
285    }
286    Ok(BlockStateDiff { accounts })
287}
288
289fn merge_trace_diff(
290    trace: &serde_json::Value,
291    accounts: &mut HashMap<Address, BlockStateAccountDiff>,
292) -> Result<()> {
293    let diff = trace.get("result").unwrap_or(trace);
294    let Some(pre) = diff.get("pre").and_then(serde_json::Value::as_object) else {
295        return Ok(());
296    };
297    let Some(post) = diff.get("post").and_then(serde_json::Value::as_object) else {
298        return Ok(());
299    };
300
301    for (address, post_account) in post {
302        let address = parse_trace_address(address)?;
303        let entry = accounts
304            .entry(address)
305            .or_insert_with(|| empty_block_state_account_diff(address));
306
307        if let Some(balance) = post_account.get("balance") {
308            entry.balance = Some(parse_trace_u256(balance)?);
309        }
310        if let Some(nonce) = post_account.get("nonce") {
311            entry.nonce = Some(parse_trace_u64(nonce)?);
312        }
313        if let Some(code) = post_account.get("code") {
314            entry.code = Some(parse_trace_bytes(code)?);
315        }
316        if let Some(storage) = post_account
317            .get("storage")
318            .and_then(serde_json::Value::as_object)
319        {
320            for (slot, value) in storage {
321                upsert_block_state_storage_diff(
322                    entry,
323                    parse_trace_u256_str(slot)?,
324                    parse_trace_u256(value)?,
325                );
326            }
327        }
328    }
329
330    // In diff mode, state that ends the block *absent* appears in `pre` but is
331    // omitted from `post`. Convert those omissions into explicit final values:
332    for (address_key, pre_account) in pre {
333        let address = parse_trace_address(address_key)?;
334        let post_account = post.get(address_key);
335
336        // An account entirely absent from `post` was deleted by the block
337        // (SELFDESTRUCT; post-Cancun, a same-tx create+destruct). Synthesize
338        // the explicit post-deletion fields so account-target resyncs resolve
339        // authoritatively from the trace instead of falling back to point
340        // reads. A later transaction in the same block re-creating the account
341        // overwrites these in its own merge pass (entries merge in tx order).
342        if post_account.is_none() {
343            let entry = accounts
344                .entry(address)
345                .or_insert_with(|| empty_block_state_account_diff(address));
346            entry.balance = Some(U256::ZERO);
347            entry.nonce = Some(0);
348            entry.code = Some(Bytes::new());
349        }
350
351        // Storage cleared to zero: present in `pre`, omitted from `post`.
352        let Some(pre_storage) = pre_account
353            .get("storage")
354            .and_then(serde_json::Value::as_object)
355        else {
356            continue;
357        };
358        let post_storage = post_account
359            .and_then(|account| account.get("storage"))
360            .and_then(serde_json::Value::as_object);
361        for slot in pre_storage.keys() {
362            let cleared = post_storage.is_none_or(|storage| !storage.contains_key(slot));
363            if cleared {
364                let entry = accounts
365                    .entry(address)
366                    .or_insert_with(|| empty_block_state_account_diff(address));
367                upsert_block_state_storage_diff(entry, parse_trace_u256_str(slot)?, U256::ZERO);
368            }
369        }
370    }
371
372    Ok(())
373}
374
375fn empty_block_state_account_diff(address: Address) -> BlockStateAccountDiff {
376    BlockStateAccountDiff {
377        address,
378        balance: None,
379        nonce: None,
380        code: None,
381        storage: Vec::new(),
382    }
383}
384
385fn upsert_block_state_storage_diff(account: &mut BlockStateAccountDiff, slot: U256, value: U256) {
386    if let Some(existing) = account.storage.iter_mut().find(|entry| entry.slot == slot) {
387        existing.value = value;
388    } else {
389        account.storage.push(BlockStateStorageDiff { slot, value });
390    }
391}
392
393fn parse_trace_address(value: &str) -> Result<Address> {
394    value.parse().map_err(|err| CacheError::TraceParse {
395        details: format!("invalid address `{value}`: {err}"),
396    })
397}
398
399fn parse_trace_u256(value: &serde_json::Value) -> Result<U256> {
400    match value {
401        serde_json::Value::String(value) => parse_trace_u256_str(value),
402        serde_json::Value::Number(value) => parse_trace_u256_str(&value.to_string()),
403        other => Err(CacheError::TraceParse {
404            details: format!("expected U256 string/number, got {other:?}"),
405        }),
406    }
407}
408
409fn parse_trace_u256_str(value: &str) -> Result<U256> {
410    if let Some(value) = value.strip_prefix("0x") {
411        if value.is_empty() {
412            return Ok(U256::ZERO);
413        }
414        return U256::from_str_radix(value, 16).map_err(|err| CacheError::TraceParse {
415            details: format!("invalid U256 `0x{value}`: {err}"),
416        });
417    }
418    if value.is_empty() {
419        return Ok(U256::ZERO);
420    }
421    U256::from_str_radix(value, 10).map_err(|err| CacheError::TraceParse {
422        details: format!("invalid U256 `{value}`: {err}"),
423    })
424}
425
426fn parse_trace_u64(value: &serde_json::Value) -> Result<u64> {
427    match value {
428        serde_json::Value::Number(value) => value.as_u64().ok_or_else(|| CacheError::TraceParse {
429            details: format!("invalid u64 number `{value}`"),
430        }),
431        serde_json::Value::String(value) => {
432            if let Some(value) = value.strip_prefix("0x") {
433                if value.is_empty() {
434                    return Ok(0);
435                }
436                return u64::from_str_radix(value, 16).map_err(|err| CacheError::TraceParse {
437                    details: format!("invalid u64 `0x{value}`: {err}"),
438                });
439            }
440            if value.is_empty() {
441                return Ok(0);
442            }
443            value.parse().map_err(|err| CacheError::TraceParse {
444                details: format!("invalid u64 `{value}`: {err}"),
445            })
446        }
447        other => Err(CacheError::TraceParse {
448            details: format!("expected u64 string/number, got {other:?}"),
449        }),
450    }
451}
452
453fn parse_trace_bytes(value: &serde_json::Value) -> Result<Bytes> {
454    let Some(value) = value.as_str() else {
455        return Err(CacheError::TraceParse {
456            details: format!("expected bytecode string, got {value:?}"),
457        });
458    };
459    let value = value.strip_prefix("0x").unwrap_or(value);
460    let bytes = alloy_primitives::hex::decode(value).map_err(|err| CacheError::TraceParse {
461        details: format!("invalid bytecode hex: {err}"),
462    })?;
463    Ok(Bytes::from(bytes))
464}
465
466pub(crate) fn unix_timestamp_secs_saturating(time: SystemTime) -> u64 {
467    time.duration_since(UNIX_EPOCH)
468        .map(|duration| duration.as_secs())
469        .unwrap_or(0)
470}
471
472/// Read a storage slot from already-borrowed layers (`account_state`-aware),
473/// mirroring [`EvmCache::cached_storage_value`] but operating on a held backend
474/// storage guard rather than re-locking. Shared by the batched slot-run fast-path
475/// ([`EvmCache::apply_slot_run`]) so the same EVM-SLOAD semantics hold inside the
476/// held guard: the overlay slot wins; a `StorageCleared`/`NotExisting` overlay
477/// account reads a missing slot as ZERO (the backend is **not** consulted);
478/// otherwise it falls through to the backend.
479fn read_slot_account_state_aware<S1, S2>(
480    overlay: &std::collections::HashMap<Address, revm::database::DbAccount, S1>,
481    storage: &std::collections::HashMap<Address, foundry_fork_db::cache::StorageInfo, S2>,
482    address: Address,
483    slot: U256,
484) -> Option<U256>
485where
486    S1: std::hash::BuildHasher,
487    S2: std::hash::BuildHasher,
488{
489    if let Some(db_account) = overlay.get(&address) {
490        if let Some(value) = db_account.storage.get(&slot) {
491            return Some(*value);
492        }
493        if matches!(
494            db_account.account_state,
495            AccountState::StorageCleared | AccountState::NotExisting
496        ) {
497            return Some(U256::ZERO);
498        }
499    }
500    storage.get(&address).and_then(|s| s.get(&slot).copied())
501}
502
503/// Write a storage slot into already-borrowed layers, mirroring
504/// [`EvmCache::write_slot_through`] but operating on a held backend storage guard.
505/// Backend (layer 2) is always written; the overlay (layer 1) is written only if
506/// an overlay account already exists (never materialize a new overlay account).
507fn write_slot_into<S1, S2>(
508    overlay: &mut std::collections::HashMap<Address, revm::database::DbAccount, S1>,
509    storage: &mut std::collections::HashMap<Address, foundry_fork_db::cache::StorageInfo, S2>,
510    address: Address,
511    slot: U256,
512    value: U256,
513) where
514    S1: std::hash::BuildHasher,
515    S2: std::hash::BuildHasher + Default,
516{
517    storage.entry(address).or_default().insert(slot, value);
518    if let Some(db_account) = overlay.get_mut(&address) {
519        db_account.storage.insert(slot, value);
520    }
521}
522
523fn account_patch_is_empty(patch: &AccountPatch) -> bool {
524    patch.balance.is_none() && patch.nonce.is_none() && patch.code.is_none()
525}
526
527/// Preset runtime tuning profile for cache-side batch storage fetches.
528///
529/// Converts into [`StorageBatchConfig`]: faster modes send larger batches with
530/// more in-flight HTTP requests, slower modes throttle to avoid RPC rate-limiting
531/// (e.g. HTTP 429 on Base). Configure a preset per cache with
532/// [`EvmCacheBuilder::speed_mode`], or supply exact values with
533/// [`EvmCacheBuilder::storage_batch_config`].
534#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
535#[repr(u8)]
536pub enum CacheSpeedMode {
537    /// Largest batches, highest concurrency — fastest, most likely to trip rate limits.
538    Fast = 0,
539    /// Moderate batch size and concurrency.
540    Normal = 1,
541    /// Conservative batch size and concurrency. The default.
542    #[default]
543    Slow = 2,
544    /// Smallest batches, single in-flight request — slowest, gentlest on the RPC provider.
545    XSlow = 3,
546}
547
548/// Concrete tuning knobs for the provider-backed [`StorageBatchFetchFn`].
549///
550/// `slots_per_batch` controls how many `eth_getStorageAt` calls are packed into
551/// each JSON-RPC batch request. `max_concurrent_batches` controls how many of
552/// those HTTP batch requests may be in flight at once. Larger values can improve
553/// cold-start / verification throughput on tolerant RPC endpoints; smaller
554/// values are gentler on rate-limited providers.
555///
556/// This config only affects the fetcher created by the cache constructors. If a
557/// caller installs a custom fetcher with
558/// [`set_storage_batch_fetcher`](EvmCache::set_storage_batch_fetcher), that
559/// fetcher owns its own batching and throttling.
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub struct StorageBatchConfig {
562    /// Number of storage slots to include in one JSON-RPC batch request.
563    pub slots_per_batch: usize,
564    /// Maximum number of JSON-RPC batch requests in flight at once.
565    pub max_concurrent_batches: usize,
566}
567
568impl StorageBatchConfig {
569    /// Construct a config, normalizing zero values to one.
570    pub fn new(slots_per_batch: usize, max_concurrent_batches: usize) -> Self {
571        Self {
572            slots_per_batch,
573            max_concurrent_batches,
574        }
575        .normalized()
576    }
577
578    fn normalized(self) -> Self {
579        Self {
580            slots_per_batch: self.slots_per_batch.max(1),
581            max_concurrent_batches: self.max_concurrent_batches.max(1),
582        }
583    }
584}
585
586impl Default for StorageBatchConfig {
587    fn default() -> Self {
588        CacheSpeedMode::default().into()
589    }
590}
591
592impl From<CacheSpeedMode> for StorageBatchConfig {
593    fn from(mode: CacheSpeedMode) -> Self {
594        match mode {
595            CacheSpeedMode::Fast => Self::new(150, 8),
596            CacheSpeedMode::Normal => Self::new(100, 6),
597            CacheSpeedMode::Slow => Self::new(75, 4),
598            CacheSpeedMode::XSlow => Self::new(25, 1),
599        }
600    }
601}
602
603/// How a cache's batch storage fetcher loads slots.
604///
605/// The default is [`BulkCall`](Self::BulkCall): bulk `eth_call` state-override
606/// extraction (one call covers thousands of slots) with the classic
607/// point-read fetcher as its fallback and repair path. Requests below the
608/// bulk config's `point_read_threshold`, providers without state-override
609/// support, and precompile targets all degrade gracefully to point reads.
610/// See the [`bulk_storage`](crate::bulk_storage) module and
611/// `docs/bulk-storage-extraction.md` for mechanism and measured economics.
612#[derive(Debug, Clone, Copy, PartialEq, Eq)]
613pub enum StorageFetchStrategy {
614    /// Bulk `eth_call` extraction with point-read fallback (the default).
615    BulkCall(crate::bulk_storage::BulkCallConfig),
616    /// Classic JSON-RPC-batched `eth_getStorageAt` point reads only
617    /// (pre-0.2.0 behavior), tuned by [`StorageBatchConfig`].
618    PointRead,
619}
620
621impl Default for StorageFetchStrategy {
622    fn default() -> Self {
623        Self::BulkCall(crate::bulk_storage::BulkCallConfig::default())
624    }
625}
626
627/// Build the classic point-read [`StorageBatchFetchFn`]: JSON-RPC batches of
628/// `eth_getStorageAt`, sized and throttled by [`StorageBatchConfig`].
629///
630/// This is the default fetcher's *fallback* path (see
631/// [`StorageFetchStrategy`]) and the whole fetcher under
632/// [`StorageFetchStrategy::PointRead`]. It is public so callers composing
633/// their own fetchers — e.g.
634/// [`bulk_call_storage_fetcher_with_fallback`](crate::bulk_storage::bulk_call_storage_fetcher_with_fallback)
635/// over a differently-tuned repair path — can reuse it.
636pub fn point_read_storage_fetcher<P>(
637    provider: Arc<P>,
638    config: StorageBatchConfig,
639) -> StorageBatchFetchFn
640where
641    P: Provider<AnyNetwork> + 'static,
642{
643    let config = config.normalized();
644    Arc::new(
645        move |requests: Vec<(Address, U256)>, current_block: BlockId| {
646            use futures::stream::{self, StreamExt};
647            // Max items per JSON-RPC batch. RPC providers typically limit batch
648            // size to ~1000 items. Kept conservative to avoid 429s on Base.
649            let batch_size = config.slots_per_batch;
650            // Max concurrent HTTP batch requests. Each batch contains batch_size
651            // individual eth_getStorageAt calls. Limiting concurrency prevents
652            // thundering herd when prefetching thousands of storage slots.
653            let max_concurrent = config.max_concurrent_batches;
654
655            // Guard against panicking inside `block_in_place` on a
656            // current-thread runtime (or when no runtime is present): return
657            // an `Err` result for every requested slot instead.
658            let handle = match block_in_place_handle() {
659                Ok(handle) => handle,
660                Err(e) => {
661                    return requests
662                        .into_iter()
663                        .map(|(addr, slot)| {
664                            (
665                                addr,
666                                slot,
667                                Err(StorageFetchError::Runtime(RuntimeError::MissingRuntime {
668                                    details: e.to_string(),
669                                })),
670                            )
671                        })
672                        .collect();
673                }
674            };
675            // The caller supplies the exact block this fetch must observe.
676            // Capturing it at the call site is what lets the deferred
677            // freshness validator fetch at the snapshot's block despite a
678            // later `set_block`.
679            tokio::task::block_in_place(|| {
680                handle.block_on(async {
681                    let mut results = Vec::with_capacity(requests.len());
682
683                    // Build and send JSON-RPC batches (each batch = one HTTP request)
684                    let batch_futs: Vec<_> = requests
685                        .chunks(batch_size)
686                        .map(|chunk| {
687                            let client = provider.client();
688                            let mut batch = alloy_rpc_client::BatchRequest::new(client);
689                            let mut waiters = Vec::with_capacity(chunk.len());
690
691                            for &(addr, slot) in chunk {
692                                let params = (addr, slot, current_block);
693                                match batch.add_call::<_, U256>("eth_getStorageAt", &params) {
694                                    Ok(waiter) => waiters.push((addr, slot, Ok(waiter))),
695                                    Err(e) => {
696                                        // Serialization error — rare, treat as failure
697                                        tracing::warn!(
698                                            ?addr,
699                                            ?slot,
700                                            "batch request serialization failed: {}",
701                                            e
702                                        );
703                                        waiters.push((
704                                            addr,
705                                            slot,
706                                            Err(StorageFetchError::serialization(e)),
707                                        ));
708                                    }
709                                }
710                            }
711
712                            async move {
713                                // Send the batch as a single HTTP request
714                                let send_result = batch.send().await;
715                                let mut chunk_results = Vec::with_capacity(waiters.len());
716
717                                let batch_error =
718                                    send_result.as_ref().err().map(|err| err.to_string());
719                                for (addr, slot, waiter) in waiters {
720                                    match waiter {
721                                        Ok(waiter) => {
722                                            if let Some(source) = &batch_error {
723                                                chunk_results.push((
724                                                    addr,
725                                                    slot,
726                                                    Err(StorageFetchError::batch_send(source)),
727                                                ));
728                                                continue;
729                                            }
730                                            match waiter.await {
731                                                Ok(value) => {
732                                                    chunk_results.push((addr, slot, Ok(value)));
733                                                }
734                                                Err(e) => {
735                                                    chunk_results.push((
736                                                        addr,
737                                                        slot,
738                                                        Err(StorageFetchError::provider(
739                                                            "eth_getStorageAt",
740                                                            e,
741                                                        )),
742                                                    ));
743                                                }
744                                            }
745                                        }
746                                        Err(err) => {
747                                            chunk_results.push((addr, slot, Err(err)));
748                                        }
749                                    }
750                                }
751                                chunk_results
752                            }
753                        })
754                        .collect();
755
756                    // Fire batches with bounded concurrency (`max_concurrent`) to avoid
757                    // a thundering herd; per-batch size is the configured `batch_size`
758                    // chosen above, so throughput scales without overwhelming RPC providers.
759                    let all_batch_results: Vec<Vec<_>> = stream::iter(batch_futs)
760                        .buffer_unordered(max_concurrent)
761                        .collect()
762                        .await;
763                    for batch_results in all_batch_results {
764                        results.extend(batch_results);
765                    }
766                    results
767                })
768            })
769        },
770    )
771}
772
773/// Outcome of [`EvmCache::prewarm_slots`].
774#[derive(Debug, Default)]
775pub struct PrewarmReport {
776    /// Slots fetched and injected into the cache.
777    pub loaded: usize,
778    /// Pairs the fetcher failed to load, with the per-slot error.
779    pub failed: Vec<(Address, U256, StorageFetchError)>,
780}
781
782/// Outcome of [`EvmCache::verify_code_seeds`]: how each `Pending` canonical
783/// code claim resolved against the chain at the pinned block.
784///
785/// Fail-closed on trust, fail-safe on transport: `mismatched` /
786/// `not_deployed` / `codeless` entries were **purged** (both cache layers and
787/// the mark — the next touch refetches authoritative chain state), while
788/// `unverifiable` entries are **still `Pending`** (a failed read proves
789/// nothing, so the seed is neither promoted nor destroyed).
790#[derive(Clone, Debug, Default)]
791pub struct CodeVerifyReport {
792    /// Claims confirmed: marked [`CodeSeedState::Verified`], real balance
793    /// injected from the same response.
794    pub verified: Vec<Address>,
795    /// Claims contradicted by on-chain code — purged. Usually a wrong
796    /// template or immutable-patch offset; the mismatching hashes are
797    /// included for debugging.
798    pub mismatched: Vec<CodeMismatch>,
799    /// `EXTCODEHASH == 0`: no account at the pinned block — purged. This is
800    /// the live-registration race (the deployment is newer than the pin);
801    /// re-pin forward and re-seed rather than debugging the template.
802    pub not_deployed: Vec<Address>,
803    /// `EXTCODEHASH == keccak256("")`: the address exists but holds no code
804    /// (an EOA) — purged.
805    pub codeless: Vec<Address>,
806    /// The fetch failed (transport error, omitted address, or the
807    /// [`MULTICALL3_ADDRESS`](crate::multicall::MULTICALL3_ADDRESS) host
808    /// caveat) — each still `Pending`, with the reason.
809    pub unverifiable: Vec<(Address, String)>,
810}
811
812/// One contradicted code claim from [`EvmCache::verify_code_seeds`].
813#[derive(Clone, Debug, PartialEq, Eq)]
814pub struct CodeMismatch {
815    /// The seeded address.
816    pub address: Address,
817    /// The hash the seed claimed (keccak256 of the seeded bytes).
818    pub expected: B256,
819    /// The on-chain `EXTCODEHASH` observed at the pinned block.
820    pub actual: B256,
821}
822
823/// Behavior when overriding code at a target account that is not known to the cache/backend.
824#[derive(Debug, Clone, Copy, PartialEq, Eq)]
825pub enum MissingTargetBehavior {
826    /// Return an error if the target account cannot be loaded.
827    Error,
828    /// Create a default account with the replacement code.
829    Create,
830}
831
832/// Per-call transaction-environment overrides for a simulation.
833///
834/// `Default` reproduces the read-only behavior of the plain `call_raw`
835/// (zero value, default gas/nonce). Use the `*_with` call variants to supply
836/// these — e.g. to simulate a payable function, a native-ETH transfer, or a
837/// gas-bounded call. Balance affordability checks are disabled in the
838/// simulator, so a non-zero `value` does not require the caller to be funded.
839#[derive(Debug, Clone, Default)]
840pub struct TxConfig {
841    /// Native value (wei) sent with the call. Set this to simulate a payable
842    /// function or a native-ETH transfer. Balance checks are disabled in the
843    /// simulator, so the caller need not be funded for a non-zero value.
844    pub value: U256,
845    /// Gas limit for the call. `None` uses revm's default. Set this to model a
846    /// gas-bounded call (e.g. to observe out-of-gas behavior).
847    pub gas_limit: Option<u64>,
848    /// Gas price (wei) for the call. `None` uses revm's default. Rarely needed
849    /// because base-fee checks are disabled in the simulator.
850    pub gas_price: Option<u128>,
851    /// Sender nonce. `None` lets the simulator pick; nonce checks are disabled,
852    /// so this is only worth setting when a contract reads the nonce explicitly.
853    pub nonce: Option<u64>,
854    /// EIP-2930 access list to pre-warm accounts and storage slots for this
855    /// call. Pre-warming changes EIP-2929 gas accounting; supply it when
856    /// reproducing the gas cost of a transaction that carried an access list.
857    pub access_list: Option<AccessList>,
858}
859
860/// Which block-context header fields a cache requires to be present.
861///
862/// Block-env fields (`NUMBER` / `BASEFEE` / `COINBASE` / `PREVRANDAO` /
863/// `GASLIMIT`) are populated from a fetched block header. When a field is
864/// absent — because a fetch failed or the chain does not carry it — the EVM
865/// silently defaults it, which can steer contracts that branch on block context
866/// down a different code path and produce quietly-wrong simulations.
867///
868/// These per-field requirements let a caller opt into failing loudly instead.
869/// [`strict()`](Self::strict) requires every field; [`lenient()`](Self::lenient)
870/// (the [`Default`]) requires none and reproduces the historical
871/// silently-default behavior. A chain without EIP-1559, for example, can start
872/// from [`strict()`](Self::strict) and clear [`require_basefee`](Self::require_basefee).
873///
874/// Requirements are checked by [`validate_header`](Self::validate_header), which
875/// [`EvmCache::advance_block`] and [`EvmCacheBuilder::try_build`] call.
876#[derive(Clone, Copy, Debug, PartialEq, Eq)]
877pub struct BlockContextRequirements {
878    /// Require the header to carry a block number (`NUMBER`).
879    pub require_number: bool,
880    /// Require the header to carry an EIP-1559 base fee (`BASEFEE`).
881    pub require_basefee: bool,
882    /// Require the header to carry a beneficiary (`COINBASE`).
883    pub require_coinbase: bool,
884    /// Require the header to carry a `prevrandao` / mix hash (`PREVRANDAO`).
885    pub require_prevrandao: bool,
886    /// Require the header to carry a gas limit (`GASLIMIT`).
887    pub require_gas_limit: bool,
888}
889
890impl Default for BlockContextRequirements {
891    fn default() -> Self {
892        Self::lenient()
893    }
894}
895
896impl BlockContextRequirements {
897    /// Require every block-context field to be present.
898    ///
899    /// Under this policy a header missing any required field is rejected rather
900    /// than silently defaulted.
901    pub const fn strict() -> Self {
902        Self {
903            require_number: true,
904            require_basefee: true,
905            require_coinbase: true,
906            require_prevrandao: true,
907            require_gas_limit: true,
908        }
909    }
910
911    /// Require no block-context field (the [`Default`]).
912    ///
913    /// Reproduces the historical behavior: a missing field is silently
914    /// defaulted by the EVM.
915    pub const fn lenient() -> Self {
916        Self {
917            require_number: false,
918            require_basefee: false,
919            require_coinbase: false,
920            require_prevrandao: false,
921            require_gas_limit: false,
922        }
923    }
924
925    /// Validate that a header carries every required block-context field.
926    ///
927    /// Only the two `Option`-typed header fields can actually be absent:
928    /// [`require_basefee`](Self::require_basefee) checks
929    /// [`base_fee_per_gas`](alloy_consensus::BlockHeader::base_fee_per_gas) and
930    /// [`require_prevrandao`](Self::require_prevrandao) checks
931    /// [`mix_hash`](alloy_consensus::BlockHeader::mix_hash). Number, beneficiary
932    /// and gas limit are non-`Option` on the [`BlockHeader`] trait, so those
933    /// requirement flags are satisfied whenever a header is present. Returns
934    /// `Ok(())` when all required fields are satisfied.
935    pub fn validate_header<H: BlockHeader>(&self, header: &H) -> Result<(), BlockContextError> {
936        // `number`, `coinbase` (beneficiary) and `gas_limit` are non-`Option` on
937        // the `BlockHeader` trait: they are always present when a header exists,
938        // so their requirement flags are trivially satisfied here.
939        if self.require_basefee && header.base_fee_per_gas().is_none() {
940            return Err(BlockContextError::MissingField { field: "basefee" });
941        }
942        if self.require_prevrandao && header.mix_hash().is_none() {
943            return Err(BlockContextError::MissingField {
944                field: "prevrandao",
945            });
946        }
947        Ok(())
948    }
949}
950
951/// Fluent builder for [`EvmCache`].
952///
953/// A readable alternative to the positional [`EvmCache::with_cache`]
954/// constructor. Defaults: latest block, no disk cache, [`SpecId::CANCUN`].
955///
956/// ```no_run
957/// # use std::sync::Arc;
958/// # use alloy_provider::{ProviderBuilder, network::AnyNetwork};
959/// # use revm::primitives::hardfork::SpecId;
960/// # use evm_fork_cache::cache::EvmCache;
961/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
962/// let provider = ProviderBuilder::new()
963///     .network::<AnyNetwork>()
964///     .connect_http("https://example-rpc.invalid".parse()?);
965/// let cache = EvmCache::builder(Arc::new(provider))
966///     .latest_block()
967///     .spec(SpecId::CANCUN)
968///     .build()
969///     .await;
970/// # let _ = cache;
971/// # Ok(())
972/// # }
973/// ```
974pub struct EvmCacheBuilder<P> {
975    provider: Arc<P>,
976    block: BlockId,
977    cache_config: Option<CacheConfig>,
978    spec_id: SpecId,
979    shared_memory_capacity: SharedMemoryCapacity,
980    storage_batch_config: StorageBatchConfig,
981    storage_fetch_strategy: StorageFetchStrategy,
982    chain_id: Option<u64>,
983    block_context_requirements: BlockContextRequirements,
984    max_concurrent_proofs: usize,
985}
986
987impl<P> EvmCacheBuilder<P>
988where
989    P: Provider<AnyNetwork> + 'static,
990{
991    /// Start a builder over the given provider.
992    pub fn new(provider: Arc<P>) -> Self {
993        Self {
994            provider,
995            block: BlockId::latest(),
996            cache_config: None,
997            spec_id: SpecId::CANCUN,
998            shared_memory_capacity: SharedMemoryCapacity::default(),
999            storage_batch_config: StorageBatchConfig::default(),
1000            storage_fetch_strategy: StorageFetchStrategy::default(),
1001            chain_id: None,
1002            block_context_requirements: BlockContextRequirements::lenient(),
1003            max_concurrent_proofs: DEFAULT_MAX_CONCURRENT_PROOFS,
1004        }
1005    }
1006
1007    /// Cap the default account-proof fetcher's concurrent `eth_getProof`
1008    /// fan-out (default 8, name-symmetric with
1009    /// [`BulkCallConfig::max_concurrent_calls`](crate::bulk_storage::BulkCallConfig)).
1010    ///
1011    /// `eth_getProof` is single-address at the RPC level, so when the root
1012    /// gate or an account resync probes N tracked accounts in one seam call,
1013    /// concurrency is the only wall-clock lever: `N × RTT` serial becomes
1014    /// `~ceil(N / cap) × RTT`. Values are clamped to at least 1. Custom
1015    /// fetchers installed via
1016    /// [`set_account_proof_fetcher`](EvmCache::set_account_proof_fetcher)
1017    /// ignore this knob.
1018    pub fn max_concurrent_proofs(mut self, cap: usize) -> Self {
1019        self.max_concurrent_proofs = cap.max(1);
1020        self
1021    }
1022
1023    /// Pin simulations and RPC fetches to a specific block.
1024    ///
1025    /// Use this to fork at a fixed height for reproducible simulation. Without
1026    /// a call to [`block`](Self::block) or [`latest_block`](Self::latest_block)
1027    /// the builder defaults to the latest block at [`build`](Self::build) time.
1028    pub fn block(mut self, block: BlockId) -> Self {
1029        self.block = block;
1030        self
1031    }
1032
1033    /// Pin to the latest block.
1034    ///
1035    /// The height is resolved when [`build`](Self::build) fetches the block
1036    /// header, so the cache forks at whatever was latest at construction. Use
1037    /// [`block`](Self::block) instead to pin a fixed, reproducible height.
1038    pub fn latest_block(mut self) -> Self {
1039        self.block = BlockId::latest();
1040        self
1041    }
1042
1043    /// Set the EVM hardfork spec (must match the chain's execution layer).
1044    pub fn spec(mut self, spec_id: SpecId) -> Self {
1045        self.spec_id = spec_id;
1046        self
1047    }
1048
1049    /// Set the chain ID reported to simulations via the `CHAINID` opcode.
1050    ///
1051    /// **Recommended.** This is the explicit, authoritative way to set the chain
1052    /// ID. If left unset, [`build`](Self::build) infers it from the provider
1053    /// (`eth_chainId`), falling back to `1` (Ethereum mainnet) only if that query
1054    /// fails. A disk [`cache_config`](Self::cache_config) also carries a
1055    /// `chain_id` (which additionally namespaces the on-disk cache directory);
1056    /// when both are set, the value passed here wins for the `CHAINID` opcode, so
1057    /// keep them consistent.
1058    pub fn chain_id(mut self, chain_id: u64) -> Self {
1059        self.chain_id = Some(chain_id);
1060        self
1061    }
1062
1063    /// Enable disk-backed caching with the given configuration.
1064    ///
1065    /// Supplying a [`CacheConfig`] turns on persistence of EVM state, bytecodes,
1066    /// and immutable data under the configured chain directory; the cache is
1067    /// loaded on [`build`](Self::build) and flushed on drop. Omit it for a
1068    /// purely in-memory cache backed solely by RPC.
1069    pub fn cache_config(mut self, cache_config: CacheConfig) -> Self {
1070        self.cache_config = Some(cache_config);
1071        self
1072    }
1073
1074    /// Set how much EVM shared memory to pre-allocate per simulation context.
1075    ///
1076    /// Defaults to [`SharedMemoryCapacity::Fixed`] with `64 * 1024` bytes
1077    /// (65,536 bytes).
1078    /// Use `Fixed(n)` to pin a size, or [`SharedMemoryCapacity::Auto`] to size it
1079    /// from the chain state loaded at [`build`](Self::build) time (e.g. a bincode
1080    /// state file supplied via [`cache_config`](Self::cache_config)). See
1081    /// [`SharedMemoryCapacity`] for the trade-offs.
1082    pub fn shared_memory_capacity(mut self, capacity: SharedMemoryCapacity) -> Self {
1083        self.shared_memory_capacity = capacity;
1084        self
1085    }
1086
1087    /// Set the concrete storage batch-fetch configuration for this cache instance.
1088    ///
1089    /// The config controls the batch size and concurrency used by the
1090    /// provider-backed [`StorageBatchFetchFn`]. Defaults to
1091    /// [`StorageBatchConfig::default`] (the [`CacheSpeedMode::Slow`] preset).
1092    /// Different cache instances can use different values in the same process.
1093    /// Zero values are normalized to one.
1094    pub fn storage_batch_config(mut self, config: impl Into<StorageBatchConfig>) -> Self {
1095        self.storage_batch_config = config.into().normalized();
1096        self
1097    }
1098
1099    /// Set the storage batch-fetch profile from a preset.
1100    ///
1101    /// Shorthand for [`storage_batch_config`](Self::storage_batch_config) with
1102    /// `mode.into()`.
1103    pub fn speed_mode(self, mode: CacheSpeedMode) -> Self {
1104        self.storage_batch_config(mode)
1105    }
1106
1107    /// Choose how the cache's batch storage fetcher loads slots.
1108    ///
1109    /// Defaults to [`StorageFetchStrategy::BulkCall`] with
1110    /// [`BulkCallConfig::default`](crate::bulk_storage::BulkCallConfig::default):
1111    /// bulk `eth_call` state-override extraction, repaired by (and degrading
1112    /// to) the point-read fetcher that [`storage_batch_config`](Self::storage_batch_config)
1113    /// tunes. Use [`StorageFetchStrategy::PointRead`] to restore the classic
1114    /// per-slot behavior.
1115    pub fn storage_fetch_strategy(mut self, strategy: StorageFetchStrategy) -> Self {
1116        self.storage_fetch_strategy = strategy;
1117        self
1118    }
1119
1120    /// Tune the bulk `eth_call` extraction path.
1121    ///
1122    /// Shorthand for [`storage_fetch_strategy`](Self::storage_fetch_strategy)
1123    /// with [`StorageFetchStrategy::BulkCall`]`(config)` — e.g. raising
1124    /// `max_slots_per_call` on a provider with a generous gas cap, or
1125    /// selecting [`CallDispatch::CallMany`](crate::bulk_storage::CallDispatch::CallMany)
1126    /// on Erigon-lineage endpoints.
1127    pub fn bulk_call_config(self, config: crate::bulk_storage::BulkCallConfig) -> Self {
1128        self.storage_fetch_strategy(StorageFetchStrategy::BulkCall(config))
1129    }
1130
1131    /// Set which block-context header fields the cache requires.
1132    ///
1133    /// See [`BlockContextRequirements`]. Defaults to
1134    /// [`lenient`](BlockContextRequirements::lenient). Only [`try_build`](Self::try_build)
1135    /// enforces non-lenient requirements at construction; the infallible
1136    /// [`build`](Self::build) always stays lenient.
1137    pub fn block_context_requirements(mut self, reqs: BlockContextRequirements) -> Self {
1138        self.block_context_requirements = reqs;
1139        self
1140    }
1141
1142    /// Convenience toggle: require every block-context field (`true`) or none
1143    /// (`false`).
1144    ///
1145    /// Equivalent to
1146    /// [`block_context_requirements`](Self::block_context_requirements) with
1147    /// [`strict`](BlockContextRequirements::strict) /
1148    /// [`lenient`](BlockContextRequirements::lenient). Enforced only by
1149    /// [`try_build`](Self::try_build).
1150    pub fn strict_block_context(mut self, strict: bool) -> Self {
1151        self.block_context_requirements = if strict {
1152            BlockContextRequirements::strict()
1153        } else {
1154            BlockContextRequirements::lenient()
1155        };
1156        self
1157    }
1158
1159    /// Build the [`EvmCache`], fetching the pinned block's header for context.
1160    ///
1161    /// If a chain ID was not set via [`chain_id`](Self::chain_id), it is inferred
1162    /// from the provider (`eth_chainId`); see [`chain_id`](Self::chain_id) for the
1163    /// full resolution order.
1164    ///
1165    /// This constructor is infallible and always uses
1166    /// [`lenient`](BlockContextRequirements::lenient) enforcement (a missing
1167    /// block-context field is silently defaulted). To enforce
1168    /// [`BlockContextRequirements`] at construction, use
1169    /// [`try_build`](Self::try_build) instead.
1170    pub async fn build(self) -> EvmCache {
1171        let explicit_chain_id = self.chain_id;
1172        let provider = self.provider.clone();
1173        let strategy = self.storage_fetch_strategy;
1174        let storage_batch_config = self.storage_batch_config;
1175        let mut cache = EvmCache::with_cache_capacity_and_storage_batch_config(
1176            self.provider,
1177            self.block,
1178            self.cache_config,
1179            self.spec_id,
1180            self.shared_memory_capacity,
1181            self.storage_batch_config,
1182            self.max_concurrent_proofs,
1183        )
1184        .await;
1185        // An explicit builder value is authoritative for the `CHAINID` opcode and
1186        // overrides both the inferred value and any `cache_config` chain id.
1187        if let Some(chain_id) = explicit_chain_id {
1188            cache.set_chain_id(chain_id);
1189        }
1190        apply_storage_fetch_strategy(&mut cache, provider, strategy, storage_batch_config);
1191        cache
1192    }
1193
1194    /// Build the [`EvmCache`], enforcing the configured
1195    /// [`BlockContextRequirements`] against the fetched block header.
1196    ///
1197    /// Builds the cache the same way [`build`](Self::build) does, then, if the
1198    /// requirements are non-lenient, validates the pinned block's header:
1199    /// - if the header could not be fetched (the provider errored or returned no
1200    ///   block), returns [`BlockContextError::FetchFailed`];
1201    /// - otherwise validates the fetched header via
1202    ///   [`BlockContextRequirements::validate_header`] and propagates any
1203    ///   [`BlockContextError::MissingField`].
1204    ///
1205    /// A [`lenient`](BlockContextRequirements::lenient) build never errors (it
1206    /// does not fetch a header solely to validate). On success the requirements
1207    /// are stored on the returned cache so a later
1208    /// [`advance_block`](EvmCache::advance_block) enforces them too.
1209    pub async fn try_build(self) -> Result<EvmCache, BlockContextError> {
1210        let explicit_chain_id = self.chain_id;
1211        let reqs = self.block_context_requirements;
1212        let block = self.block;
1213        let provider = self.provider.clone();
1214        let strategy = self.storage_fetch_strategy;
1215        let storage_batch_config = self.storage_batch_config;
1216
1217        let mut cache = EvmCache::with_cache_capacity_and_storage_batch_config(
1218            self.provider,
1219            self.block,
1220            self.cache_config,
1221            self.spec_id,
1222            self.shared_memory_capacity,
1223            self.storage_batch_config,
1224            self.max_concurrent_proofs,
1225        )
1226        .await;
1227        if let Some(chain_id) = explicit_chain_id {
1228            cache.set_chain_id(chain_id);
1229        }
1230        cache.set_block_context_requirements(reqs);
1231        apply_storage_fetch_strategy(&mut cache, provider.clone(), strategy, storage_batch_config);
1232
1233        // Only a non-lenient policy fetches a header to validate: a lenient
1234        // build must never error and must not incur an extra RPC round-trip.
1235        if reqs != BlockContextRequirements::lenient() {
1236            match provider.get_block(block).await {
1237                Ok(Some(blk)) => reqs.validate_header(blk.header())?,
1238                Ok(None) => {
1239                    return Err(BlockContextError::FetchFailed(format!(
1240                        "no block header returned for {block:?}"
1241                    )));
1242                }
1243                Err(e) => return Err(BlockContextError::FetchFailed(e.to_string())),
1244            }
1245        }
1246
1247        Ok(cache)
1248    }
1249}
1250
1251/// Install the fetcher a [`StorageFetchStrategy`] describes on a built cache.
1252///
1253/// The constructor already installs the default strategy (bulk extraction
1254/// wrapping the point-read fetcher), so the default case is a no-op rather
1255/// than a redundant re-wrap.
1256fn apply_storage_fetch_strategy<P>(
1257    cache: &mut EvmCache,
1258    provider: Arc<P>,
1259    strategy: StorageFetchStrategy,
1260    batch_config: StorageBatchConfig,
1261) where
1262    P: Provider<AnyNetwork> + 'static,
1263{
1264    match strategy {
1265        StorageFetchStrategy::BulkCall(config)
1266            if config == crate::bulk_storage::BulkCallConfig::default() => {}
1267        StorageFetchStrategy::BulkCall(config) => {
1268            let fallback = point_read_storage_fetcher(provider.clone(), batch_config);
1269            cache.set_storage_batch_fetcher(
1270                crate::bulk_storage::bulk_call_storage_fetcher_with_fallback(
1271                    provider, config, fallback,
1272                ),
1273            );
1274        }
1275        StorageFetchStrategy::PointRead => {
1276            cache.set_storage_batch_fetcher(point_read_storage_fetcher(provider, batch_config));
1277        }
1278    }
1279}
1280
1281type CacheEvm<'a> = revm::MainnetEvm<
1282    Context<BlockEnv, TxEnv, CfgEnv, &'a mut ForkCacheDB, Journal<&'a mut ForkCacheDB>, ()>,
1283>;
1284type InspectorCacheEvm<'a, INSP> = revm::MainnetEvm<
1285    Context<BlockEnv, TxEnv, CfgEnv, &'a mut ForkCacheDB, Journal<&'a mut ForkCacheDB>, ()>,
1286    INSP,
1287>;
1288
1289/// Default initial capacity for the EVM shared-memory (working-memory) buffer.
1290/// 64 KiB (65,536 bytes), chosen from profiling a state-heavy workload (16x the
1291/// revm default of 4 KiB) so simulations rarely reallocate. Exposed for tuning via
1292/// [`SharedMemoryCapacity`].
1293const DEFAULT_SHARED_MEMORY_CAPACITY: usize = 64 * 1024;
1294
1295/// Default cap on the default account-proof fetcher's concurrent
1296/// `eth_getProof` fan-out (see [`EvmCacheBuilder::max_concurrent_proofs`]).
1297const DEFAULT_MAX_CONCURRENT_PROOFS: usize = 8;
1298
1299/// How much EVM shared memory (per-context working memory) to pre-allocate for
1300/// simulations.
1301///
1302/// revm grows its shared memory on demand during execution; pre-allocating just
1303/// avoids repeated reallocations when simulations touch a lot of memory — the
1304/// original motivation was a state-heavy workload where resizing was hot. The
1305/// trade-off cuts both ways: a wide parallel fan-out of *small* simulations pays
1306/// this much memory per overlay, so general users may want a smaller `Fixed` size,
1307/// while state-heavy users can raise it or let it auto-size from the loaded state.
1308///
1309/// The default is `Fixed(64 * 1024)` (65,536 bytes). Configure it on
1310/// [`EvmCacheBuilder::shared_memory_capacity`].
1311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1312pub enum SharedMemoryCapacity {
1313    /// Pre-allocate exactly this many bytes. The [`Default`] is
1314    /// `Fixed(64 * 1024)`.
1315    Fixed(usize),
1316    /// Size the buffer from the amount of chain state loaded into the cache at
1317    /// construction (e.g. from a bincode state file via
1318    /// [`CacheConfig`]/[`EvmCacheBuilder::cache_config`]), clamped to a sane
1319    /// floor/ceiling. Falls back to the floor when nothing is loaded.
1320    ///
1321    /// This is a heuristic proxy — persisted state size loosely correlates with the
1322    /// working-set size of simulations over it, not an exact peak-memory model. Use
1323    /// `Fixed` when you have profiled your workload.
1324    Auto,
1325}
1326
1327impl Default for SharedMemoryCapacity {
1328    fn default() -> Self {
1329        Self::Fixed(DEFAULT_SHARED_MEMORY_CAPACITY)
1330    }
1331}
1332
1333impl SharedMemoryCapacity {
1334    /// Floor for [`Auto`](Self::Auto) (and the default fixed size): 64 KiB
1335    /// (65,536 bytes).
1336    pub const MIN_AUTO: usize = DEFAULT_SHARED_MEMORY_CAPACITY;
1337    /// Ceiling for [`Auto`](Self::Auto): 4 MiB. A simulation that needs more than
1338    /// this still works — revm grows the buffer past it on demand.
1339    pub const MAX_AUTO: usize = 4 * 1024 * 1024;
1340    /// Heuristic proxy: bytes of pre-allocated working memory per loaded storage
1341    /// slot. Tune if profiling warrants.
1342    const AUTO_BYTES_PER_SLOT: usize = 16;
1343
1344    /// Resolve to a concrete byte capacity. `loaded_slots` is the number of layer-2
1345    /// storage slots present in the cache at construction (0 when nothing is
1346    /// loaded); it is consulted only for [`Auto`](Self::Auto).
1347    pub(crate) fn resolve(self, loaded_slots: usize) -> usize {
1348        match self {
1349            Self::Fixed(bytes) => bytes,
1350            Self::Auto => loaded_slots
1351                .saturating_mul(Self::AUTO_BYTES_PER_SLOT)
1352                .clamp(Self::MIN_AUTO, Self::MAX_AUTO),
1353        }
1354    }
1355}
1356
1357/// EVM cache with lazy-loading RPC backend.
1358///
1359/// Uses `foundry-fork-db` for intelligent caching and request deduplication.
1360/// Storage and account data is fetched on-demand when accessed during EVM execution,
1361/// eliminating the need for expensive access list prefetching.
1362pub struct EvmCache {
1363    backend: SharedBackend,
1364    blockchain_db: BlockchainDb,
1365    db: ForkCacheDB,
1366    token_decimals: HashMap<Address, u8>,
1367    block: BlockId,
1368    cache_config: Option<CacheConfig>,
1369    /// Cache for immutable on-chain data (token decimals).
1370    immutable_cache: ImmutableDataCache,
1371    /// Optional timestamp override for simulating future blocks.
1372    /// When set, EVM simulations use this timestamp instead of the current system time.
1373    timestamp_override: Option<u64>,
1374    /// Chain ID for EVM simulation (e.g. 42161 for Arbitrum, 1 for Ethereum).
1375    chain_id: u64,
1376    /// Block number for EVM simulations (NUMBER opcode).
1377    /// Fetched from block header during construction. Without this, revm defaults to 0
1378    /// which causes contracts that read block.number to execute different code paths.
1379    block_number: Option<u64>,
1380    /// Base fee per gas for EVM simulations (BASEFEE opcode).
1381    /// Fetched from block header during construction.
1382    basefee: Option<u64>,
1383    /// Block beneficiary for EVM simulations (COINBASE opcode).
1384    /// Fetched from the block header; commonly read by MEV/builder tip logic.
1385    coinbase: Option<Address>,
1386    /// `prevrandao` for EVM simulations (PREVRANDAO opcode), i.e. the header's
1387    /// mix hash post-merge. Drives on-chain randomness.
1388    prevrandao: Option<B256>,
1389    /// Block gas limit for EVM simulations (GASLIMIT opcode).
1390    block_gas_limit: Option<u64>,
1391    /// Which block-context header fields this cache requires to be present.
1392    /// [`lenient`](BlockContextRequirements::lenient) by default; the strict
1393    /// builder path sets it before returning. Enforced by
1394    /// [`advance_block`](Self::advance_block).
1395    block_context_requirements: BlockContextRequirements,
1396    /// Cache-side batch-fetch configuration for this instance.
1397    storage_batch_config: StorageBatchConfig,
1398    /// Shared memory buffer reused across EVM simulations.
1399    /// This avoids repeated allocations and allows measuring peak memory usage.
1400    shared_memory_buffer: Rc<RefCell<Vec<u8>>>,
1401    /// Optional callback for direct RPC `eth_call` (bypasses revm simulation).
1402    /// Set during construction from the provider. Useful for batch operations
1403    /// where revm's lazy storage fetching would be too slow.
1404    rpc_caller: Option<RpcCallFn>,
1405    /// Optional batch storage fetcher that bypasses SharedBackend.
1406    /// Captures a provider clone and fires concurrent `eth_getStorageAt` calls directly.
1407    /// Monotonic snapshot-consistency generation (see
1408    /// [`snapshot_generation`](Self::snapshot_generation)). Bumped by targeted
1409    /// state writes (`apply_update` / `apply_updates` / `modify_slot`) and
1410    /// block re-pins (`set_block` / `advance_block`); cold prefetch
1411    /// (`inject_storage_batch`) does not bump it.
1412    snapshot_generation: u64,
1413    storage_batch_fetcher: Option<StorageBatchFetchFn>,
1414    /// Optional account/root fetcher that bypasses SharedBackend.
1415    /// Captures a provider clone and fires `eth_getProof` calls directly to fetch
1416    /// authoritative account fields (balance/nonce/code hash) and `storageHash`.
1417    account_proof_fetcher: Option<AccountProofFetchFn>,
1418    /// Optional block state-diff fetcher backed by debug/trace RPC.
1419    block_state_diff_fetcher: Option<BlockStateDiffFetchFn>,
1420    /// Optional bulk account-fields fetcher (balance + `EXTCODEHASH` in one
1421    /// `eth_call`), the read side of code-seed verification.
1422    account_fields_fetcher: Option<AccountFieldsFetchFn>,
1423    /// Provenance + trust marks for bytecode that did not arrive via the lazy
1424    /// RPC backend (see [`CodeSeedState`]). Absence of a mark = RPC-origin.
1425    /// Persisted to `code_seeds.bin` (saved before `bytecodes.bin`, full
1426    /// replace) so a `Pending` claim never masquerades as chain-fetched
1427    /// across restarts.
1428    code_seeds: HashMap<Address, CodeSeedState>,
1429    /// Best-known ERC20 `balanceOf` mapping descriptor per token contract,
1430    /// carrying both the base slot and the detected [`SlotLayout`] so writes
1431    /// honor Vyper/Solady byte order — not just Solidity's `keccak(key‖slot)`.
1432    ///
1433    /// Populated by discovery (or seeding) and used by
1434    /// `set_erc20_balance_with_slot_scan` to avoid re-discovering per token.
1435    erc20_balance_slots: HashMap<Address, TrackedMapping>,
1436    /// EVM hardfork spec for simulations. Must match the chain's current execution
1437    /// layer hardfork for accurate gas accounting. Configured per-chain via `evm_spec`
1438    /// in `chains.toml`.
1439    spec_id: SpecId,
1440    /// Memoized, `Arc`-shared flatten of the cold layer-2 index, reused across
1441    /// successive [`snapshot`](Self::snapshot) calls (Pillar A).
1442    /// `None` until the first snapshot. Rebuilt copy-on-write by
1443    /// [`refresh_base`](Self::refresh_base); never mutated in place once shared.
1444    /// Not part of any public API and not serialized.
1445    base: Option<Arc<snapshot::BaseState>>,
1446    /// Layer-2 addresses changed since `base` was built, folded into the next base
1447    /// rebuild. Populated by the base-invalidation sites (write-through, batch
1448    /// injects, layer-2 seeding, purges). Not serialized.
1449    base_dirty: HashSet<Address>,
1450    /// When set, the next [`refresh_base`](Self::refresh_base) rebuilds the base
1451    /// from scratch. Set by [`set_block`](Self::set_block) /
1452    /// [`repin_to_block`](Self::repin_to_block), which replace layer 2 wholesale.
1453    /// Not serialized.
1454    base_full_rebuild: bool,
1455    /// Per-account layer-2 slot count at the last base build, used by
1456    /// [`refresh_base`](Self::refresh_base)'s `O(accounts)` length-scan to detect
1457    /// uncontrolled lazy-fetch growth that bypasses the write funnel. Not
1458    /// serialized.
1459    base_storage_lens: HashMap<Address, usize>,
1460    /// Resolved per-context EVM shared-memory pre-allocation (bytes), from the
1461    /// [`SharedMemoryCapacity`] at construction (resolving `Auto` against the loaded
1462    /// state). Propagated to each [`EvmSnapshot`] so snapshot-backed overlays
1463    /// pre-allocate the same amount. See
1464    /// [`shared_memory_capacity`](Self::shared_memory_capacity).
1465    shared_memory_capacity: usize,
1466}
1467
1468/// Outcome of a balance-delta-tracking simulation.
1469///
1470/// Produced by [`EvmCache::simulate_call_with_balance_deltas`] and
1471/// [`EvmCache::simulate_with_transfer_tracking`]: a successful call together
1472/// with the per-token balance changes it caused, its emitted logs, the touched
1473/// access list, and its raw return data.
1474/// Execution outcome of a simulated call.
1475///
1476/// Lets a caller distinguish a successful call — even one that emitted no logs,
1477/// such as a view call — from a revert or a halt, without guessing from `logs`
1478/// or `output`. Revert payloads live in [`CallSimulationResult::output`] and can
1479/// be decoded with [`RevertDecoder`](crate::errors::RevertDecoder); only `Halt`
1480/// carries extra data here, since its reason has nowhere else to live.
1481#[derive(Clone, Debug, PartialEq, Eq)]
1482pub enum SimStatus {
1483    /// The call returned successfully.
1484    Success,
1485    /// The call reverted; the revert payload (if any) is in `output`.
1486    Revert,
1487    /// The call halted (e.g. out of gas, invalid opcode).
1488    Halt {
1489        /// Debug-formatted halt reason.
1490        reason: String,
1491    },
1492}
1493
1494/// Outcome of a simulated call: status, return data, gas used, and the touched
1495/// access list. `#[non_exhaustive]` — construct via the simulation APIs and match
1496/// with a wildcard arm.
1497#[derive(Clone, Debug)]
1498#[non_exhaustive]
1499pub struct CallSimulationResult {
1500    /// Whether the call succeeded, reverted, or halted.
1501    pub status: SimStatus,
1502    /// Gas consumed by the (successful) call.
1503    pub gas_used: u64,
1504    /// Net change in `owner`'s balance per tracked token, as a **signed**
1505    /// [`I256`] (`post - pre`): positive means the call increased the balance,
1506    /// negative means it decreased it. Tokens not seen by the call may be
1507    /// absent or zero.
1508    pub token_deltas: HashMap<Address, I256>,
1509    /// Logs emitted by the call (in emission order).
1510    pub logs: Vec<Log>,
1511    /// EIP-2930 access list of all accounts and storage slots touched during simulation.
1512    /// Extracted from the EVM journaled state after execution.
1513    pub access_list: AccessList,
1514    /// Raw return data of the call.
1515    ///
1516    /// `Success` carries the returned bytes, `Revert` the revert payload, and
1517    /// `Halt` an empty slice. This makes a corrected view-call result observable:
1518    /// when a re-run reads a changed slot, the new return value differs here even
1519    /// if both runs succeed.
1520    pub output: Bytes,
1521}
1522
1523sol!(
1524    #[sol(rpc)]
1525    contract IERC20 {
1526        function balanceOf(address target) returns (uint256);
1527        function decimals() returns (uint8);
1528        function allowance(address owner, address spender) returns (uint256);
1529    }
1530);
1531
1532/// Parse an EVM hardfork spec name (e.g. from TOML config) into a revm [`SpecId`].
1533///
1534/// Accepts revm's canonical names (e.g. `"Cancun"`, `"Shanghai"`, `"Prague"`)
1535/// case-insensitively. Falls back to [`SpecId::CANCUN`] for unrecognized values.
1536pub fn parse_evm_spec(spec: &str) -> SpecId {
1537    // SpecId::from_str expects title-case (e.g. "Cancun"), so normalize the input.
1538    let mut chars = spec.chars();
1539    let title_case: String = match chars.next() {
1540        Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
1541        None => String::new(),
1542    };
1543    title_case.parse::<SpecId>().unwrap_or_else(|_| {
1544        warn!(spec, "Unknown EVM spec, defaulting to Cancun");
1545        SpecId::CANCUN
1546    })
1547}
1548
1549impl EvmCache {
1550    /// Start a fluent [`EvmCacheBuilder`] over the given provider.
1551    ///
1552    /// Preferred over the positional [`with_cache`](Self::with_cache) /
1553    /// [`new`](Self::new) constructors for readability.
1554    pub fn builder<P>(provider: Arc<P>) -> EvmCacheBuilder<P>
1555    where
1556        P: Provider<AnyNetwork> + 'static,
1557    {
1558        EvmCacheBuilder::new(provider)
1559    }
1560
1561    /// Create a new EvmCache with a SharedBackend that lazily fetches from RPC.
1562    ///
1563    /// The backend spawns a background handler task that manages RPC requests
1564    /// and deduplicates concurrent requests for the same data.
1565    ///
1566    /// # Runtime requirement
1567    /// RPC-backed operation requires a **multi-thread** tokio runtime
1568    /// (`#[tokio::main(flavor = "multi_thread")]` or
1569    /// `tokio::runtime::Builder::new_multi_thread()`). The direct RPC callbacks
1570    /// (`eth_call` and batch `eth_getStorageAt`) drive async work synchronously
1571    /// via `tokio::task::block_in_place`, which is unsupported on a
1572    /// current-thread runtime. On a current-thread runtime those callbacks
1573    /// degrade to typed errors rather than panicking.
1574    pub async fn new<P>(provider: Arc<P>) -> Self
1575    where
1576        P: Provider<AnyNetwork> + 'static,
1577    {
1578        Self::at_block(provider, BlockId::latest()).await
1579    }
1580
1581    /// Create a new EvmCache pinned to an explicit block.
1582    ///
1583    /// Prefer this over [`new`](Self::new) when reproducibility matters and the
1584    /// caller has already chosen the fork block.
1585    pub async fn at_block<P>(provider: Arc<P>, block: BlockId) -> Self
1586    where
1587        P: Provider<AnyNetwork> + 'static,
1588    {
1589        Self::with_cache(provider, block, None, SpecId::CANCUN).await
1590    }
1591
1592    /// Create a new EvmCache with disk-based caching.
1593    ///
1594    /// This enables several caching features:
1595    /// 1. Unified EVM state: Accounts + storage loaded from `evm_state.bin` (bincode)
1596    /// 2. Bytecode caching: Contract bytecodes from `bytecodes.bin`
1597    /// 3. Immutable data: Token decimals
1598    ///
1599    /// # Runtime requirement
1600    /// RPC-backed operation requires a **multi-thread** tokio runtime
1601    /// (`#[tokio::main(flavor = "multi_thread")]` or
1602    /// `tokio::runtime::Builder::new_multi_thread()`). The direct RPC callbacks
1603    /// (`eth_call` and batch `eth_getStorageAt`) drive async work synchronously
1604    /// via `tokio::task::block_in_place`, which is unsupported on a
1605    /// current-thread runtime. On a current-thread runtime those callbacks
1606    /// degrade to typed errors rather than panicking.
1607    pub async fn with_cache<P>(
1608        provider: Arc<P>,
1609        block: BlockId,
1610        cache_config: Option<CacheConfig>,
1611        spec_id: SpecId,
1612    ) -> Self
1613    where
1614        P: Provider<AnyNetwork> + 'static,
1615    {
1616        Self::with_cache_capacity(
1617            provider,
1618            block,
1619            cache_config,
1620            spec_id,
1621            SharedMemoryCapacity::default(),
1622        )
1623        .await
1624    }
1625
1626    /// Like [`with_cache`](Self::with_cache) but takes an explicit
1627    /// [`SharedMemoryCapacity`] controlling per-context EVM working-memory
1628    /// pre-allocation. This is what [`EvmCacheBuilder::build`] calls; prefer the
1629    /// builder. With [`SharedMemoryCapacity::Auto`] the buffer is sized from the
1630    /// layer-2 storage loaded at construction (e.g. a bincode state file).
1631    pub async fn with_cache_capacity<P>(
1632        provider: Arc<P>,
1633        block: BlockId,
1634        cache_config: Option<CacheConfig>,
1635        spec_id: SpecId,
1636        shared_memory_capacity: SharedMemoryCapacity,
1637    ) -> Self
1638    where
1639        P: Provider<AnyNetwork> + 'static,
1640    {
1641        Self::with_cache_capacity_and_storage_batch_config(
1642            provider,
1643            block,
1644            cache_config,
1645            spec_id,
1646            shared_memory_capacity,
1647            StorageBatchConfig::default(),
1648            DEFAULT_MAX_CONCURRENT_PROOFS,
1649        )
1650        .await
1651    }
1652
1653    #[allow(clippy::too_many_arguments)]
1654    async fn with_cache_capacity_and_storage_batch_config<P>(
1655        provider: Arc<P>,
1656        block: BlockId,
1657        cache_config: Option<CacheConfig>,
1658        spec_id: SpecId,
1659        shared_memory_capacity: SharedMemoryCapacity,
1660        storage_batch_config: StorageBatchConfig,
1661        max_concurrent_proofs: usize,
1662    ) -> Self
1663    where
1664        P: Provider<AnyNetwork> + 'static,
1665    {
1666        let block_id = block;
1667        let storage_batch_config = storage_batch_config.normalized();
1668        let max_concurrent_proofs = max_concurrent_proofs.max(1);
1669
1670        // Fetch the pinned block header for accurate block context (NUMBER,
1671        // BASEFEE, COINBASE, PREVRANDAO, GASLIMIT opcodes). Without this, revm
1672        // defaults to 0/default values, causing contracts that read block
1673        // context to execute different code paths. Use the concrete BlockId the
1674        // cache is pinned to so hash pins do not accidentally inherit latest
1675        // header context.
1676        let (block_number, basefee, coinbase, prevrandao, block_gas_limit) =
1677            match provider.get_block(block_id).await {
1678                Ok(Some(blk)) => {
1679                    let h = blk.header();
1680                    (
1681                        Some(h.number()),
1682                        h.base_fee_per_gas(),
1683                        Some(h.beneficiary()),
1684                        h.mix_hash(),
1685                        Some(h.gas_limit()),
1686                    )
1687                }
1688                Ok(None) => {
1689                    debug!("Block header not found for block context initialization");
1690                    (None, None, None, None, None)
1691                }
1692                Err(e) => {
1693                    debug!(error = %e, "Failed to fetch block header for block context");
1694                    (None, None, None, None, None)
1695                }
1696            };
1697
1698        // Ensure cache directory exists
1699        if let Some(cfg) = &cache_config {
1700            let _ = fs::create_dir_all(cfg.chain_dir());
1701        }
1702
1703        // Try to load EVM state from binary cache (bincode format)
1704        let blockchain_db = if let Some(cfg) = &cache_config {
1705            let binary_path = cfg.binary_state_cache_path();
1706
1707            if binary_path.exists() {
1708                let meta = BlockchainDbMeta::default();
1709                let db = BlockchainDb::new(meta, None);
1710                if binary_state::load_binary_state(&db, &binary_path) {
1711                    db
1712                } else {
1713                    let meta = BlockchainDbMeta::default();
1714                    BlockchainDb::new(meta, None)
1715                }
1716            } else {
1717                let meta = BlockchainDbMeta::default();
1718                BlockchainDb::new(meta, None)
1719            }
1720        } else {
1721            let meta = BlockchainDbMeta::default();
1722            BlockchainDb::new(meta, None)
1723        };
1724
1725        // Filter storage by maintain list (if configured)
1726        if let Some(cfg) = &cache_config {
1727            let has_filter = !cfg.maintain_addresses.is_empty() || !cfg.maintain_slots.is_empty();
1728            if has_filter {
1729                let mut storage = blockchain_db.storage().write();
1730                let before_contracts = storage.len();
1731                let before_slots: usize = storage.values().map(|s| s.len()).sum();
1732
1733                // Remove addresses not in any maintain list
1734                let addrs_to_remove: Vec<Address> = storage
1735                    .keys()
1736                    .filter(|addr| {
1737                        !cfg.maintain_addresses.contains(*addr)
1738                            && !cfg.maintain_slots.contains_key(*addr)
1739                    })
1740                    .copied()
1741                    .collect();
1742                for addr in &addrs_to_remove {
1743                    storage.remove(addr);
1744                }
1745
1746                // For maintain_slots addresses: keep only the specified slots
1747                for (addr, allowed_slots) in &cfg.maintain_slots {
1748                    if let Some(addr_storage) = storage.get_mut(addr) {
1749                        addr_storage.retain(|slot, _| allowed_slots.contains(slot));
1750                    }
1751                }
1752
1753                let after_contracts = storage.len();
1754                let after_slots: usize = storage.values().map(|s| s.len()).sum();
1755                drop(storage);
1756
1757                debug!(
1758                    contracts_removed = before_contracts.saturating_sub(after_contracts),
1759                    slots_removed = before_slots.saturating_sub(after_slots),
1760                    contracts_kept = after_contracts,
1761                    slots_kept = after_slots,
1762                    "Filtered cached storage by maintain list"
1763                );
1764            }
1765        }
1766
1767        // Seed bytecodes from the bytecodes.bin cache.
1768        // The binary EVM state cache stores accounts without bytecode,
1769        // so this is always needed when a cache config is present.
1770        if let Some(cfg) = &cache_config {
1771            let bytecode_path = cfg.bytecode_cache_path();
1772            if let Some(bytecode_cache) = BytecodeCache::load(&bytecode_path) {
1773                let loaded_count = Self::seed_bytecodes_from_cache(&blockchain_db, &bytecode_cache);
1774                if loaded_count > 0 {
1775                    debug!(
1776                        count = loaded_count,
1777                        path = ?bytecode_path,
1778                        "Loaded contract bytecodes from cache"
1779                    );
1780                }
1781            }
1782        }
1783
1784        // Restore code-seed marks. Pruning rule: a mark is kept only while the
1785        // account it describes still holds code with the marked hash — a mark
1786        // whose code did not survive (evicted, never persisted, or clobbered)
1787        // is meaningless and must not outlive it. The reverse orphan
1788        // (code-without-mark) is prevented by `flush()` writing
1789        // `code_seeds.bin` BEFORE `bytecodes.bin`.
1790        let code_seeds: HashMap<Address, CodeSeedState> = cache_config
1791            .as_ref()
1792            .and_then(|cfg| CodeSeedCache::load(&cfg.code_seeds_cache_path()))
1793            .map(|cache| {
1794                let accounts = blockchain_db.accounts().read();
1795                let before = cache.entries.len();
1796                let mut entries = cache.entries;
1797                entries.retain(|addr, state| {
1798                    accounts.get(addr).is_some_and(|info| {
1799                        info.code.as_ref().is_some_and(|code| !code.is_empty())
1800                            && info.code_hash == state.code_hash()
1801                    })
1802                });
1803                if entries.len() < before {
1804                    debug!(
1805                        pruned = before - entries.len(),
1806                        kept = entries.len(),
1807                        "Pruned code-seed marks whose code did not survive the reload"
1808                    );
1809                }
1810                entries
1811            })
1812            .unwrap_or_default();
1813
1814        // Load immutable data cache (token decimals).
1815        // This is still needed for validation and metadata lookups
1816        let immutable_cache = cache_config
1817            .as_ref()
1818            .and_then(|cfg| {
1819                let path = cfg.immutable_cache_path();
1820                ImmutableDataCache::load(&path).inspect(|cache| {
1821                    debug!(
1822                        token_decimals = cache.token_decimals.len(),
1823                        path = ?path,
1824                        "Loaded immutable data from cache"
1825                    );
1826                })
1827            })
1828            .unwrap_or_default();
1829
1830        // Pre-populate in-memory token decimals from immutable cache
1831        let token_decimals = immutable_cache.token_decimals.clone();
1832
1833        // Create an RPC callback for direct eth_call before moving provider into backend.
1834        // This bypasses revm simulation for batch queries where lazy storage fetching is too slow.
1835        let provider_for_rpc = provider.clone();
1836        let rpc_caller: RpcCallFn = Arc::new(move |to: Address, calldata: Bytes| {
1837            // Guard against panicking inside `block_in_place` on a current-thread
1838            // runtime (or when no runtime is present): degrade to a typed error.
1839            let handle = block_in_place_handle()?;
1840            tokio::task::block_in_place(|| {
1841                handle.block_on(async {
1842                    let tx = TransactionRequest::default()
1843                        .to(to)
1844                        .input(alloy_primitives::Bytes::from(calldata.to_vec()).into());
1845                    provider_for_rpc
1846                        .call(tx.into())
1847                        .await
1848                        .map_err(|e| RpcError::provider("eth_call", e))
1849                })
1850            })
1851        });
1852
1853        // Batch storage fetcher: bulk `eth_call` state-override extraction by
1854        // default, with the classic point-read fetcher as its fallback and
1855        // repair path (see the `bulk_storage` module and
1856        // docs/bulk-storage-extraction.md). `StorageBatchConfig` tunes the
1857        // point-read path; `EvmCacheBuilder::storage_fetch_strategy` swaps or
1858        // tunes the bulk path.
1859        let storage_batch_fetcher: StorageBatchFetchFn =
1860            crate::bulk_storage::bulk_call_storage_fetcher_with_fallback(
1861                provider.clone(),
1862                crate::bulk_storage::BulkCallConfig::default(),
1863                point_read_storage_fetcher(provider.clone(), storage_batch_config),
1864            );
1865
1866        // Create an account/root fetcher that bypasses SharedBackend, firing
1867        // `eth_getProof` calls directly for authoritative account fields plus the
1868        // account's `storageHash`. `eth_getProof` is single-address at the RPC
1869        // level, so a multi-account seam call (the reactive root gate, account
1870        // resyncs, cold-start probe_roots) fans out with bounded, order-
1871        // preserving concurrency (`buffered`): wall clock drops from N × RTT to
1872        // ~ceil(N / max_concurrent_proofs) × RTT.
1873        let provider_for_proof = provider.clone();
1874        let account_proof_fetcher: AccountProofFetchFn = Arc::new(
1875            move |requests: Vec<(Address, Vec<U256>)>, current_block: BlockId| {
1876                // Guard against panicking inside `block_in_place` on a
1877                // current-thread runtime (or when no runtime is present): return
1878                // an `Err` result for every requested address instead.
1879                let handle = match block_in_place_handle() {
1880                    Ok(handle) => handle,
1881                    Err(e) => {
1882                        return requests
1883                            .into_iter()
1884                            .map(|(addr, _keys)| {
1885                                (
1886                                    addr,
1887                                    Err(StorageFetchError::Runtime(RuntimeError::MissingRuntime {
1888                                        details: e.to_string(),
1889                                    })),
1890                                )
1891                            })
1892                            .collect();
1893                    }
1894                };
1895                let provider = provider_for_proof.clone();
1896                // The caller supplies the exact block this proof fetch must observe.
1897                tokio::task::block_in_place(|| {
1898                    handle.block_on(async {
1899                        use futures::StreamExt;
1900                        futures::stream::iter(requests.into_iter().map(|(addr, keys)| {
1901                            let provider = provider.clone();
1902                            async move {
1903                                // `eth_getProof` takes slot keys as 32-byte B256.
1904                                let proof_keys: Vec<B256> =
1905                                    keys.iter().map(|slot| B256::from(*slot)).collect();
1906                                let outcome = provider
1907                                    .get_proof(addr, proof_keys)
1908                                    .block_id(current_block)
1909                                    .await;
1910                                match outcome {
1911                                    Ok(response) => {
1912                                        // Map proven storage-proof entries back to
1913                                        // the requested `(slot, value)` pairs.
1914                                        let slots = response
1915                                            .storage_proof
1916                                            .iter()
1917                                            .map(|proof| {
1918                                                (
1919                                                    U256::from_be_bytes(proof.key.as_b256().0),
1920                                                    proof.value,
1921                                                )
1922                                            })
1923                                            .collect();
1924                                        (
1925                                            addr,
1926                                            Ok(AccountProof {
1927                                                storage_hash: response.storage_hash,
1928                                                balance: response.balance,
1929                                                nonce: response.nonce,
1930                                                code_hash: response.code_hash,
1931                                                slots,
1932                                            }),
1933                                        )
1934                                    }
1935                                    Err(e) => {
1936                                        (addr, Err(StorageFetchError::provider("eth_getProof", e)))
1937                                    }
1938                                }
1939                            }
1940                        }))
1941                        .buffered(max_concurrent_proofs)
1942                        .collect::<Vec<_>>()
1943                        .await
1944                    })
1945                })
1946            },
1947        );
1948
1949        // Create a bulk account-fields fetcher: balance + EXTCODEHASH for many
1950        // addresses in ONE eth_call via the account-fields extractor program.
1951        // This is the read side of code-seed verification; the call is
1952        // all-or-nothing per the `AccountFieldsFetchFn` contract.
1953        let provider_for_fields = provider.clone();
1954        let account_fields_fetcher: AccountFieldsFetchFn =
1955            Arc::new(move |addresses: Vec<Address>, block: BlockId| {
1956                // Guard against panicking inside `block_in_place` on a
1957                // current-thread runtime (or with no runtime present): degrade
1958                // to a typed error, matching the sibling fetchers.
1959                let handle = block_in_place_handle()?;
1960                tokio::task::block_in_place(|| {
1961                    handle.block_on(crate::bulk_storage::fetch_account_fields_bulk(
1962                        provider_for_fields.as_ref(),
1963                        &addresses,
1964                        block,
1965                    ))
1966                })
1967            });
1968
1969        // Create a block-level state-diff fetcher over debug trace RPC. The
1970        // reactive runtime uses this as a trace-first accelerator before falling
1971        // back to point reads for unresolved cold targets.
1972        let provider_for_trace = provider.clone();
1973        let block_state_diff_fetcher: BlockStateDiffFetchFn = Arc::new(move |block: BlockId| {
1974            let handle = block_in_place_handle()?;
1975            tokio::task::block_in_place(|| {
1976                handle.block_on(async {
1977                    let (method, params) = trace_rpc_method_and_params(block);
1978                    let response = provider_for_trace
1979                        .client()
1980                        .request::<_, serde_json::Value>(method, params)
1981                        .await
1982                        .map_err(|e| StorageFetchError::provider(method, e))?;
1983                    parse_block_state_diff_trace(&response)
1984                        .map_err(|err| StorageFetchError::custom(err.to_string()))
1985                })
1986            })
1987        });
1988
1989        // Resolve the chain ID reported to simulations (the `CHAINID` opcode). A
1990        // disk `CacheConfig` is authoritative (its `chain_id` also namespaces the
1991        // on-disk cache directory); otherwise infer it from the provider via
1992        // `eth_chainId`, falling back to 1 (Ethereum mainnet) only if that query
1993        // fails. Resolved before `provider` is moved into the backend below.
1994        // Prefer setting it explicitly through `EvmCacheBuilder::chain_id`.
1995        let chain_id = match cache_config.as_ref() {
1996            Some(cfg) => cfg.chain_id,
1997            None => match provider.get_chain_id().await {
1998                Ok(id) => id,
1999                Err(e) => {
2000                    debug!(
2001                        error = %e,
2002                        "Failed to infer chain ID from provider; defaulting to 1 (Ethereum mainnet). Set it explicitly via EvmCacheBuilder::chain_id."
2003                    );
2004                    1
2005                }
2006            },
2007        };
2008
2009        // Spawn the backend handler on a background task
2010        let backend =
2011            SharedBackend::spawn_backend(provider, blockchain_db.clone(), Some(block_id)).await;
2012
2013        let db = CacheDB::new(backend.clone());
2014
2015        // Resolve the shared-memory pre-allocation. For `Auto` we size from the
2016        // amount of layer-2 chain state actually loaded (post-filter), so a large
2017        // bincode state file yields a larger buffer; `Fixed` ignores the count.
2018        let loaded_slots = match shared_memory_capacity {
2019            SharedMemoryCapacity::Auto => blockchain_db
2020                .storage()
2021                .read()
2022                .values()
2023                .map(|s| s.len())
2024                .sum(),
2025            SharedMemoryCapacity::Fixed(_) => 0,
2026        };
2027        let shared_memory_capacity = shared_memory_capacity.resolve(loaded_slots);
2028
2029        Self {
2030            backend,
2031            blockchain_db,
2032            db,
2033            token_decimals,
2034            block,
2035            cache_config,
2036            immutable_cache,
2037            timestamp_override: None,
2038            chain_id,
2039            block_number,
2040            basefee,
2041            coinbase,
2042            prevrandao,
2043            block_gas_limit,
2044            block_context_requirements: BlockContextRequirements::lenient(),
2045            storage_batch_config,
2046            shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(shared_memory_capacity))),
2047            snapshot_generation: 0,
2048            rpc_caller: Some(rpc_caller),
2049            storage_batch_fetcher: Some(storage_batch_fetcher),
2050            account_proof_fetcher: Some(account_proof_fetcher),
2051            block_state_diff_fetcher: Some(block_state_diff_fetcher),
2052            account_fields_fetcher: Some(account_fields_fetcher),
2053            code_seeds,
2054            erc20_balance_slots: HashMap::new(),
2055            spec_id,
2056            base: None,
2057            base_dirty: HashSet::new(),
2058            base_full_rebuild: false,
2059            base_storage_lens: HashMap::new(),
2060            shared_memory_capacity,
2061        }
2062    }
2063
2064    /// Seed contract bytecodes into the BlockchainDb from a bytecode cache.
2065    ///
2066    /// This allows subsequent EVM executions to use cached bytecode instead of
2067    /// fetching from RPC. Storage slots will still be fetched fresh since they
2068    /// may have changed between blocks.
2069    fn seed_bytecodes_from_cache(db: &BlockchainDb, cache: &BytecodeCache) -> usize {
2070        let mut count = 0;
2071        for (addr, entry) in &cache.contracts {
2072            if entry.bytecode.is_empty() {
2073                continue;
2074            }
2075
2076            // Create bytecode and compute hash
2077            let bytecode = Bytecode::new_raw(Bytes::from(entry.bytecode.clone()));
2078            let code_hash: B256 = bytecode.hash_slow();
2079
2080            // Create account info with bytecode but zeroed balance/nonce
2081            // The balance/nonce will be fetched from RPC if needed during execution
2082            let info = AccountInfo {
2083                balance: U256::ZERO,
2084                nonce: 0,
2085                code_hash,
2086                code: Some(bytecode),
2087                account_id: None,
2088            };
2089
2090            db.db().do_insert_account(*addr, info);
2091            count += 1;
2092        }
2093        count
2094    }
2095
2096    /// Create a new EvmCache from an existing SharedBackend.
2097    ///
2098    /// Useful when you want to share a backend between multiple caches
2099    /// (e.g. parallel simulation threads).
2100    ///
2101    /// **Shared pinned block.** A `SharedBackend` owns a single pinned fork
2102    /// height. Calling [`set_block`](Self::set_block) / `repin_to_block` on *any*
2103    /// cache built from the same backend re-pins the RPC fork height for **all**
2104    /// of them. Sibling caches sharing one backend should agree on a block and not
2105    /// re-pin independently; build separate backends if they must fork at
2106    /// different heights.
2107    pub fn from_backend(
2108        backend: SharedBackend,
2109        blockchain_db: BlockchainDb,
2110        block: BlockId,
2111        chain_id: u64,
2112        block_number: Option<u64>,
2113        basefee: Option<u64>,
2114        spec_id: SpecId,
2115    ) -> Self {
2116        let db = CacheDB::new(backend.clone());
2117        Self {
2118            backend,
2119            blockchain_db,
2120            db,
2121            token_decimals: HashMap::new(),
2122            block,
2123            cache_config: None,
2124            immutable_cache: ImmutableDataCache::default(),
2125            timestamp_override: None,
2126            chain_id,
2127            block_number,
2128            basefee,
2129            coinbase: None,
2130            prevrandao: None,
2131            block_gas_limit: None,
2132            block_context_requirements: BlockContextRequirements::lenient(),
2133            storage_batch_config: StorageBatchConfig::default(),
2134            snapshot_generation: 0,
2135            shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(
2136                DEFAULT_SHARED_MEMORY_CAPACITY,
2137            ))),
2138            rpc_caller: None,
2139            storage_batch_fetcher: None,
2140            account_proof_fetcher: None,
2141            block_state_diff_fetcher: None,
2142            account_fields_fetcher: None,
2143            code_seeds: HashMap::new(),
2144            erc20_balance_slots: HashMap::new(),
2145            spec_id,
2146            base: None,
2147            base_dirty: HashSet::new(),
2148            base_full_rebuild: false,
2149            base_storage_lens: HashMap::new(),
2150            shared_memory_capacity: DEFAULT_SHARED_MEMORY_CAPACITY,
2151        }
2152    }
2153
2154    /// Flush the cache state to disk.
2155    ///
2156    /// This persists:
2157    /// 1. Unified EVM state (accounts + storage) to `evm_state.bin` (bincode)
2158    /// 2. Contract bytecodes to `bytecodes.bin`
2159    /// 3. Immutable data (token decimals) to `immutable_data.bin`
2160    ///
2161    /// Call this after loading hot contract state and running simulations to
2162    /// speed up subsequent runs.
2163    /// The cache is also automatically flushed when the EvmCache is dropped.
2164    pub fn flush(&self) -> Result<()> {
2165        if let Some(cfg) = &self.cache_config {
2166            // Save EVM state to binary cache (bincode format)
2167            let binary_path = cfg.binary_state_cache_path();
2168            binary_state::save_binary_state(&self.blockchain_db, &binary_path)?;
2169
2170            // Save code-seed marks BEFORE bytecodes (fail-closed ordering: a
2171            // mark without code is pruned on load and harmless; code without
2172            // its mark would let a Pending seed masquerade as RPC-origin).
2173            // Full replace, not merge: marks are mutable trust state, and a
2174            // merge would resurrect marks purged this session.
2175            let code_seeds_path = cfg.code_seeds_cache_path();
2176            CodeSeedCache {
2177                entries: self.code_seeds.clone(),
2178            }
2179            .save(&code_seeds_path)?;
2180            debug!(
2181                count = self.code_seeds.len(),
2182                path = ?code_seeds_path,
2183                "Updated code-seed mark cache (binary format)"
2184            );
2185
2186            // Save bytecode cache
2187            let bytecode_path = cfg.bytecode_cache_path();
2188            let mut bytecode_cache = BytecodeCache::load(&bytecode_path).unwrap_or_default();
2189            bytecode_cache.merge_from_db(&self.blockchain_db);
2190            bytecode_cache.save(&bytecode_path)?;
2191            debug!(
2192                count = bytecode_cache.contracts.len(),
2193                path = ?bytecode_path,
2194                "Updated bytecode cache (binary format)"
2195            );
2196
2197            // Save the immutable data cache
2198            let immutable_path = cfg.immutable_cache_path();
2199            self.immutable_cache.save(&immutable_path)?;
2200            debug!(
2201                token_decimals = self.immutable_cache.token_decimals.len(),
2202                path = ?immutable_path,
2203                "Updated immutable data cache"
2204            );
2205        }
2206        Ok(())
2207    }
2208
2209    /// Get the cache configuration, if any.
2210    ///
2211    /// Returns `None` when the cache is purely in-memory (no disk persistence),
2212    /// i.e. constructed without a [`CacheConfig`] or via
2213    /// [`from_backend`](Self::from_backend).
2214    pub fn cache_config(&self) -> Option<&CacheConfig> {
2215        self.cache_config.as_ref()
2216    }
2217
2218    /// Run a synchronous direct mutation against the underlying [`BlockchainDb`]
2219    /// and invalidate the memoized snapshot base afterwards.
2220    ///
2221    /// This is the preferred escape hatch for unavoidable layer-2 map writes such
2222    /// as `accounts().write().insert(...)` or `storage().write().insert(...)`.
2223    /// The closure still bypasses the CacheDB overlay and the normal write funnel,
2224    /// so use higher-level mutators when they can express the change. Unlike
2225    /// [`unchecked_blockchain_db`](Self::unchecked_blockchain_db), this wrapper
2226    /// keeps the copy-on-write snapshot base honest automatically after in-place
2227    /// overwrites whose map cardinality does not change.
2228    pub fn with_blockchain_db_mut<R>(&mut self, f: impl FnOnce(&BlockchainDb) -> R) -> R {
2229        let result = f(&self.blockchain_db);
2230        self.invalidate_base();
2231        result
2232    }
2233
2234    /// Get an unchecked reference to the underlying [`BlockchainDb`] (the layer-2
2235    /// backend store of accounts, storage, and bytecodes).
2236    ///
2237    /// This exposes an internal store and bypasses the cache's two-layer
2238    /// consistency model: reads here see only the backend layer, not the CacheDB
2239    /// overlay, and any writes performed through it skip the overlay. Prefer
2240    /// higher-level accessors or [`with_blockchain_db_mut`](Self::with_blockchain_db_mut)
2241    /// for direct synchronous writes.
2242    ///
2243    /// # Snapshot base
2244    /// Writing layer 2 directly through this unchecked handle also bypasses the
2245    /// memoized copy-on-write snapshot base (Pillar A). The next
2246    /// [`snapshot`](Self::snapshot) only performs a count/absence
2247    /// growth scan over layer 2, which catches lazy RPC-populated accounts/slots
2248    /// because that path only appends at a fixed block. It does **not** catch
2249    /// direct in-place changes where cardinality is unchanged: overwriting an
2250    /// existing storage slot, or changing an existing account's info/code/balance
2251    /// without adding a new account, can leave a stale snapshot base. After such a
2252    /// direct write, call
2253    /// [`invalidate_snapshot_base`](Self::invalidate_snapshot_base) (or re-pin via
2254    /// [`set_block`](Self::set_block)) before the next snapshot. Writes via the
2255    /// crate's own mutators (`inject_storage_batch`, `apply_update`, the `inject_*`
2256    /// helpers, the purges) keep the base honest automatically.
2257    pub fn unchecked_blockchain_db(&self) -> &BlockchainDb {
2258        &self.blockchain_db
2259    }
2260
2261    /// Get an unchecked reference to the underlying [`SharedBackend`] (the lazy
2262    /// RPC-backed fetcher shared across clones).
2263    ///
2264    /// This exposes an internal handle and bypasses the cache's two-layer consistency
2265    /// model: it reads/fetches directly without consulting the CacheDB overlay.
2266    /// Prefer the higher-level accessors; use with care.
2267    ///
2268    /// # Snapshot base
2269    /// Lazy RPC fetches through this backend only append missing accounts/slots at
2270    /// the pinned block, so the snapshot growth scan catches them without an
2271    /// explicit invalidation. Direct `SharedBackend::insert_or_update_storage` /
2272    /// `insert_or_update_address` calls are different: they enqueue a background
2273    /// handler request that can rewrite layer-2 entries **in place**, leaving the
2274    /// memoized copy-on-write base stale at an unchanged slot/account count.
2275    ///
2276    /// If you use those helpers directly, first synchronize with the backend
2277    /// handler by reading back the updated account/slot through `SharedBackend`
2278    /// (for example via `basic_ref` / `storage_ref`), then call
2279    /// [`invalidate_snapshot_base`](Self::invalidate_snapshot_base) before the next
2280    /// [`snapshot`](Self::snapshot). Calling
2281    /// `invalidate_snapshot_base` immediately after `insert_or_update_*` is not, by
2282    /// itself, a guarantee that the queued update has been applied before the next
2283    /// snapshot.
2284    pub fn unchecked_backend(&self) -> &SharedBackend {
2285        &self.backend
2286    }
2287
2288    /// Get a mutable reference to the underlying [`ForkCacheDB`] (the layer-1
2289    /// CacheDB overlay).
2290    ///
2291    /// This exposes an internal and bypasses the cache's two-layer consistency
2292    /// model: writes made here land only in the overlay and are not mirrored
2293    /// into the BlockchainDb backend, so parallel tasks sharing the backend
2294    /// will not see them. Prefer the higher-level mutators; use with care.
2295    pub fn db_mut(&mut self) -> &mut ForkCacheDB {
2296        &mut self.db
2297    }
2298
2299    /// Make a direct RPC `eth_call` to the node, bypassing revm simulation.
2300    ///
2301    /// This is much faster than `call_raw` for batch operations because the RPC
2302    /// node has all state in memory and doesn't need lazy storage fetching.
2303    /// Returns `None` if no RPC caller is available (e.g. `from_backend` constructor).
2304    ///
2305    /// # Panics
2306    /// Must be called from within a **multi-thread** tokio runtime: the callback
2307    /// drives the async `eth_call` to completion via
2308    /// `tokio::task::block_in_place`. On a current-thread runtime (or with no
2309    /// runtime), the callback degrades to an `Err` rather than panicking, but
2310    /// `block_in_place` itself will panic if invoked from a non-worker thread of
2311    /// a multi-thread runtime.
2312    pub fn rpc_call(&self, to: Address, calldata: Bytes) -> Option<Result<Bytes, RpcError>> {
2313        self.rpc_caller
2314            .as_ref()
2315            .map(|caller| (caller)(to, calldata))
2316    }
2317
2318    /// Get the batch storage fetcher, if available.
2319    ///
2320    /// Returns `None` when constructed via `from_backend` (no provider available).
2321    ///
2322    /// # Panics
2323    /// The returned [`StorageBatchFetchFn`] must be invoked from within a
2324    /// **multi-thread** tokio runtime: it drives concurrent `eth_getStorageAt`
2325    /// calls to completion via `tokio::task::block_in_place`. On a
2326    /// current-thread runtime (or with no runtime) it degrades to an `Err`
2327    /// result for every requested slot rather than panicking, but
2328    /// `block_in_place` itself will panic if invoked from a non-worker thread of
2329    /// a multi-thread runtime.
2330    pub fn storage_batch_fetcher(&self) -> Option<&StorageBatchFetchFn> {
2331        self.storage_batch_fetcher.as_ref()
2332    }
2333
2334    /// Get the account/root proof fetcher, if available.
2335    ///
2336    /// Returns `None` when constructed via `from_backend` (no provider
2337    /// available) unless a fetcher was injected via
2338    /// [`set_account_proof_fetcher`](Self::set_account_proof_fetcher).
2339    ///
2340    /// # Panics
2341    /// The returned [`AccountProofFetchFn`] must be invoked from within a
2342    /// **multi-thread** tokio runtime: it drives `eth_getProof` calls to
2343    /// completion via `tokio::task::block_in_place`. On a current-thread runtime
2344    /// (or with no runtime) it degrades to an `Err` result for every requested
2345    /// address rather than panicking, but `block_in_place` itself will panic if
2346    /// invoked from a non-worker thread of a multi-thread runtime.
2347    pub fn account_proof_fetcher(&self) -> Option<&AccountProofFetchFn> {
2348        self.account_proof_fetcher.as_ref()
2349    }
2350
2351    /// Get the block state-diff fetcher, if available.
2352    ///
2353    /// Returns `None` when constructed via `from_backend` (no provider
2354    /// available) unless a fetcher was injected via
2355    /// [`set_block_state_diff_fetcher`](Self::set_block_state_diff_fetcher).
2356    pub fn block_state_diff_fetcher(&self) -> Option<&BlockStateDiffFetchFn> {
2357        self.block_state_diff_fetcher.as_ref()
2358    }
2359
2360    /// Inject batch-fetched storage values directly into BlockchainDb (layer 2).
2361    ///
2362    /// This bypasses SharedBackend and makes values available for subsequent
2363    /// `storage_ref()` calls and EVM SLOADs. Used after `StorageBatchFetchFn`
2364    /// returns results to populate the cache in bulk.
2365    ///
2366    /// Takes `&mut self` (as of Pillar A) so it can mark each touched address dirty
2367    /// for the memoized copy-on-write base; the write itself is still a direct
2368    /// layer-2 backend write. Overwriting an existing slot at an unchanged slot
2369    /// count is invalidated here too, since the `refresh_base` growth scan only
2370    /// catches length changes.
2371    pub fn inject_storage_batch(&mut self, results: &[(Address, U256, U256)]) {
2372        {
2373            let mut storage = self.blockchain_db.storage().write();
2374            for &(addr, slot, value) in results {
2375                storage.entry(addr).or_default().insert(slot, value);
2376            }
2377        }
2378        for &(addr, _, _) in results {
2379            self.mark_base_dirty(addr);
2380        }
2381    }
2382
2383    /// Inject freshly-fetched storage values, healing **both** cache layers.
2384    ///
2385    /// Like [`inject_storage_batch`](Self::inject_storage_batch) this writes each
2386    /// value into the BlockchainDb backend (layer 2). Additionally, for any
2387    /// address that *already* has a CacheDB overlay entry (layer 1), it writes
2388    /// the slot into that overlay too.
2389    ///
2390    /// This matters because both [`snapshot`](Self::snapshot) and
2391    /// the synchronous EVM SLOAD path let the overlay win over the backend. A
2392    /// correction written only to layer 2 would be shadowed by a stale layer-1
2393    /// slot, so the cache could never converge — the freshness validator would
2394    /// re-detect the same change and re-correct it every cycle. Writing through
2395    /// the overlay keeps the layer that wins authoritative.
2396    ///
2397    /// It deliberately does **not** create a new overlay account for an address
2398    /// that has none: such a slot is layer-2-only (e.g. cold prefetch), where
2399    /// the backend write is already authoritative and materializing an overlay
2400    /// entry would pollute layer 1 and could shadow later RPC reads.
2401    pub fn inject_storage_batch_fresh(&mut self, results: &[(Address, U256, U256)]) {
2402        // Thin wrapper over the unified write primitive (the F1 fix now lives in
2403        // `apply_slot`). Each tuple becomes a write-through `StateUpdate::Slot`;
2404        // the returned diff is discarded to preserve this method's `-> ()` API.
2405        let updates: Vec<StateUpdate> = results
2406            .iter()
2407            .map(|&(addr, slot, value)| StateUpdate::slot(addr, slot, value))
2408            .collect();
2409        let _ = self.apply_updates(&updates);
2410    }
2411
2412    /// Bulk-load the given slots into the cache at its pinned block.
2413    ///
2414    /// Fetches through the installed [`StorageBatchFetchFn`] — bulk `eth_call`
2415    /// extraction by default, so thousands of slots (across many contracts)
2416    /// arrive in a handful of calls — and injects every successfully fetched
2417    /// value into layer 2 via
2418    /// [`inject_storage_batch`](Self::inject_storage_batch), the cold-prefetch
2419    /// write. Use it to prewarm a declared working set (an AMM pool's tick
2420    /// range, a protocol's config slots) before entering a simulation or
2421    /// reactive loop, complementing the *recorded* working sets that
2422    /// [`prefetch_registry`](crate::prefetch_registry) replays.
2423    ///
2424    /// Duplicate pairs are fetched once each and injected idempotently.
2425    /// Returns how many slots loaded and which pairs failed; failures leave
2426    /// the cache unchanged (those slots lazily point-read later as usual).
2427    pub fn prewarm_slots(&mut self, requests: &[(Address, U256)]) -> PrewarmReport {
2428        let Some(fetcher) = self.storage_batch_fetcher.clone() else {
2429            return PrewarmReport {
2430                loaded: 0,
2431                failed: requests
2432                    .iter()
2433                    .map(|&(addr, slot)| {
2434                        (
2435                            addr,
2436                            slot,
2437                            StorageFetchError::custom("no storage batch fetcher installed"),
2438                        )
2439                    })
2440                    .collect(),
2441            };
2442        };
2443        let results = fetcher(requests.to_vec(), self.block);
2444        let mut to_inject = Vec::with_capacity(results.len());
2445        let mut failed = Vec::new();
2446        for (addr, slot, result) in results {
2447            match result {
2448                Ok(value) => to_inject.push((addr, slot, value)),
2449                Err(e) => failed.push((addr, slot, e)),
2450            }
2451        }
2452        self.inject_storage_batch(&to_inject);
2453        PrewarmReport {
2454            loaded: to_inject.len(),
2455            failed,
2456        }
2457    }
2458
2459    /// Apply a single targeted [`StateUpdate`], returning a [`StateDiff`] of what
2460    /// actually changed.
2461    ///
2462    /// This is the single primitive that writes the state-update vocabulary
2463    /// across both cache layers with one consistent, documented policy. It is
2464    /// **synchronous and infallible** — a write, not a fetch, so it never touches
2465    /// RPC and never errors. See the [`state_update`](crate::state_update) module
2466    /// for the dual-layer write-through policy and the diff semantics.
2467    ///
2468    /// - [`StateUpdate::Slot`] — write `value` into the backend (layer 2) always,
2469    ///   and into the overlay (layer 1) only if an overlay account already
2470    ///   exists. Records a [`SlotChange`] only when the value actually changes
2471    ///   (`old.unwrap_or(ZERO) != value`).
2472    /// - [`StateUpdate::SlotDelta`] — *relative*, cold-aware. If the slot has a
2473    ///   cached value, write the saturating delta through the same path and record
2474    ///   a [`SlotChange`] iff it changed; if the slot is cold (absent from both
2475    ///   layers), apply nothing and surface a `SkippedDelta` in `diff.skipped`.
2476    /// - [`StateUpdate::BalanceDelta`] — *relative*, cold-aware native-balance
2477    ///   update. If the account is present in either layer, apply the saturating
2478    ///   delta to its balance (nonce/code preserved) write-through and record an
2479    ///   [`AccountChange`] iff it changed; if the account is cold (absent from both
2480    ///   layers), apply nothing and surface a [`SkippedBalanceDelta`] in
2481    ///   `diff.skipped_balances` (no default account is materialized).
2482    /// - [`StateUpdate::Account`] — load the current `AccountInfo` from the cached
2483    ///   layers (no RPC), apply each `Some` patch field (recomputing the code hash
2484    ///   when `code` is set), then write through with the same layer policy.
2485    ///   Records an [`AccountChange`] with `Some((old, new))` only for fields
2486    ///   that changed. If the account is cold (absent from both layers), apply
2487    ///   nothing and surface a [`SkippedAccountPatch`] in
2488    ///   `diff.skipped_accounts`.
2489    /// - [`StateUpdate::AccountUpsert`] — same patch semantics, but intentionally
2490    ///   materializes a cold/default account when absent from both layers.
2491    /// - [`StateUpdate::Purge`] — dispatch to the matching purge layer logic and
2492    ///   record a [`PurgeRecord`].
2493    ///
2494    /// # Warning — relative updates can be skipped
2495    ///
2496    /// A cold-aware update targeting a **cold** address is *dropped, not applied*
2497    /// unless it is an explicit [`StateUpdate::AccountUpsert`]. Because a skip
2498    /// produces no change, it is invisible to the changes-only
2499    /// [`StateDiff::is_empty`] / [`StateDiff::len`] success check, so after
2500    /// applying cold-aware updates the caller **must** inspect
2501    /// [`StateDiff::has_skipped`] (or the `skipped_*` fields) and fetch+seed the
2502    /// cold target.
2503    ///
2504    /// ```no_run
2505    /// # use alloy_primitives::{Address, U256};
2506    /// # use evm_fork_cache::StateUpdate;
2507    /// # fn example(cache: &mut evm_fork_cache::cache::EvmCache) {
2508    /// let contract = Address::repeat_byte(0x01);
2509    /// let diff = cache.apply_update(&StateUpdate::slot(contract, U256::from(0), U256::from(42)));
2510    /// assert_eq!(diff.slots.len(), 1);
2511    /// # }
2512    /// ```
2513    pub fn apply_update(&mut self, update: &StateUpdate) -> StateDiff {
2514        self.bump_snapshot_generation();
2515        let mut diff = StateDiff::default();
2516        match update {
2517            StateUpdate::Slot {
2518                address,
2519                slot,
2520                value,
2521            } => {
2522                if let Some(change) = self.apply_slot(*address, *slot, *value) {
2523                    diff.slots.push(change);
2524                }
2525            }
2526            StateUpdate::SlotDelta {
2527                address,
2528                slot,
2529                delta,
2530            } => match self.cached_storage_value(*address, *slot) {
2531                // Hot slot: apply the saturating delta write-through. Build the
2532                // change from the value we already read (do not route through
2533                // `apply_slot`, which would re-read the same slot — §16.9.1).
2534                Some(current) => {
2535                    let new = delta.apply(current);
2536                    self.write_slot_through(*address, *slot, new);
2537                    if current != new {
2538                        diff.slots.push(SlotChange {
2539                            address: *address,
2540                            slot: *slot,
2541                            old: current,
2542                            new,
2543                        });
2544                    }
2545                }
2546                // Cold slot: applying `0 ± amount` would corrupt an unknown value,
2547                // so write nothing and surface the skip for the caller to seed.
2548                None => diff.skipped.push(SkippedDelta {
2549                    address: *address,
2550                    slot: *slot,
2551                    delta: *delta,
2552                }),
2553            },
2554            StateUpdate::SlotMasked {
2555                address,
2556                slot,
2557                mask,
2558                value,
2559            } => match self.cached_storage_value(*address, *slot) {
2560                // Hot slot: overwrite only the masked bits, preserving the rest.
2561                // Build the change from the value we already read (mirroring the
2562                // `SlotDelta` arm; do not re-read through `apply_slot`).
2563                Some(old) => {
2564                    let new = (old & !*mask) | (*value & *mask);
2565                    self.write_slot_through(*address, *slot, new);
2566                    if old != new {
2567                        diff.slots.push(SlotChange {
2568                            address: *address,
2569                            slot: *slot,
2570                            old,
2571                            new,
2572                        });
2573                    }
2574                }
2575                // Cold slot: the un-masked bits are unknown, so the result cannot
2576                // be computed; write nothing and surface the skip for re-seeding.
2577                None => diff.skipped_masks.push(SkippedMask {
2578                    address: *address,
2579                    slot: *slot,
2580                    mask: *mask,
2581                    value: *value,
2582                }),
2583            },
2584            StateUpdate::BalanceDelta { address, delta } => {
2585                match self.apply_balance_delta(*address, *delta) {
2586                    // Hot account: the saturating delta was applied.
2587                    Ok(Some(change)) => diff.accounts.push(change),
2588                    // Hot account but no change (e.g. Sub from 0, Add of 0).
2589                    Ok(None) => {}
2590                    // Cold account: surface the skip; nothing was materialized.
2591                    Err(skipped) => diff.skipped_balances.push(skipped),
2592                }
2593            }
2594            StateUpdate::Account { address, patch } => {
2595                match self.apply_account_patch(*address, patch, false) {
2596                    Ok(Some(change)) => diff.accounts.push(change),
2597                    Ok(None) => {}
2598                    Err(skipped) => diff.skipped_accounts.push(skipped),
2599                }
2600            }
2601            StateUpdate::AccountUpsert { address, patch } => {
2602                if let Some(change) = self
2603                    .apply_account_patch(*address, patch, true)
2604                    .expect("AccountUpsert never skips cold account patches")
2605                {
2606                    diff.accounts.push(change);
2607                }
2608            }
2609            StateUpdate::Purge { address, scope } => {
2610                diff.purged.push(self.apply_purge(*address, scope));
2611            }
2612        }
2613        diff
2614    }
2615
2616    /// Apply a batch of [`StateUpdate`]s left-to-right, merging each per-update
2617    /// [`StateDiff`].
2618    ///
2619    /// Later updates observe the effect of earlier ones: two `Slot` writes to the
2620    /// same key record `old → a` then `a → b`. Like
2621    /// [`apply_update`](Self::apply_update) this is synchronous and infallible.
2622    ///
2623    /// # Performance — batched single-lock fast-path
2624    ///
2625    /// Consecutive `Slot`/`SlotDelta` writes are processed holding the backend
2626    /// storage write-guard **once** for the run (the overlay map is lock-free), so
2627    /// a bulk slot seed pays one lock acquisition instead of one read + one write
2628    /// lock per slot. Apply order is preserved: when an `Account`/`BalanceDelta`/
2629    /// `Purge` update is reached the guard is dropped first (those take the
2630    /// `accounts()` / `storage()` locks themselves — holding the storage
2631    /// write-guard across them would deadlock the non-reentrant `RwLock`), the
2632    /// update is processed via [`apply_update`](Self::apply_update), then the guard
2633    /// is lazily re-acquired on the next slot run. The result is byte-identical to
2634    /// folding [`apply_update`](Self::apply_update) over the batch.
2635    ///
2636    /// # Warning — relative updates can be skipped
2637    ///
2638    /// See [`apply_update`](Self::apply_update): a cold relative update is dropped,
2639    /// not applied, and is invisible to [`StateDiff::is_empty`] /
2640    /// [`StateDiff::len`]. After a batch with relative updates, check
2641    /// [`StateDiff::has_skipped`].
2642    pub fn apply_updates(&mut self, updates: &[StateUpdate]) -> StateDiff {
2643        if !updates.is_empty() {
2644            self.bump_snapshot_generation();
2645        }
2646        let mut diff = StateDiff::default();
2647        let mut i = 0;
2648        while i < updates.len() {
2649            match &updates[i] {
2650                // A run of consecutive slot writes: process them under a single
2651                // held storage write-guard, then advance past the run.
2652                StateUpdate::Slot { .. } | StateUpdate::SlotDelta { .. } => {
2653                    let run_end = updates[i..]
2654                        .iter()
2655                        .position(|u| {
2656                            !matches!(u, StateUpdate::Slot { .. } | StateUpdate::SlotDelta { .. })
2657                        })
2658                        .map(|off| i + off)
2659                        .unwrap_or(updates.len());
2660                    self.apply_slot_run(&updates[i..run_end], &mut diff);
2661                    i = run_end;
2662                }
2663                // Account / BalanceDelta / Purge: no held guard (they take their
2664                // own locks), so route through the single-update primitive.
2665                _ => {
2666                    diff.merge(self.apply_update(&updates[i]));
2667                    i += 1;
2668                }
2669            }
2670        }
2671        diff
2672    }
2673
2674    /// Apply a run of consecutive `Slot`/`SlotDelta` updates under one held backend
2675    /// storage write-guard (§16.9.2), merging each change into `diff`.
2676    ///
2677    /// The backend storage guard is acquired once for the whole run; overlay access
2678    /// is lock-free (`self.db.cache.accounts`). The old-value read stays
2679    /// `account_state`-aware (matching [`cached_storage_value`](Self::cached_storage_value)):
2680    /// for an overlay account whose slot is absent, a `StorageCleared`/`NotExisting`
2681    /// state reads ZERO and the backend is **not** consulted. Behavior is identical
2682    /// to applying each update via [`apply_update`](Self::apply_update); the
2683    /// `apply_updates_batched_equals_sequential` test pins this.
2684    fn apply_slot_run(&mut self, run: &[StateUpdate], diff: &mut StateDiff) {
2685        // Borrow the two layers as disjoint fields: the backend storage guard
2686        // (layer 2) held for the whole run, and the overlay accounts map (layer 1,
2687        // lock-free). Base invalidation is deferred until after the guard is
2688        // dropped (it needs `&mut self`): collect the layer-2 addresses written
2689        // here and mark them dirty below.
2690        let mut dirtied: Vec<Address> = Vec::new();
2691        let overlay = &mut self.db.cache.accounts;
2692        let mut storage = self.blockchain_db.storage().write();
2693
2694        for update in run {
2695            // Resolve `(address, slot, old, new)` for the write; a cold SlotDelta
2696            // is skipped here (write nothing). `old` is the `account_state`-aware
2697            // read (overlay ▸ cleared-as-ZERO ▸ backend), reused for both the write
2698            // gate and the change record so each slot is read at most once.
2699            let (address, slot, old, new) = match update {
2700                StateUpdate::Slot {
2701                    address,
2702                    slot,
2703                    value,
2704                } => {
2705                    let old = read_slot_account_state_aware(overlay, &storage, *address, *slot)
2706                        .unwrap_or(U256::ZERO);
2707                    (*address, *slot, old, *value)
2708                }
2709                StateUpdate::SlotDelta {
2710                    address,
2711                    slot,
2712                    delta,
2713                } => match read_slot_account_state_aware(overlay, &storage, *address, *slot) {
2714                    // Hot: apply the saturating delta to the value already read.
2715                    Some(current) => (*address, *slot, current, delta.apply(current)),
2716                    // Cold: skip and surface (write nothing).
2717                    None => {
2718                        diff.skipped.push(SkippedDelta {
2719                            address: *address,
2720                            slot: *slot,
2721                            delta: *delta,
2722                        });
2723                        continue;
2724                    }
2725                },
2726                // The caller only ever hands this method slot updates.
2727                _ => unreachable!("apply_slot_run only processes Slot/SlotDelta"),
2728            };
2729
2730            write_slot_into(overlay, &mut storage, address, slot, new);
2731            // Layer 2 was written for this address → it must be re-folded into the
2732            // memoized base. Mirrors `write_slot_through`'s `mark_base_dirty`.
2733            dirtied.push(address);
2734            if old != new {
2735                diff.slots.push(SlotChange {
2736                    address,
2737                    slot,
2738                    old,
2739                    new,
2740                });
2741            }
2742        }
2743
2744        // Drop the storage write-guard before taking `&mut self` for invalidation.
2745        drop(storage);
2746        for address in dirtied {
2747            self.mark_base_dirty(address);
2748        }
2749    }
2750
2751    /// Write-through a single storage slot (§5.1). Returns a [`SlotChange`] iff
2752    /// the slot's value actually changes.
2753    fn apply_slot(&mut self, address: Address, slot: U256, value: U256) -> Option<SlotChange> {
2754        // Old value: overlay ▸ backend ▸ None (treated as ZERO).
2755        let old = self
2756            .cached_storage_value(address, slot)
2757            .unwrap_or(U256::ZERO);
2758
2759        self.write_slot_through(address, slot, value);
2760
2761        // Record only an actual change.
2762        (old != value).then_some(SlotChange {
2763            address,
2764            slot,
2765            old,
2766            new: value,
2767        })
2768    }
2769
2770    /// The single dual-layer slot write path (§5.1), shared by [`apply_slot`],
2771    /// the [`StateUpdate::SlotDelta`] handler, and [`modify_slot`](Self::modify_slot).
2772    ///
2773    /// Backend (layer 2) is always written; the overlay (layer 1) is written only
2774    /// if an overlay account already exists. A new overlay account is never
2775    /// materialized: that preserves the layer-2-only invariant (a fresh
2776    /// `StorageCleared` overlay account would read missing slots as ZERO and could
2777    /// shadow later RPC reads), and an absent overlay entry falls through to the
2778    /// backend on reads so the backend write is authoritative.
2779    fn write_slot_through(&mut self, address: Address, slot: U256, value: U256) {
2780        // Backend (layer 2): always write.
2781        {
2782            let mut storage = self.blockchain_db.storage().write();
2783            storage.entry(address).or_default().insert(slot, value);
2784        }
2785
2786        // Overlay (layer 1): write only if an overlay account already exists.
2787        if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
2788            db_account.storage.insert(slot, value);
2789        }
2790
2791        // Layer 2 changed → invalidate the memoized base for this address (D2:
2792        // over-invalidation when also shadowed by layer 1 is safe).
2793        self.mark_base_dirty(address);
2794    }
2795
2796    /// Read-modify-write one storage slot through a caller-supplied transform.
2797    ///
2798    /// The general closure escape hatch behind [`StateUpdate::SlotDelta`] (the
2799    /// data-level form flows through [`apply_update`](Self::apply_update); this is
2800    /// for arbitrary transforms). `f` is called with the current cached value
2801    /// (overlay ▸ backend ▸ `None` when the slot is cold) and decides the new
2802    /// value:
2803    ///
2804    /// - `Some(new)` writes `new` through both layers (the same write path as
2805    ///   [`StateUpdate::Slot`]) and returns a [`SlotChange`] iff it changed
2806    ///   (`old.unwrap_or(ZERO) != new`);
2807    /// - `None` writes nothing and returns `None`.
2808    ///
2809    /// The caller owns the cold/overflow policy. To skip cold slots (the
2810    /// cold-aware read-modify-write rule), map through the `Option`:
2811    /// `|cur| cur.map(|v| v.saturating_add(amount))` leaves a cold slot untouched.
2812    /// To write an absolute value regardless, ignore the argument: `|_| Some(v)`.
2813    ///
2814    /// ```no_run
2815    /// # use alloy_primitives::{Address, U256};
2816    /// # fn example(cache: &mut evm_fork_cache::cache::EvmCache) {
2817    /// let token = Address::repeat_byte(0x01);
2818    /// let slot = U256::from(0);
2819    /// // Saturating +100, but only if the slot is already hot.
2820    /// let change = cache.modify_slot(token, slot, |cur| cur.map(|v| v.saturating_add(U256::from(100))));
2821    /// # let _ = change;
2822    /// # }
2823    /// ```
2824    pub fn modify_slot(
2825        &mut self,
2826        address: Address,
2827        slot: U256,
2828        f: impl FnOnce(Option<U256>) -> Option<U256>,
2829    ) -> Option<SlotChange> {
2830        let current = self.cached_storage_value(address, slot);
2831        let new = f(current)?;
2832
2833        self.bump_snapshot_generation();
2834        self.write_slot_through(address, slot, new);
2835
2836        let old = current.unwrap_or(U256::ZERO);
2837        (old != new).then_some(SlotChange {
2838            address,
2839            slot,
2840            old,
2841            new,
2842        })
2843    }
2844
2845    /// Read-modify-write an account's native balance through a caller-supplied
2846    /// transform.
2847    ///
2848    /// The closure analog of [`StateUpdate::BalanceDelta`] (the data-level form
2849    /// flows through [`apply_update`](Self::apply_update); this is for arbitrary
2850    /// transforms). `f` is called with the account's current native balance
2851    /// (overlay ▸ backend ▸ `None` when the account is absent from **both**
2852    /// layers) and decides the new balance:
2853    ///
2854    /// - `Some(new)` writes `new` through both layers — backend always, overlay
2855    ///   only if an overlay account already exists — preserving the account's
2856    ///   nonce and code, and returns an [`AccountChange`] (balance only) iff the
2857    ///   balance changed;
2858    /// - `None` writes nothing (no account is materialized) and returns `None`.
2859    ///
2860    /// "Cold" for a balance is the account being absent from both layers — or
2861    /// present in the overlay as revm `NotExisting` (absent to the EVM), which the
2862    /// internal account read also treats as cold, mirroring `DbAccount::info()`.
2863    /// To skip cold accounts, map through the `Option`:
2864    /// `|cur| cur.map(|v| v.saturating_add(amount))`.
2865    ///
2866    /// ```no_run
2867    /// # use alloy_primitives::{Address, U256};
2868    /// # fn example(cache: &mut evm_fork_cache::cache::EvmCache) {
2869    /// let acct = Address::repeat_byte(0x01);
2870    /// // Saturating +100, but only if the account's balance is already known.
2871    /// let change = cache.modify_account_balance(acct, |cur| cur.map(|v| v.saturating_add(U256::from(100))));
2872    /// # let _ = change;
2873    /// # }
2874    /// ```
2875    pub fn modify_account_balance(
2876        &mut self,
2877        address: Address,
2878        f: impl FnOnce(Option<U256>) -> Option<U256>,
2879    ) -> Option<AccountChange> {
2880        // Load the full info from the cached layers only (overlay ▸ backend); the
2881        // account is "cold" when absent from both.
2882        let base = self.loaded_account_info(address);
2883        let current_balance = base.as_ref().map(|info| info.balance);
2884        let new_balance = f(current_balance)?;
2885
2886        // The closure asked to write `new_balance`. Materialize from the loaded
2887        // base (or a default if the caller chose to write a cold account).
2888        let mut info = base.unwrap_or_default();
2889        let old_balance = info.balance;
2890        info.balance = new_balance;
2891        self.write_account_info_through(address, info);
2892
2893        (old_balance != new_balance).then_some(AccountChange {
2894            address,
2895            balance: Some((old_balance, new_balance)),
2896            nonce: None,
2897            code_hash: None,
2898        })
2899    }
2900
2901    /// Apply a relative (saturating) [`SlotDelta`] to an account's native balance
2902    /// (§16.5). Cold-aware:
2903    ///
2904    /// - `Ok(Some(change))` — present account, balance changed;
2905    /// - `Ok(None)` — present account, balance unchanged (e.g. `Sub` from 0);
2906    /// - `Err(skipped)` — cold account (absent from both layers): nothing applied,
2907    ///   nothing materialized.
2908    fn apply_balance_delta(
2909        &mut self,
2910        address: Address,
2911        delta: SlotDelta,
2912    ) -> std::result::Result<Option<AccountChange>, SkippedBalanceDelta> {
2913        let Some(mut info) = self.loaded_account_info(address) else {
2914            // Cold: applying a delta against an unknown balance would corrupt it,
2915            // and materializing a default account would mask the real on-chain one.
2916            return Err(SkippedBalanceDelta { address, delta });
2917        };
2918
2919        let old_balance = info.balance;
2920        let new_balance = delta.apply(old_balance);
2921        info.balance = new_balance;
2922        self.write_account_info_through(address, info);
2923
2924        Ok((old_balance != new_balance).then_some(AccountChange {
2925            address,
2926            balance: Some((old_balance, new_balance)),
2927            nonce: None,
2928            code_hash: None,
2929        }))
2930    }
2931
2932    /// Load an account's `AccountInfo` from the cached layers only (overlay ▸
2933    /// backend), without touching RPC. `None` when the account is absent from
2934    /// both layers.
2935    fn loaded_account_info(&self, address: Address) -> Option<AccountInfo> {
2936        let mut info = if let Some(a) = self.db.cache.accounts.get(&address) {
2937            // Mirror revm `DbAccount::info()` / `basic_ref`: a NotExisting overlay
2938            // account is absent to the EVM (returns None) and does NOT fall through
2939            // to the backend. Without this, a relative balance update / partial
2940            // patch would compute against a stale `info` the EVM never sees.
2941            if matches!(a.account_state, AccountState::NotExisting) {
2942                return None;
2943            }
2944            a.info.clone()
2945        } else {
2946            self.blockchain_db
2947                .accounts()
2948                .read()
2949                .get(&address)
2950                .cloned()?
2951        };
2952        // Normalize like revm `insert_contract`: a ZERO code_hash denotes empty
2953        // code -> KECCAK_EMPTY. Done at load time so a patch's `old_code_hash`
2954        // matches what `write_account_info_through` stores (a self-consistent diff,
2955        // no phantom/under-reported code_hash change).
2956        if info.code_hash == B256::ZERO {
2957            info.code_hash = revm::primitives::KECCAK_EMPTY;
2958        }
2959        Some(info)
2960    }
2961
2962    /// Write an `AccountInfo` through both layers, mirroring the slot policy:
2963    /// backend (layer 2) always; overlay (layer 1) only if an overlay account
2964    /// already exists (never materialize a new overlay account).
2965    fn write_account_info_through(&mut self, address: Address, mut info: AccountInfo) {
2966        // Normalize the code hash the way revm's `insert_contract` (applied on the
2967        // overlay write below) does, so both layers store an identical hash: a ZERO
2968        // code_hash denotes empty code → KECCAK_EMPTY. Otherwise the overlay would
2969        // hold KECCAK_EMPTY while the backend kept ZERO for the same account.
2970        if info.code_hash == B256::ZERO {
2971            info.code_hash = revm::primitives::KECCAK_EMPTY;
2972        }
2973        let overlay_present = self.db.cache.accounts.contains_key(&address);
2974        {
2975            let mut accounts = self.blockchain_db.accounts().write();
2976            accounts.insert(address, info.clone());
2977        }
2978        if overlay_present {
2979            self.db.insert_account_info(address, info);
2980        }
2981        // Layer-2 account info changed → invalidate the memoized base for this
2982        // address (D2: over-invalidation when also in layer 1 is safe).
2983        self.mark_base_dirty(address);
2984    }
2985
2986    /// Apply a partial [`AccountPatch`] write-through (§5.2). Returns an
2987    /// [`AccountChange`] iff any field actually changes.
2988    fn apply_account_patch(
2989        &mut self,
2990        address: Address,
2991        patch: &AccountPatch,
2992        allow_cold_upsert: bool,
2993    ) -> std::result::Result<Option<AccountChange>, SkippedAccountPatch> {
2994        // 1. Current info from the cached layers only (overlay ▸ backend). No RPC:
2995        //    apply is a write, not a fetch. A partial patch on a cold account is
2996        //    skipped unless the caller explicitly chose AccountUpsert.
2997        let mut info = match self.loaded_account_info(address) {
2998            Some(info) => info,
2999            None if account_patch_is_empty(patch) => return Ok(None),
3000            None if allow_cold_upsert => AccountInfo::default(),
3001            None => {
3002                return Err(SkippedAccountPatch {
3003                    address,
3004                    patch: patch.clone(),
3005                });
3006            }
3007        };
3008
3009        let old_balance = info.balance;
3010        let old_nonce = info.nonce;
3011        let old_code_hash = info.code_hash;
3012
3013        // 2. Apply each `Some` field.
3014        if let Some(balance) = patch.balance {
3015            info.balance = balance;
3016        }
3017        if let Some(nonce) = patch.nonce {
3018            info.nonce = nonce;
3019        }
3020        if let Some(code) = &patch.code {
3021            let bytecode = Bytecode::new_raw(code.clone());
3022            info.code_hash = bytecode.hash_slow();
3023            info.code = Some(bytecode);
3024        }
3025
3026        // 3. Compute the change first. A no-op patch (every field equals the
3027        //    loaded base) must NOT write either layer — otherwise an all-`None`
3028        //    patch on an absent address would insert `AccountInfo::default()` into
3029        //    the shared backend (masking a future RPC fetch) while returning an
3030        //    empty diff. Only a real field change materializes anything.
3031        let change = AccountChange {
3032            address,
3033            balance: (old_balance != info.balance).then_some((old_balance, info.balance)),
3034            nonce: (old_nonce != info.nonce).then_some((old_nonce, info.nonce)),
3035            code_hash: (old_code_hash != info.code_hash).then_some((old_code_hash, info.code_hash)),
3036        };
3037        if change.balance.is_none() && change.nonce.is_none() && change.code_hash.is_none() {
3038            return Ok(None);
3039        }
3040
3041        // 4. Write-through, mirroring the slot policy: backend always; overlay
3042        //    only if an overlay account already exists (do not materialize one).
3043        self.write_account_info_through(address, info);
3044
3045        Ok(Some(change))
3046    }
3047
3048    /// Dispatch a [`PurgeScope`] to the matching layer logic (§5.3), returning a
3049    /// [`PurgeRecord`] of what was removed from each layer.
3050    fn apply_purge(&mut self, address: Address, scope: &PurgeScope) -> PurgeRecord {
3051        match scope {
3052            PurgeScope::Account => {
3053                let (slots_removed, account_removed) = self.purge_account_inner(address);
3054                PurgeRecord {
3055                    address,
3056                    scope: PurgeScope::Account,
3057                    slots_removed,
3058                    account_removed,
3059                }
3060            }
3061            PurgeScope::AllStorage => {
3062                let slots_removed = self.purge_contract_storage_inner(address);
3063                PurgeRecord {
3064                    address,
3065                    scope: PurgeScope::AllStorage,
3066                    slots_removed,
3067                    account_removed: false,
3068                }
3069            }
3070            PurgeScope::Slots(slots) => {
3071                let slots_removed = self.purge_contract_slots_inner(address, slots);
3072                PurgeRecord {
3073                    address,
3074                    scope: PurgeScope::Slots(slots.clone()),
3075                    slots_removed,
3076                    account_removed: false,
3077                }
3078            }
3079        }
3080    }
3081
3082    /// Set (or replace) the batch storage fetcher.
3083    ///
3084    /// This is the seam the freshness controller and tests use to drive
3085    /// re-verification without a live provider: a stubbed
3086    /// [`StorageBatchFetchFn`] can be injected over a mocked-provider cache.
3087    /// Production callers can also inject their own transport, retry, batching,
3088    /// or rate-limiting strategy here. Once replaced, the cache's
3089    /// [`StorageBatchConfig`] no longer controls batching; the custom fetcher is
3090    /// responsible for honoring the [`StorageBatchFetchFn`] contract.
3091    pub fn set_storage_batch_fetcher(&mut self, f: StorageBatchFetchFn) {
3092        self.storage_batch_fetcher = Some(f);
3093    }
3094
3095    /// Set (or replace) the account/root proof fetcher.
3096    ///
3097    /// This is the seam account-target resyncs and account-level freshness use to
3098    /// drive `eth_getProof` fetches without a live provider: a stubbed
3099    /// [`AccountProofFetchFn`] can be injected over a mocked-provider cache,
3100    /// mirroring [`set_storage_batch_fetcher`](Self::set_storage_batch_fetcher).
3101    pub fn set_account_proof_fetcher(&mut self, f: AccountProofFetchFn) {
3102        self.account_proof_fetcher = Some(f);
3103    }
3104
3105    /// Set (or replace) the block state-diff fetcher.
3106    ///
3107    /// This is the seam trace-backed reactive resync uses to resolve matching
3108    /// targets from one block-level debug trace before falling back to storage or
3109    /// account proof point reads.
3110    pub fn set_block_state_diff_fetcher(&mut self, f: BlockStateDiffFetchFn) {
3111        self.block_state_diff_fetcher = Some(f);
3112    }
3113
3114    /// Set (or replace) the bulk account-fields fetcher.
3115    ///
3116    /// This is the seam [`verify_code_seeds`](Self::verify_code_seeds) (and
3117    /// the cold-start `verify_code` phase) reads through: a stubbed
3118    /// [`AccountFieldsFetchFn`] can be injected over a mocked-provider cache,
3119    /// mirroring [`set_storage_batch_fetcher`](Self::set_storage_batch_fetcher).
3120    pub fn set_account_fields_fetcher(&mut self, f: AccountFieldsFetchFn) {
3121        self.account_fields_fetcher = Some(f);
3122    }
3123
3124    /// The installed bulk account-fields fetcher, if any.
3125    ///
3126    /// `Some` on provider-backed caches (default-wired to
3127    /// [`fetch_account_fields_bulk`](crate::bulk_storage::fetch_account_fields_bulk));
3128    /// `None` on [`from_backend`](Self::from_backend) caches until one is
3129    /// installed via
3130    /// [`set_account_fields_fetcher`](Self::set_account_fields_fetcher).
3131    pub fn account_fields_fetcher(&self) -> Option<&AccountFieldsFetchFn> {
3132        self.account_fields_fetcher.as_ref()
3133    }
3134
3135    /// Return the currently-cached value for a storage slot, if any.
3136    ///
3137    /// Mirrors what the EVM would `SLOAD` from the cached layers (it never touches
3138    /// RPC, unlike [`read_storage_slot`](Self::read_storage_slot)):
3139    ///
3140    /// 1. The CacheDB overlay (layer 1) wins: if the overlay account holds the
3141    ///    slot, return it.
3142    /// 2. Match revm's `CacheDB::storage_ref`: if the overlay account exists but
3143    ///    does **not** hold the slot, and its `account_state` is `StorageCleared`
3144    ///    or `NotExisting`, the live EVM reads the slot as ZERO and never consults
3145    ///    the backend — so return `Some(U256::ZERO)`, **not** the (shadowed)
3146    ///    backend value. Returning the backend value here would let a
3147    ///    `SlotDelta`/`modify_slot` compute a delta against a base the EVM never
3148    ///    sees (silent corruption) and would mis-record `apply_slot`'s `old`.
3149    /// 3. Otherwise fall through to the BlockchainDb backend (layer 2); `None` when
3150    ///    neither layer has seen the slot.
3151    pub fn cached_storage_value(&self, address: Address, slot: U256) -> Option<U256> {
3152        if let Some(db_account) = self.db.cache.accounts.get(&address) {
3153            if let Some(value) = db_account.storage.get(&slot) {
3154                return Some(*value);
3155            }
3156            // A StorageCleared / NotExisting overlay account reads a missing slot
3157            // as ZERO and never consults the backend (matching the EVM SLOAD).
3158            if matches!(
3159                db_account.account_state,
3160                AccountState::StorageCleared | AccountState::NotExisting
3161            ) {
3162                return Some(U256::ZERO);
3163            }
3164        }
3165        let storage = self.blockchain_db.storage().read();
3166        storage.get(&address).and_then(|s| s.get(&slot).copied())
3167    }
3168
3169    /// Re-fetch the given slots via the batch fetcher, compare to the currently
3170    /// cached values, and inject the ones that changed.
3171    ///
3172    /// For each slot whose freshly-fetched value differs from the cached value,
3173    /// the fresh value is written into the cache via
3174    /// [`inject_storage_batch_fresh`](Self::inject_storage_batch_fresh) and a
3175    /// [`SlotChange`] is recorded. Slots that are unchanged, or that the fetcher
3176    /// fails to return, are left as-is. Returns the set of changed slots.
3177    ///
3178    /// Requires a batch fetcher (set at construction or via
3179    /// [`set_storage_batch_fetcher`](Self::set_storage_batch_fetcher)); errors if
3180    /// none is available. This is the synchronous main-thread primitive; the
3181    /// background validator performs the equivalent comparison against a snapshot.
3182    pub fn verify_slots(&mut self, slots: &[(Address, U256)]) -> Result<Vec<SlotChange>> {
3183        Ok(self.verify_slots_inner(slots)?.0)
3184    }
3185
3186    /// Shared implementation for [`verify_slots`](Self::verify_slots) and the
3187    /// pipeline's reconcile path. Returns `(changed, fetched_ok)` where
3188    /// `fetched_ok` is the number of requested slots the fetcher returned a value
3189    /// for (failed per-slot fetches are skipped, not errors). Errors only when no
3190    /// batch fetcher is configured.
3191    fn verify_slots_inner(
3192        &mut self,
3193        slots: &[(Address, U256)],
3194    ) -> Result<(Vec<SlotChange>, usize)> {
3195        let (changed, outcomes) = self.verify_slots_core(slots)?;
3196        let fetched_ok = outcomes
3197            .iter()
3198            .filter(|o| matches!(o.fetch, SlotFetch::Value(_) | SlotFetch::Zero))
3199            .count();
3200        Ok((changed, fetched_ok))
3201    }
3202
3203    /// Classify a single fetched slot value into a [`SlotFetch`].
3204    ///
3205    /// This is purely the *fetch* classification (`Value` / `Zero` /
3206    /// `FetchFailed`); it is independent of change detection, which compares the
3207    /// fetched value to the cached baseline separately. A non-zero `Ok` is
3208    /// [`SlotFetch::Value`], a genuine `Ok(0)` is [`SlotFetch::Zero`], and an
3209    /// `Err` is [`SlotFetch::FetchFailed`] carrying the error string.
3210    ///
3211    /// Shared with the cold-start probe phase
3212    /// ([`execute_cold_start_round`](Self::execute_cold_start_round)) so the
3213    /// single classification is reused rather than duplicated.
3214    pub(crate) fn classify(fetched: StorageFetchResult<U256>) -> SlotFetch {
3215        match fetched {
3216            Ok(v) if v != U256::ZERO => SlotFetch::Value(v),
3217            Ok(_) => SlotFetch::Zero,
3218            Err(e) => SlotFetch::FetchFailed {
3219                reason: e.to_string(),
3220            },
3221        }
3222    }
3223
3224    /// Core slot-verification loop shared by [`verify_slots_inner`](Self::verify_slots_inner)
3225    /// and [`verify_slots_with_outcomes`](Self::verify_slots_with_outcomes).
3226    ///
3227    /// Fetches every slot via the batch fetcher and, for each slot, performs two
3228    /// **independent** reads of the same fetched value:
3229    ///
3230    /// 1. *Fetch classification* — every slot (including failed ones) produces one
3231    ///    [`SlotOutcome`] via [`classify`](Self::classify): `Value` / `Zero` /
3232    ///    `FetchFailed`.
3233    /// 2. *Change detection* — a successfully-fetched value that differs from the
3234    ///    cached baseline (`old`, defaulting to `ZERO` for an unseen slot) is
3235    ///    injected via [`inject_storage_batch_fresh`](Self::inject_storage_batch_fresh)
3236    ///    and recorded as a [`SlotChange`].
3237    ///
3238    /// These two reads are deliberately not collapsed: a genuine `Ok(0)` on a slot
3239    /// whose cached value was also `0` yields [`SlotFetch::Zero`] **and** no
3240    /// `SlotChange`. The returned `outcomes` vec has exactly one entry per
3241    /// requested slot. An empty `slots` input short-circuits to empty results
3242    /// without requiring a fetcher; otherwise a missing fetcher is an error.
3243    fn verify_slots_core(
3244        &mut self,
3245        slots: &[(Address, U256)],
3246    ) -> Result<(Vec<SlotChange>, Vec<SlotOutcome>)> {
3247        if slots.is_empty() {
3248            return Ok((Vec::new(), Vec::new()));
3249        }
3250        let fetcher = self
3251            .storage_batch_fetcher
3252            .as_ref()
3253            .ok_or(CacheError::MissingStorageBatchFetcher)?
3254            .clone();
3255
3256        // Snapshot the cached values before fetching so we compare against a
3257        // stable baseline.
3258        let cached: HashMap<(Address, U256), Option<U256>> = slots
3259            .iter()
3260            .map(|&(addr, slot)| ((addr, slot), self.cached_storage_value(addr, slot)))
3261            .collect();
3262
3263        let results = (fetcher)(slots.to_vec(), self.block);
3264
3265        let mut changed = Vec::new();
3266        let mut outcomes = Vec::with_capacity(results.len());
3267        let mut to_inject = Vec::new();
3268        for (addr, slot, fetched) in results {
3269            // Read 1: classify the fetch outcome for every slot, failed or not.
3270            let fetch = Self::classify(match &fetched {
3271                Ok(v) => Ok(*v),
3272                Err(e) => Err(StorageFetchError::custom(e.to_string())),
3273            });
3274            outcomes.push(SlotOutcome {
3275                address: addr,
3276                slot,
3277                fetch,
3278            });
3279
3280            // Read 2: change detection, independent of the classification above.
3281            let fresh = match fetched {
3282                Ok(value) => value,
3283                Err(e) => {
3284                    debug!(%addr, %slot, error = %e, "verify_slots: fetch failed, skipping slot");
3285                    continue;
3286                }
3287            };
3288            // A slot the cache never saw is treated as old = ZERO (the value a
3289            // sim would have read), so a non-zero fresh value counts as a change.
3290            let old = cached
3291                .get(&(addr, slot))
3292                .copied()
3293                .flatten()
3294                .unwrap_or(U256::ZERO);
3295            if fresh != old {
3296                to_inject.push((addr, slot, fresh));
3297                changed.push(SlotChange {
3298                    address: addr,
3299                    slot,
3300                    old,
3301                    new: fresh,
3302                });
3303            }
3304        }
3305
3306        if !to_inject.is_empty() {
3307            self.inject_storage_batch_fresh(&to_inject);
3308        }
3309        Ok((changed, outcomes))
3310    }
3311
3312    /// Like [`verify_slots`](Self::verify_slots), but additionally returns one
3313    /// [`SlotOutcome`] per requested slot (including slots the fetcher failed to
3314    /// return), classified as `Value` / `Zero` / `FetchFailed`.
3315    ///
3316    /// This is the per-slot surface the cold-start driver consumes: it
3317    /// distinguishes a genuine on-chain zero from a fetch failure for every slot,
3318    /// closing the archive-miss gap. It is a pure alias of
3319    /// [`verify_slots_core`](Self::verify_slots_core) and shares its injection
3320    /// behaviour with [`verify_slots`](Self::verify_slots).
3321    #[cfg(feature = "reactive")]
3322    pub(crate) fn verify_slots_with_outcomes(
3323        &mut self,
3324        slots: &[(Address, U256)],
3325    ) -> Result<(Vec<SlotChange>, Vec<SlotOutcome>)> {
3326        self.verify_slots_core(slots)
3327    }
3328
3329    /// Reconciliation re-read used by [`EventPipeline::reconcile`](crate::events::EventPipeline::reconcile).
3330    ///
3331    /// Like [`verify_slots`](Self::verify_slots) it fetches the requested slots,
3332    /// injects the ones that changed, and returns the changed set — but it is
3333    /// **honest about reachability**: it errors not only when no batch fetcher is
3334    /// configured, but also when a non-empty request could not fetch **any** slot
3335    /// (a total fetch failure — e.g. the default RPC fetcher invoked with no usable
3336    /// runtime, or an unreachable provider). Reconciliation that silently "verified
3337    /// nothing" would be a false all-clear, so it surfaces as an error for the
3338    /// caller to retry. A partially-successful fetch returns `Ok` with whatever
3339    /// changed.
3340    pub fn reconcile_slots(&mut self, slots: &[(Address, U256)]) -> Result<Vec<SlotChange>> {
3341        let (changed, fetched_ok) = self.verify_slots_inner(slots)?;
3342        if !slots.is_empty() && fetched_ok == 0 {
3343            return Err(CacheError::ReconcileFetchFailed {
3344                requested: slots.len(),
3345            });
3346        }
3347        Ok(changed)
3348    }
3349
3350    /// Purge an account fully from both cache layers: its `AccountInfo`
3351    /// (balance/nonce/code hash) **and** all of its storage.
3352    ///
3353    /// Removes `addr` from the CacheDB overlay accounts map, the BlockchainDb
3354    /// accounts map, and the BlockchainDb storage map, so the next access
3355    /// re-fetches a clean account from RPC. This is the account-level
3356    /// counterpart to the storage-only [`purge_contract_storage`](Self::purge_contract_storage):
3357    /// use it when an address is fully volatile (no pinned slots) and even its
3358    /// balance/nonce/code can no longer be trusted.
3359    pub fn purge_account(&mut self, addr: Address) {
3360        // Thin wrapper over the unified purge primitive; the layer logic lives in
3361        // `purge_account_inner` (shared with `apply_update(Purge { Account })`).
3362        let _ = self.apply_update(&StateUpdate::purge(addr, PurgeScope::Account));
3363    }
3364
3365    /// Account-scope purge layer logic. Removes `addr` from the overlay accounts
3366    /// map, the backend accounts map, and the backend storage map. Returns
3367    /// `(backend_slots_removed, account_removed)` where `account_removed` is true
3368    /// if an account entry was removed from either account layer.
3369    fn purge_account_inner(&mut self, addr: Address) -> (usize, bool) {
3370        // An account-scope purge also discards any code-seed mark: whatever
3371        // trust state the code carried, the code itself is gone, and the next
3372        // touch refetches authoritative chain state (which is unmarked
3373        // RPC-origin by definition). This is also the documented escape hatch
3374        // for re-seeding after a believed redeploy.
3375        self.code_seeds.remove(&addr);
3376
3377        // Layer 1: CacheDB overlay (accounts + their storage live together).
3378        let overlay_removed = self.db.cache.accounts.remove(&addr).is_some();
3379
3380        // Layer 2: BlockchainDb accounts + storage maps.
3381        let backend_account_removed = self
3382            .blockchain_db
3383            .accounts()
3384            .write()
3385            .remove(&addr)
3386            .is_some();
3387        let backend_storage_removed = self.blockchain_db.storage().write().remove(&addr);
3388        let slots_removed = backend_storage_removed
3389            .map(|slots| slots.len())
3390            .unwrap_or(0);
3391
3392        let account_removed = overlay_removed || backend_account_removed;
3393        if account_removed || slots_removed > 0 {
3394            debug!(
3395                account = %addr,
3396                overlay_removed,
3397                backend_account_removed,
3398                backend_storage_slots = slots_removed,
3399                "purged account from both cache layers"
3400            );
3401        }
3402        // Layer 2 (account + storage) changed for this address → invalidate base.
3403        self.mark_base_dirty(addr);
3404        (slots_removed, account_removed)
3405    }
3406
3407    /// Get the chain ID used for EVM simulations (the `CHAINID` opcode).
3408    pub fn chain_id(&self) -> u64 {
3409        self.chain_id
3410    }
3411
3412    /// Set the chain ID reported to simulations via the `CHAINID` opcode.
3413    ///
3414    /// Prefer setting this at construction through
3415    /// [`EvmCacheBuilder::chain_id`]. This setter exists for cases where the
3416    /// chain ID must change after construction. It takes effect on the next
3417    /// [`snapshot`](Self::snapshot) / `build_evm`; existing
3418    /// snapshots and overlays keep the chain ID captured when they were created.
3419    pub fn set_chain_id(&mut self, chain_id: u64) {
3420        self.chain_id = chain_id;
3421    }
3422
3423    /// Take a low-level, same-thread checkpoint of the CacheDB overlay for
3424    /// in-place restore.
3425    ///
3426    /// Clones the inner [`revm::database::Cache`] (the layer-1 overlay's
3427    /// accounts and storage) only — not the underlying database wrapper or the
3428    /// BlockchainDb backend. Pair with [`restore`](Self::restore) to roll the
3429    /// overlay back on the same `EvmCache` after speculative mutations (this is
3430    /// how the balance-slot scan probes and rewinds).
3431    ///
3432    /// For cross-thread fan-out use [`snapshot`](Self::snapshot)
3433    /// instead: it merges both layers into an `Arc<`[`EvmSnapshot`]`>` that is
3434    /// `Send + Sync` and can be shared with parallel simulators via
3435    /// [`EvmOverlay`].
3436    pub fn checkpoint(&self) -> revm::database::Cache {
3437        self.db.cache.clone()
3438    }
3439
3440    /// Restore the CacheDB overlay from a checkpoint taken with
3441    /// [`checkpoint`](Self::checkpoint).
3442    ///
3443    /// Overwrites the layer-1 overlay wholesale with `checkpoint`, discarding any
3444    /// overlay mutations made since it was taken. The BlockchainDb backend is
3445    /// untouched. This is the in-place counterpart to the cross-thread
3446    /// [`snapshot`](Self::snapshot) / [`EvmOverlay`] path.
3447    pub fn restore(&mut self, checkpoint: revm::database::Cache) {
3448        self.db.cache = checkpoint;
3449    }
3450
3451    /// Create a new session for executing multiple operations.
3452    ///
3453    /// Changes made within the session are only committed to the underlying database
3454    /// when `session.commit()` is called. Dropping the session without calling commit
3455    /// discards all changes made during the session.
3456    pub fn session(&mut self) -> EvmSession<'_> {
3457        EvmSession {
3458            evm: self.build_evm(),
3459        }
3460    }
3461
3462    /// Create an immutable, `Send + Sync` snapshot of the current EVM state for
3463    /// cross-thread fan-out (the copy-on-write two-tier view, Pillar A).
3464    ///
3465    /// Rather than deep-copying both layers, this memoizes the cold layer-2
3466    /// (`BlockchainDb`) index as an `Arc`-shared base — reused as a cheap
3467    /// `Arc::clone` when layer 2 is unchanged, rebuilt copy-on-write only for the
3468    /// addresses that changed — and folds the hot layer-1 (`CacheDB` overlay)
3469    /// delta over it. Layer-1 values shadow the base on reads, reproducing the
3470    /// live cache's layered semantics; the resulting [`EvmSnapshot`] is shared
3471    /// across threads via `Arc`. Its cost tracks *changed* state, not *total*
3472    /// state. (The retained [`snapshot_deep_clone`](Self::snapshot_deep_clone)
3473    /// is the read-equivalent O(total) reference, kept for benchmarking/testing.)
3474    ///
3475    /// Takes `&mut self` because it refreshes and memoizes the base. For cheap
3476    /// same-thread save/restore of just the overlay, prefer
3477    /// [`checkpoint`](Self::checkpoint) / [`restore`](Self::restore) instead.
3478    pub fn snapshot(&mut self) -> Arc<snapshot::EvmSnapshot> {
3479        // 1. Refresh / memoize the cold layer-2 base, then take a cheap Arc handle
3480        //    (O(1) when layer 2 is unchanged since the last snapshot).
3481        self.refresh_base();
3482        let base = Arc::clone(self.base.as_ref().expect("refresh_base sets base"));
3483
3484        // 2. Fold layer 1 (the hot CacheDB overlay) into the snapshot's overlay
3485        //    maps + cleared/not-existing sets, applying the same classification as
3486        //    the legacy flatten (O(layer-1)).
3487        let mut overlay_accounts = HashMap::new();
3488        let mut overlay_storage = HashMap::new();
3489        let mut overlay_code_by_hash = HashMap::new();
3490        let mut storage_cleared = std::collections::HashSet::new();
3491        let mut accounts_not_existing = std::collections::HashSet::new();
3492        for (addr, db_account) in &self.db.cache.accounts {
3493            let not_existing = matches!(db_account.account_state, AccountState::NotExisting);
3494            let cleared =
3495                not_existing || matches!(db_account.account_state, AccountState::StorageCleared);
3496
3497            // Account info. Mirror revm `DbAccount::info()` / `loaded_account_info`:
3498            // a NotExisting overlay account is absent to the EVM (`basic` returns
3499            // None), so it must NOT contribute info/code to the overlay — and
3500            // `accounts_not_existing` makes the read short-circuit to None before
3501            // ever consulting the base.
3502            if not_existing {
3503                accounts_not_existing.insert(*addr);
3504            } else {
3505                if let Some(code) = &db_account.info.code {
3506                    overlay_code_by_hash.insert(db_account.info.code_hash, code.clone());
3507                }
3508                overlay_accounts.insert(*addr, db_account.info.clone());
3509            }
3510
3511            // Storage. A StorageCleared/NotExisting account's storage is locally
3512            // complete: the overlay holds ONLY its own slots (so a cleared account
3513            // ALWAYS gets an `overlay_storage` entry, possibly empty), an absent
3514            // slot reads ZERO via `storage_cleared`, and the base is never consulted
3515            // for it. A non-cleared overlay account contributes its slots; absent
3516            // slots fall through to the base on a read.
3517            if cleared {
3518                storage_cleared.insert(*addr);
3519                let account_storage: HashMap<U256, U256> =
3520                    db_account.storage.iter().map(|(k, v)| (*k, *v)).collect();
3521                overlay_storage.insert(*addr, account_storage);
3522            } else if !db_account.storage.is_empty() {
3523                let account_storage = overlay_storage.entry(*addr).or_default();
3524                for (slot, value) in &db_account.storage {
3525                    account_storage.insert(*slot, *value);
3526                }
3527            }
3528        }
3529
3530        Arc::new(snapshot::EvmSnapshot {
3531            base,
3532            overlay_accounts,
3533            overlay_storage,
3534            overlay_code_by_hash,
3535            storage_cleared,
3536            accounts_not_existing,
3537            block_hashes: HashMap::new(),
3538            block_number: self.block_number,
3539            basefee: self.basefee,
3540            coinbase: self.coinbase,
3541            prevrandao: self.prevrandao,
3542            gas_limit: self.block_gas_limit,
3543            chain_id: self.chain_id,
3544            timestamp: self.timestamp_override,
3545            spec_id: self.spec_id,
3546            shared_memory_capacity: self.shared_memory_capacity,
3547        })
3548    }
3549
3550    /// Force the next [`snapshot`](Self::snapshot) to rebuild the
3551    /// memoized copy-on-write base from scratch (Pillar A).
3552    ///
3553    /// The crate's own mutators keep the base honest automatically. This is the
3554    /// **escape-hatch re-honest hook**: call it after writing layer 2 directly
3555    /// through [`unchecked_blockchain_db`](Self::unchecked_blockchain_db) or
3556    /// [`unchecked_backend`](Self::unchecked_backend) — those bypass the write
3557    /// funnel, and in-place changes at unchanged cardinality are invisible to the
3558    /// snapshot growth scan.
3559    /// That includes overwriting an existing storage slot and changing an existing
3560    /// account's info/code/balance without adding a new account. Lazy RPC-populated
3561    /// data does not need this call because it only appends accounts/slots, which
3562    /// the growth scan catches.
3563    ///
3564    /// When using `SharedBackend::insert_or_update_*` through
3565    /// [`unchecked_backend`](Self::unchecked_backend), remember those helpers only
3566    /// enqueue a background update. Synchronize/read back the update through
3567    /// `SharedBackend` before the next snapshot; `invalidate_snapshot_base` alone
3568    /// is not a backend-handler synchronization point. Once the direct write is
3569    /// present, calling this before the next snapshot guarantees it reflects that
3570    /// write rather than a stale memoized value. Over-invalidation is always safe
3571    /// (Decision D2); the only cost is one full base rebuild on the next snapshot.
3572    pub fn invalidate_snapshot_base(&mut self) {
3573        self.invalidate_base();
3574    }
3575
3576    /// Refresh the memoized cold layer-2 [`BaseState`](snapshot::BaseState),
3577    /// reusing the previous `Arc` wherever layer 2 is unchanged (Pillar A).
3578    ///
3579    /// Called at the top of [`snapshot`](Self::snapshot). It never
3580    /// mutates an `Arc<BaseState>` that may already be shared with a live
3581    /// snapshot: on any change it builds a *new* `BaseState` that shares the `Arc`
3582    /// handles of unchanged accounts and rebuilds only the changed ones
3583    /// (copy-on-write).
3584    ///
3585    /// Algorithm (see `docs/phase-5-spec.md` §2.3):
3586    /// 1. **Full rebuild** when there is no base yet or `base_full_rebuild` is set
3587    ///    (`set_block` / re-pin replaced layer 2): flatten all of layer 2.
3588    /// 2. **Detect uncontrolled growth**: a lazy RPC fetch / prefetch can write
3589    ///    layer 2 from inside `foundry-fork-db`, bypassing our write funnel. An
3590    ///    `O(accounts)` length-scan over the current layer-2 storage/accounts marks
3591    ///    any address whose slot count differs from the recorded length, or any
3592    ///    account absent from the base, as dirty.
3593    /// 3. **Nothing dirty** → reuse the existing `Arc<BaseState>` unchanged (the
3594    ///    common hot-loop case; the base side of `snapshot` is then O(1)).
3595    /// 4. **Some addresses dirty** → build a new `BaseState` sharing the `Arc`s of
3596    ///    unchanged accounts and rebuilding only the dirty ones.
3597    fn refresh_base(&mut self) {
3598        // Case 1: full rebuild.
3599        if self.base.is_none() || self.base_full_rebuild {
3600            self.base = Some(Arc::new(self.build_base_full()));
3601            self.base_dirty.clear();
3602            self.base_full_rebuild = false;
3603            return;
3604        }
3605
3606        // Case 2: detect uncontrolled layer-2 growth via an O(accounts) length scan
3607        // (NOT an O(slots) value scan). Any address whose slot count changed, or any
3608        // account that newly appeared in layer 2, is folded into `base_dirty`.
3609        //
3610        // LOAD-BEARING INVARIANT: the count/absence scan is sufficient *only* because
3611        // the one uncontrolled layer-2 writer — the foundry-fork-db `SharedBackend`
3612        // lazy fetch — is append-only at a fixed block (its request handler answers an
3613        // already-cached account/slot from the store and only inserts on a miss; it
3614        // never overwrites an existing entry in place). So an uncontrolled fetch can
3615        // only add a new account (caught by the absence check) or a new slot (caught
3616        // by the count check). An in-place value overwrite at unchanged length is
3617        // invisible here; the controlled writers therefore call `mark_base_dirty`
3618        // explicitly, and a direct out-of-band write via `unchecked_blockchain_db()`/`unchecked_backend()`
3619        // must call `invalidate_snapshot_base`. If a future foundry-fork-db bump makes
3620        // the lazy path overwrite-in-place, this scan must gain a value/version check.
3621        {
3622            let db_storage = self.blockchain_db.storage().read();
3623            for (addr, slots) in db_storage.iter() {
3624                if self.base_storage_lens.get(addr).copied() != Some(slots.len()) {
3625                    self.base_dirty.insert(*addr);
3626                }
3627            }
3628            let db_accounts = self.blockchain_db.accounts().read();
3629            let base = self.base.as_ref().expect("base present in case 2/3/4");
3630            for addr in db_accounts.keys() {
3631                if !base.accounts.contains_key(addr) {
3632                    self.base_dirty.insert(*addr);
3633                }
3634            }
3635        }
3636
3637        // Case 3: nothing changed → reuse the existing Arc unchanged.
3638        if self.base_dirty.is_empty() {
3639            return;
3640        }
3641
3642        // Case 4: rebuild copy-on-write — clone the outer maps (Arc handles +
3643        // AccountInfo, no per-slot copy) and rebuild only the dirty addresses.
3644        let prev = self.base.as_ref().expect("base present in case 4");
3645        let mut accounts = prev.accounts.clone();
3646        let mut storage = prev.storage.clone();
3647
3648        let db_accounts = self.blockchain_db.accounts().read();
3649        let db_storage = self.blockchain_db.storage().read();
3650        for addr in self.base_dirty.iter().copied() {
3651            // Account info: refresh from the current layer-2 account, or drop it if
3652            // the account no longer exists in layer 2 (e.g. after a purge).
3653            match db_accounts.get(&addr) {
3654                Some(info) => {
3655                    accounts.insert(addr, info.clone());
3656                }
3657                None => {
3658                    accounts.remove(&addr);
3659                }
3660            }
3661
3662            // Storage: rebuild this account's Arc<HashMap> from the current layer-2
3663            // storage, or drop it if the account has no layer-2 storage anymore.
3664            match db_storage.get(&addr) {
3665                Some(slots) => {
3666                    let rebuilt: HashMap<U256, U256> =
3667                        slots.iter().map(|(k, v)| (*k, *v)).collect();
3668                    self.base_storage_lens.insert(addr, rebuilt.len());
3669                    storage.insert(addr, Arc::new(rebuilt));
3670                }
3671                None => {
3672                    storage.remove(&addr);
3673                    self.base_storage_lens.remove(&addr);
3674                }
3675            }
3676        }
3677        drop(db_accounts);
3678        drop(db_storage);
3679
3680        // Rebuild the code index from the refreshed accounts (NOT cloned from the
3681        // previous base): a purged or recoded dirty account must not leave a stale
3682        // `code_by_hash` entry, which would diverge from `snapshot_deep_clone`
3683        // on a direct `code_by_hash(old_hash)` lookup. Rebuilding from scratch also
3684        // handles shared code hashes correctly (a hash survives iff some present
3685        // account still carries it).
3686        let code_by_hash = Self::code_index(&accounts);
3687
3688        self.base = Some(Arc::new(snapshot::BaseState {
3689            accounts,
3690            storage,
3691            code_by_hash,
3692        }));
3693        self.base_dirty.clear();
3694    }
3695
3696    /// Build the bytecode-by-hash index from a set of (layer-2) accounts, matching
3697    /// the deep-clone reference: a hash is present iff some account carries that
3698    /// code inline. Rebuilt from scratch on every base (re)build so a purged or
3699    /// recoded account never leaves a stale entry — preserving read-equivalence
3700    /// with [`snapshot_deep_clone`](Self::snapshot_deep_clone).
3701    fn code_index(accounts: &HashMap<Address, AccountInfo>) -> HashMap<B256, Bytecode> {
3702        accounts
3703            .values()
3704            .filter_map(|info| {
3705                info.code
3706                    .as_ref()
3707                    .map(|code| (info.code_hash, code.clone()))
3708            })
3709            .collect()
3710    }
3711
3712    /// Build a fresh [`BaseState`](snapshot::BaseState) by flattening all of layer
3713    /// 2, recording `base_storage_lens`. Shared by `refresh_base`'s full-rebuild
3714    /// path and [`snapshot_deep_clone`](Self::snapshot_deep_clone).
3715    fn build_base_full(&mut self) -> snapshot::BaseState {
3716        let mut accounts = HashMap::new();
3717        {
3718            let db_accounts = self.blockchain_db.accounts().read();
3719            for (addr, info) in db_accounts.iter() {
3720                accounts.insert(*addr, info.clone());
3721            }
3722        }
3723        let code_by_hash = Self::code_index(&accounts);
3724        let mut storage = HashMap::new();
3725        self.base_storage_lens.clear();
3726        {
3727            let db_storage = self.blockchain_db.storage().read();
3728            for (addr, slots) in db_storage.iter() {
3729                let converted: HashMap<U256, U256> = slots.iter().map(|(k, v)| (*k, *v)).collect();
3730                self.base_storage_lens.insert(*addr, converted.len());
3731                storage.insert(*addr, Arc::new(converted));
3732            }
3733        }
3734        snapshot::BaseState {
3735            accounts,
3736            storage,
3737            code_by_hash,
3738        }
3739    }
3740
3741    /// The retained deep-clone snapshot — today's full flatten, kept reachable for
3742    /// A/B benchmarking and as the read-equivalence reference (Decision D3).
3743    ///
3744    /// Produces the same two-tier [`EvmSnapshot`](snapshot::EvmSnapshot) shape as
3745    /// [`snapshot`](Self::snapshot), but with `base` set to the
3746    /// fully-merged flatten of **both** layers and **empty** overlay maps (the
3747    /// cleared / not-existing sets still in place). It is read-indistinguishable
3748    /// from `snapshot` by construction (the `tests/cow_snapshot.rs`
3749    /// differential gate pins this), at the cost of an O(total state) deep copy
3750    /// every call — exactly the cost `snapshot` now amortizes away.
3751    ///
3752    /// Stays `&self`: it does not touch the memoized base.
3753    #[doc(hidden)]
3754    pub fn snapshot_deep_clone(&self) -> Arc<snapshot::EvmSnapshot> {
3755        let mut accounts = HashMap::new();
3756        let mut storage: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
3757        let mut code_by_hash = HashMap::new();
3758
3759        // 1. Load from BlockchainDb (persistent cache / Layer 2).
3760        {
3761            let db_accounts = self.blockchain_db.accounts().read();
3762            for (addr, info) in db_accounts.iter() {
3763                if let Some(code) = &info.code {
3764                    code_by_hash.insert(info.code_hash, code.clone());
3765                }
3766                accounts.insert(*addr, info.clone());
3767            }
3768        }
3769        {
3770            let db_storage = self.blockchain_db.storage().read();
3771            for (addr, slots) in db_storage.iter() {
3772                let converted: HashMap<U256, U256> = slots.iter().map(|(k, v)| (*k, *v)).collect();
3773                storage.insert(*addr, converted);
3774            }
3775        }
3776
3777        // 2. Overlay from CacheDB (Layer 1, takes precedence). Merge into the same
3778        //    flat maps, dropping shadowed entries, exactly as the original
3779        //    `snapshot` did. A cleared account's storage is routed into
3780        //    `overlay_storage` (not the base), because `EvmSnapshot::storage_value`
3781        //    only applies the cleared-as-ZERO rule for an address with an
3782        //    `overlay_storage` entry — so the cleared semantics must be expressed
3783        //    there for both snapshot constructors to read identically.
3784        let mut overlay_storage: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
3785        let mut storage_cleared = std::collections::HashSet::new();
3786        let mut accounts_not_existing = std::collections::HashSet::new();
3787        for (addr, db_account) in &self.db.cache.accounts {
3788            let not_existing = matches!(db_account.account_state, AccountState::NotExisting);
3789            let cleared =
3790                not_existing || matches!(db_account.account_state, AccountState::StorageCleared);
3791
3792            if not_existing {
3793                accounts_not_existing.insert(*addr);
3794                accounts.remove(addr);
3795            } else {
3796                if let Some(code) = &db_account.info.code {
3797                    code_by_hash.insert(db_account.info.code_hash, code.clone());
3798                }
3799                accounts.insert(*addr, db_account.info.clone());
3800            }
3801
3802            if cleared {
3803                // Cleared: storage is locally complete. Drop any shadowed base
3804                // slots and keep ONLY the overlay slots, in `overlay_storage`.
3805                storage_cleared.insert(*addr);
3806                storage.remove(addr);
3807                let account_storage: HashMap<U256, U256> =
3808                    db_account.storage.iter().map(|(k, v)| (*k, *v)).collect();
3809                overlay_storage.insert(*addr, account_storage);
3810            } else {
3811                // Non-cleared: overlay slots win over base; fold them into base.
3812                let account_storage = storage.entry(*addr).or_default();
3813                for (slot, value) in &db_account.storage {
3814                    account_storage.insert(*slot, *value);
3815                }
3816            }
3817        }
3818
3819        let base = snapshot::BaseState {
3820            accounts,
3821            storage: storage
3822                .into_iter()
3823                .map(|(addr, slots)| (addr, Arc::new(slots)))
3824                .collect(),
3825            code_by_hash,
3826        };
3827
3828        Arc::new(snapshot::EvmSnapshot {
3829            base: Arc::new(base),
3830            overlay_accounts: HashMap::new(),
3831            overlay_storage,
3832            overlay_code_by_hash: HashMap::new(),
3833            storage_cleared,
3834            accounts_not_existing,
3835            block_hashes: HashMap::new(),
3836            block_number: self.block_number,
3837            basefee: self.basefee,
3838            coinbase: self.coinbase,
3839            prevrandao: self.prevrandao,
3840            gas_limit: self.block_gas_limit,
3841            chain_id: self.chain_id,
3842            timestamp: self.timestamp_override,
3843            spec_id: self.spec_id,
3844            shared_memory_capacity: self.shared_memory_capacity,
3845        })
3846    }
3847
3848    /// Mark a layer-2 address dirty so the next [`refresh_base`](Self::refresh_base)
3849    /// re-folds it into the memoized base (Pillar A invalidation; see
3850    /// `docs/phase-5-spec.md` §3).
3851    ///
3852    /// Called from every site that can change a layer-2 value a snapshot read
3853    /// would surface (write-through, batch injects, layer-2 seeding, purges).
3854    /// Over-invalidation is safe (Decision D2): marking an address that is also
3855    /// shadowed by layer 1 just re-folds that one account.
3856    fn mark_base_dirty(&mut self, address: Address) {
3857        self.base_dirty.insert(address);
3858    }
3859
3860    /// Force a full rebuild of the memoized base on the next
3861    /// [`refresh_base`](Self::refresh_base) (Pillar A invalidation).
3862    ///
3863    /// Used by layer-2 changes too broad to enumerate per-address efficiently
3864    /// (multi-contract / full-storage purges, block re-pins). Coarser than
3865    /// [`mark_base_dirty`](Self::mark_base_dirty) but always correct.
3866    fn invalidate_base(&mut self) {
3867        self.base_full_rebuild = true;
3868    }
3869
3870    /// Update the block that RPC fetches are pinned to.
3871    ///
3872    /// This re-pins the SharedBackend and the batch storage fetcher to `block`,
3873    /// so subsequent RPC fetches read state at the new block.
3874    ///
3875    /// # Block-context contract
3876    /// To prevent the EVM block context from silently diverging from the pinned
3877    /// block, when `block` is a concrete `BlockId::Number(Number(n))` this also
3878    /// updates `block_number` (the `NUMBER` opcode) to `n`. For tag-based block
3879    /// ids (`latest`, `pending`, hashes, etc.), the height is not
3880    /// statically known, so `block_number` is cleared.
3881    ///
3882    /// `basefee` (the `BASEFEE` opcode) is **cleared on every block change** and
3883    /// on every non-concrete tag/hash pin call because deriving it requires
3884    /// fetching the block header, which this synchronous method cannot do. Callers
3885    /// that change blocks should refresh it via
3886    /// [`set_block_context`](Self::set_block_context) after fetching the new
3887    /// header. Prefer [`repin_to_block`](Self::repin_to_block) when re-pinning to
3888    /// a concrete height, since it keeps `block_number` and the pinned block in
3889    /// lockstep.
3890    pub fn set_block(&mut self, block: BlockId) {
3891        let changed = self.block != block;
3892        let concrete_number = match block {
3893            BlockId::Number(BlockNumberOrTag::Number(n)) => Some(n),
3894            _ => None,
3895        };
3896        if changed {
3897            self.block = block;
3898            self.bump_snapshot_generation();
3899            // Re-pinning replaces layer 2 wholesale (state at a new block): the
3900            // memoized base must be rebuilt from scratch on the next snapshot.
3901            self.invalidate_base();
3902            let _ = self.backend.set_pinned_block(block);
3903        }
3904        if changed || concrete_number.is_none() {
3905            self.basefee = None;
3906        }
3907
3908        // Keep the EVM `NUMBER` opcode aligned with the pin. Only a concrete
3909        // height is meaningful; tags and hashes clear it so a stale number from
3910        // an earlier concrete block cannot leak into simulation.
3911        self.block_number = concrete_number;
3912    }
3913
3914    /// Get the block that RPC fetches are currently pinned to.
3915    pub fn block(&self) -> BlockId {
3916        self.block
3917    }
3918
3919    /// Monotonic generation counter for snapshot consistency (G6).
3920    ///
3921    /// Increments on every targeted state write ([`apply_update`](Self::apply_update),
3922    /// [`apply_updates`](Self::apply_updates), [`modify_slot`](Self::modify_slot)
3923    /// — and therefore everything built on them: reactive ingestion, freshness
3924    /// corrections, fresh injections) and on block re-pins
3925    /// ([`set_block`](Self::set_block), [`advance_block`](Self::advance_block)).
3926    /// Cold prefetch ([`inject_storage_batch`](Self::inject_storage_batch)) and
3927    /// lazy backend fetches do **not** increment it: they materialize the pinned
3928    /// block's existing state rather than changing it.
3929    ///
3930    /// The magnitude is opaque — how much one call increments it is
3931    /// unspecified — so compare values for **equality only**.
3932    ///
3933    /// The fan-out pattern: read the generation, take the
3934    /// [`snapshot`](Self::snapshot), read the generation again. If the two
3935    /// reads differ, state mutated in between (e.g. your event loop applied
3936    /// part of a block between the reads) — discard and re-snapshot to avoid
3937    /// fanning out simulations over a mid-block state.
3938    ///
3939    /// ```no_run
3940    /// # fn demo(cache: &mut evm_fork_cache::cache::EvmCache) {
3941    /// let snapshot = loop {
3942    ///     let generation = cache.snapshot_generation();
3943    ///     let snapshot = cache.snapshot();
3944    ///     if cache.snapshot_generation() == generation {
3945    ///         break snapshot;
3946    ///     }
3947    ///     // A mutation interleaved: try again at the next stable point.
3948    /// };
3949    /// # let _ = snapshot;
3950    /// # }
3951    /// ```
3952    pub fn snapshot_generation(&self) -> u64 {
3953        self.snapshot_generation
3954    }
3955
3956    /// Advance the snapshot-consistency generation (see
3957    /// [`snapshot_generation`](Self::snapshot_generation)).
3958    fn bump_snapshot_generation(&mut self) {
3959        self.snapshot_generation = self.snapshot_generation.wrapping_add(1);
3960    }
3961
3962    /// Set a custom timestamp for EVM simulations.
3963    ///
3964    /// When set, all EVM executions will use this timestamp instead of the current
3965    /// system time. This is useful for simulating future blocks to predict when
3966    /// time-dependent opportunities (like yield farming rewards) become profitable.
3967    ///
3968    /// Pass `None` to use the current system time (default behavior).
3969    pub fn set_timestamp(&mut self, timestamp: Option<u64>) {
3970        self.timestamp_override = timestamp;
3971    }
3972
3973    /// Get the current timestamp override, if any.
3974    ///
3975    /// Returns `None` if the cache is using the current system time (default).
3976    pub fn timestamp(&self) -> Option<u64> {
3977        self.timestamp_override
3978    }
3979
3980    /// Get the block number used for EVM simulations (the `NUMBER` opcode).
3981    ///
3982    /// Fetched from the pinned block's header at construction. Concrete-number
3983    /// pins set it via [`set_block`](Self::set_block) /
3984    /// [`repin_to_block`](Self::repin_to_block); tag/hash pins clear it
3985    /// because their height is not statically known. `None` means revm falls back
3986    /// to `0`, which can steer contracts that branch on `block.number` down a
3987    /// different code path. Override directly via
3988    /// [`set_block_context`](Self::set_block_context).
3989    pub fn block_number(&self) -> Option<u64> {
3990        self.block_number
3991    }
3992
3993    /// Get the base fee per gas used for EVM simulations (the `BASEFEE` opcode).
3994    ///
3995    /// Fetched from the pinned block's header at construction. `None` means
3996    /// revm falls back to `0`. This is cleared by [`set_block`](Self::set_block)
3997    /// / [`repin_to_block`](Self::repin_to_block) when the pin changes, and by
3998    /// non-concrete tag/hash pin calls because those can drift without a
3999    /// concrete number in the API. Refresh it with
4000    /// [`set_block_context`](Self::set_block_context) after fetching a new header
4001    /// if `BASEFEE` accuracy matters.
4002    pub fn basefee(&self) -> Option<u64> {
4003        self.basefee
4004    }
4005
4006    /// Update the block context for EVM simulations.
4007    ///
4008    /// Call this when the simulation block changes (e.g. at the start of each
4009    /// search cycle) to keep NUMBER and BASEFEE opcodes accurate.
4010    pub fn set_block_context(&mut self, block_number: Option<u64>, basefee: Option<u64>) {
4011        self.block_number = block_number;
4012        self.basefee = basefee;
4013    }
4014
4015    /// Set the block base fee (the `BASEFEE` opcode) for subsequent simulations,
4016    /// propagated into the next [`snapshot`](Self::snapshot).
4017    ///
4018    /// Offline caches built over a mocked provider have no fetched block header,
4019    /// so the base fee is unset (and the `BASEFEE` opcode reads `0`). Use this to
4020    /// install one explicitly — it determines the priority fee
4021    /// (`gas_price − basefee`) credited to the beneficiary, and thus the
4022    /// `coinbase_payment` a [`simulate_bundle`](Self::simulate_bundle) reports.
4023    ///
4024    /// The cache stores the base fee as a `u64` (matching the block header and the
4025    /// `EvmSnapshot` field), so a `U256` larger than `u64::MAX` is saturated.
4026    pub fn set_basefee(&mut self, basefee: U256) {
4027        self.basefee = Some(basefee.saturating_to::<u64>());
4028    }
4029
4030    /// Override the block beneficiary (the `COINBASE` opcode) for subsequent
4031    /// simulations.
4032    ///
4033    /// Set this when simulating logic that reads `block.coinbase` (e.g.
4034    /// MEV/builder tip accounting). `None` lets revm use its default beneficiary.
4035    pub fn set_coinbase(&mut self, coinbase: Option<Address>) {
4036        self.coinbase = coinbase;
4037    }
4038
4039    /// Override `prevrandao` (the `PREVRANDAO` opcode, the post-merge header mix
4040    /// hash) for subsequent simulations.
4041    ///
4042    /// Set this when reproducing contracts that source on-chain randomness from
4043    /// `block.prevrandao`. `None` leaves revm's default in place.
4044    pub fn set_prevrandao(&mut self, prevrandao: Option<B256>) {
4045        self.prevrandao = prevrandao;
4046    }
4047
4048    /// Override the block gas limit (the `GASLIMIT` opcode) for subsequent
4049    /// simulations.
4050    ///
4051    /// Set this when simulating logic that reads `block.gaslimit`. `None` lets
4052    /// revm use its default.
4053    pub fn set_block_gas_limit(&mut self, gas_limit: Option<u64>) {
4054        self.block_gas_limit = gas_limit;
4055    }
4056
4057    /// Get the block beneficiary used for EVM simulations (the `COINBASE`
4058    /// opcode).
4059    ///
4060    /// Fetched from the pinned block's header at construction, refreshed by
4061    /// [`advance_block`](Self::advance_block), or overridden via
4062    /// [`set_coinbase`](Self::set_coinbase). `None` means revm uses its default
4063    /// beneficiary.
4064    pub fn coinbase(&self) -> Option<Address> {
4065        self.coinbase
4066    }
4067
4068    /// Get `prevrandao` used for EVM simulations (the `PREVRANDAO` opcode, the
4069    /// post-merge header mix hash).
4070    ///
4071    /// Fetched from the pinned block's header at construction, refreshed by
4072    /// [`advance_block`](Self::advance_block), or overridden via
4073    /// [`set_prevrandao`](Self::set_prevrandao). `None` leaves revm's default in
4074    /// place.
4075    pub fn prevrandao(&self) -> Option<B256> {
4076        self.prevrandao
4077    }
4078
4079    /// Get the block gas limit used for EVM simulations (the `GASLIMIT` opcode).
4080    ///
4081    /// Fetched from the pinned block's header at construction, refreshed by
4082    /// [`advance_block`](Self::advance_block), or overridden via
4083    /// [`set_block_gas_limit`](Self::set_block_gas_limit). `None` lets revm use
4084    /// its default.
4085    pub fn block_gas_limit(&self) -> Option<u64> {
4086        self.block_gas_limit
4087    }
4088
4089    /// Set which block-context header fields subsequent
4090    /// [`advance_block`](Self::advance_block) calls require to be present.
4091    ///
4092    /// See [`BlockContextRequirements`]. Under
4093    /// [`strict`](BlockContextRequirements::strict) enforcement,
4094    /// [`advance_block`](Self::advance_block) rejects a header missing a required
4095    /// field rather than silently defaulting it.
4096    pub fn set_block_context_requirements(&mut self, reqs: BlockContextRequirements) {
4097        self.block_context_requirements = reqs;
4098    }
4099
4100    /// Engine-driven per-block env refresh from a canonical block header.
4101    ///
4102    /// First validates the header against the configured
4103    /// [`BlockContextRequirements`] (set via
4104    /// [`set_block_context_requirements`](Self::set_block_context_requirements)
4105    /// or the strict builder path). Under strict/partial requirements a header
4106    /// missing a required field is rejected with [`BlockContextError`] instead of
4107    /// being silently defaulted; under the [`lenient`](BlockContextRequirements::lenient)
4108    /// default this never errors.
4109    ///
4110    /// On success it refreshes the full EVM block env from the header — block
4111    /// number (`NUMBER`), base fee (`BASEFEE`), beneficiary (`COINBASE`),
4112    /// `prevrandao` (`PREVRANDAO`), gas limit (`GASLIMIT`) and timestamp — and
4113    /// re-pins **every** RPC fetch path (the SharedBackend lazy fallback, the
4114    /// batch storage fetcher, and the account-proof fetcher) to the header's
4115    /// block number, so a lazy miss after the advance reads state at the
4116    /// advanced block, in lockstep with the env. Intended to be driven once per
4117    /// canonical block (e.g. by the reactive runtime as new headers arrive).
4118    ///
4119    /// Unlike [`set_block`](Self::set_block), this does **not** invalidate the
4120    /// memoized COW snapshot base: an advance is a forward roll of the same live
4121    /// view (canonical mutations flow through the write funnel, which already
4122    /// marks the base dirty; lazy fetches stay insert-only-on-miss, which the
4123    /// base's growth scan catches), whereas `set_block` is a wholesale re-fork
4124    /// that must rebuild layer 2. Re-pinning to an *older* block is a re-fork,
4125    /// not an advance — use `set_block` for that.
4126    pub fn advance_block<H: BlockHeader>(&mut self, header: &H) -> Result<(), BlockContextError> {
4127        self.block_context_requirements.validate_header(header)?;
4128
4129        self.block_number = Some(header.number());
4130        self.basefee = header.base_fee_per_gas();
4131        self.coinbase = Some(header.beneficiary());
4132        self.prevrandao = header.mix_hash();
4133        self.block_gas_limit = Some(header.gas_limit());
4134        self.timestamp_override = Some(header.timestamp());
4135
4136        // Advance every fetch path to the new height in lockstep with the env:
4137        // the SharedBackend lazy fallback (a miss must not serve state from the
4138        // previously pinned block) and the pin accessor. Mirrors `set_block`'s
4139        // pin updates, minus the base invalidation (see the method docs for why
4140        // an advance keeps the memoized base).
4141        let block = BlockId::number(header.number());
4142        self.block = block;
4143        let _ = self.backend.set_pinned_block(block);
4144        // A snapshot spanning the env refresh would pair the new block context
4145        // with pre-advance state — bump so consumers can detect it (G6).
4146        self.bump_snapshot_generation();
4147
4148        Ok(())
4149    }
4150
4151    /// Re-pin the cache to a specific block number.
4152    ///
4153    /// Updates the SharedBackend pinned block, the batch fetcher block, and the
4154    /// EVM block context (`NUMBER` opcode) in lockstep. The current `basefee` is
4155    /// cleared because it cannot be refreshed synchronously; callers should set it
4156    /// via [`set_block_context`](Self::set_block_context) after fetching the new
4157    /// block header if `BASEFEE` accuracy matters.
4158    pub fn repin_to_block(&mut self, block_number: u64) {
4159        let old_block = self.block;
4160        self.set_block(BlockId::Number(block_number.into()));
4161
4162        if let BlockId::Number(BlockNumberOrTag::Number(old_num)) = old_block {
4163            let drift = block_number.saturating_sub(old_num);
4164            if drift > 0 {
4165                debug!(
4166                    old_block = old_num,
4167                    new_block = block_number,
4168                    drift,
4169                    "Re-pinned cache to current block"
4170                );
4171            }
4172        }
4173    }
4174
4175    /// Ensure an account is loaded into the cache.
4176    ///
4177    /// With the lazy-loading backend, this is optional - accounts are fetched
4178    /// automatically when accessed. However, you can use this to pre-warm
4179    /// the cache for specific accounts.
4180    #[instrument(level = "trace", skip(self))]
4181    pub async fn ensure_account(&mut self, address: Address) -> Result<()> {
4182        if self.db.cache.accounts.contains_key(&address) {
4183            return Ok(());
4184        }
4185
4186        // Load account info via SharedBackend (fetches from RPC if not cached).
4187        // basic_ref populates BlockchainDb; we also insert into the CacheDB
4188        // overlay so the account is immediately available for direct reads.
4189        use revm::database_interface::DatabaseRef;
4190        let info = self
4191            .backend
4192            .basic_ref(address)
4193            .map_err(|e| CacheError::AccountFetch {
4194                address,
4195                details: format!("{e:?}"),
4196            })?;
4197
4198        if let Some(info) = info {
4199            self.db.insert_account_info(address, info);
4200        }
4201
4202        Ok(())
4203    }
4204
4205    /// Read a single storage slot through the SharedBackend (BlockchainDb -> RPC fallback).
4206    ///
4207    /// After `purge_contract_slots` removes a slot from BlockchainDb, this method fetches
4208    /// fresh data from RPC and caches it in BlockchainDb. Subsequent EVM SLOADs find
4209    /// the value there without additional RPC calls.
4210    pub fn read_storage_slot(&mut self, address: Address, slot: U256) -> Result<U256> {
4211        use revm::database_interface::DatabaseRef;
4212        self.backend
4213            .storage_ref(address, slot)
4214            .map_err(|e| CacheError::StorageRead {
4215                address,
4216                slot,
4217                details: e.to_string(),
4218            })
4219    }
4220
4221    /// Write a raw storage slot value directly into the CacheDB layer.
4222    ///
4223    /// Subsequent EVM SLOADs for this (address, slot) will read the injected value
4224    /// without any RPC call. Used for hot-state injection where we already know the
4225    /// current on-chain value from WebSocket events.
4226    pub fn insert_storage_slot(&mut self, address: Address, slot: U256, value: U256) -> Result<()> {
4227        self.db
4228            .insert_account_storage(address, slot, value)
4229            .map_err(|e| CacheError::StorageInsert {
4230                address,
4231                slot,
4232                details: e.to_string(),
4233            })?;
4234        Ok(())
4235    }
4236
4237    /// Pre-seed known ERC20 `balanceOf` mapping base slots, keyed by token.
4238    ///
4239    /// Each `(token, slot)` records the storage slot of the token's
4240    /// `mapping(address => uint256) balances`, letting
4241    /// [`set_erc20_balance_with_slot_scan`](Self::set_erc20_balance_with_slot_scan)
4242    /// skip its discovery pass for that token and write the balance directly.
4243    /// Seeded slots are assumed to use Solidity's `keccak(key‖slot)` layout
4244    /// (use [`seed_erc20_balance_layouts`](Self::seed_erc20_balance_layouts) for
4245    /// Vyper/Solady tokens). Seeding a wrong slot is self-correcting: the write
4246    /// is verified and a fresh discovery pass runs (evicting the bad seed) if it
4247    /// fails. Later entries overwrite earlier ones for the same token.
4248    pub fn seed_erc20_balance_slots(&mut self, slots: impl IntoIterator<Item = (Address, U256)>) {
4249        for (token, slot) in slots {
4250            self.erc20_balance_slots.insert(
4251                token,
4252                TrackedMapping::new(token, slot, SlotLayout::SolidityMapping),
4253            );
4254        }
4255    }
4256
4257    /// Pre-seed known ERC20 balance mapping *descriptors* (base slot **and**
4258    /// layout), keyed by [`TrackedMapping::contract`].
4259    ///
4260    /// The layout-aware companion to
4261    /// [`seed_erc20_balance_slots`](Self::seed_erc20_balance_slots): use this for
4262    /// Vyper (`keccak(slot‖key)`) or Solady (packed) tokens whose layout is known
4263    /// up front, so [`set_erc20_balance_with_slot_scan`](Self::set_erc20_balance_with_slot_scan)
4264    /// writes the correct slot without a discovery pass.
4265    pub fn seed_erc20_balance_layouts(
4266        &mut self,
4267        mappings: impl IntoIterator<Item = TrackedMapping>,
4268    ) {
4269        for tracked in mappings {
4270            self.erc20_balance_slots.insert(tracked.contract, tracked);
4271        }
4272    }
4273
4274    /// Write a value into a Solidity `mapping(address => ...)` entry on
4275    /// `contract`, at the mapping declared at base slot `slot`.
4276    ///
4277    /// Computes the entry's storage key as
4278    /// `keccak256(abi.encode(slot_address, slot))` — Solidity's layout for an
4279    /// address-keyed mapping — and writes `value` there in the CacheDB overlay.
4280    /// Used to forge ERC20 balances and allowances without an on-chain transfer.
4281    ///
4282    /// # Errors
4283    /// Returns an error if the underlying CacheDB storage insert fails (e.g. the
4284    /// account cannot be loaded from the backend).
4285    pub fn insert_mapping_storage_slot(
4286        &mut self,
4287        contract: Address,
4288        slot: U256,
4289        slot_address: Address,
4290        value: U256,
4291    ) -> Result<()> {
4292        let hashed_balance_slot = keccak256((slot_address, slot).abi_encode());
4293        self.db
4294            .insert_account_storage(contract, hashed_balance_slot.into(), value)
4295            .map_err(|e| CacheError::StorageInsert {
4296                address: contract,
4297                slot: hashed_balance_slot.into(),
4298                details: e.to_string(),
4299            })?;
4300        Ok(())
4301    }
4302
4303    /// Run a call with a composable [`revm::Inspector`] attached, **without
4304    /// committing** state (the journal is reverted after execution), returning
4305    /// the execution result and the inspector moved back out so you can read what
4306    /// it captured.
4307    ///
4308    /// This is the cache-level counterpart to
4309    /// [`EvmOverlay::call_raw_with_inspector`](crate::cache::EvmOverlay::call_raw_with_inspector).
4310    /// Unlike the overlay form it runs directly against the cache's database, so
4311    /// any missing state is fetched lazily during the call — discovery works on a
4312    /// cold fork with no pre-warming.
4313    pub fn call_raw_with_inspector<I>(
4314        &mut self,
4315        from: Address,
4316        to: Address,
4317        calldata: Bytes,
4318        tx: &TxConfig,
4319        inspector: I,
4320    ) -> Result<(ExecutionResult, I)>
4321    where
4322        I: for<'a> revm::Inspector<
4323                Context<
4324                    BlockEnv,
4325                    TxEnv,
4326                    CfgEnv,
4327                    &'a mut ForkCacheDB,
4328                    Journal<&'a mut ForkCacheDB>,
4329                    (),
4330                >,
4331            >,
4332    {
4333        let tx_env = Self::build_tx_env_with(from, to, calldata, tx)?;
4334        let mut evm = self.build_evm_with_inspector(inspector);
4335        let checkpoint = evm.journaled_state.checkpoint();
4336        let result = evm.inspect_one_tx(tx_env);
4337        evm.journaled_state.checkpoint_revert(checkpoint);
4338        let inspector = evm.inspector;
4339        result.map(|r| (r, inspector)).map_err(CacheError::transact)
4340    }
4341
4342    /// Discover every hash-derived storage slot a call reads, factored into
4343    /// [`HashSlotAccess`]es (mapping keys, base slot, layout, exact slot, value).
4344    ///
4345    /// This is the general, ERC-20-agnostic entry point: it works for any mapping
4346    /// (balances, allowances, protocol positions, …) and any layout
4347    /// (Solidity / Vyper / Solady / nested), in a single simulation.
4348    /// `known_keys` are words — typically addresses via [`Address::into_word`] —
4349    /// used to anchor key/slot disambiguation; pass `&[]` to rely on the
4350    /// magnitude heuristic.
4351    ///
4352    /// # Limitations
4353    ///
4354    /// Discovery only sees slots the call actually `SLOAD`s. A getter that
4355    /// returns a *computed* value without reading a per-key backing slot — a
4356    /// rebasing balance derived from shares (stETH, Aave aTokens), or a value
4357    /// served purely from memory/calldata — yields no matching access. Callers
4358    /// that need a slot regardless (e.g. `set_erc20_balance_with_slot_scan`)
4359    /// fall through to a brute-force fallback rather than acting on a wrong slot.
4360    pub fn trace_hashed_slots(
4361        &mut self,
4362        from: Address,
4363        to: Address,
4364        calldata: Bytes,
4365        known_keys: &[B256],
4366    ) -> Result<Vec<HashSlotAccess>> {
4367        let (_result, probe) = self.call_raw_with_inspector(
4368            from,
4369            to,
4370            calldata,
4371            &TxConfig::default(),
4372            HashStorageProbe::new(),
4373        )?;
4374        Ok(probe.accesses(known_keys))
4375    }
4376
4377    /// Discover a token's `balanceOf` mapping slot for `owner` from a single
4378    /// simulated call — layout-agnostic (Solidity / Vyper / Solady), with no
4379    /// `max_slot` bound and no repeated probing.
4380    ///
4381    /// Returns the [`HashSlotAccess`] whose loaded value matches the getter's
4382    /// return and whose key is `owner`, or `None` if the token exposes no hashed
4383    /// balance read — a rebasing/computed getter that does not `SLOAD` a
4384    /// per-owner slot (stETH, aTokens), or unavailable code. Capture the
4385    /// result with [`HashSlotAccess::as_tracked`] to reuse the layout for other
4386    /// holders without re-simulating.
4387    pub fn discover_erc20_balance_slot(
4388        &mut self,
4389        token: Address,
4390        owner: Address,
4391    ) -> Result<Option<HashSlotAccess>> {
4392        let calldata = Bytes::from(IERC20::balanceOfCall { target: owner }.abi_encode());
4393        let (result, probe) = self.call_raw_with_inspector(
4394            owner,
4395            token,
4396            calldata,
4397            &TxConfig::default(),
4398            HashStorageProbe::new(),
4399        )?;
4400        let ret = match result {
4401            ExecutionResult::Success { output, .. } => output.into_data(),
4402            _ => return Ok(None),
4403        };
4404        let ret_val = if ret.len() >= 32 {
4405            U256::from_be_slice(&ret[..32])
4406        } else {
4407            U256::from_be_slice(&ret)
4408        };
4409        let owner_word = owner.into_word();
4410        // Prefer the hit whose value equals the return, then higher confidence,
4411        // then the shallowest derivation.
4412        let best = probe
4413            .accesses(&[owner_word])
4414            .into_iter()
4415            .filter(|a| a.keyed_by(owner_word))
4416            .max_by_key(|a| (a.value == ret_val, a.confidence, std::cmp::Reverse(a.depth)));
4417        Ok(best)
4418    }
4419
4420    /// Derive a token's balance mapping layout once, then compute the exact
4421    /// storage slot for each of `holders` — the "discover, then track these
4422    /// addresses" primitive.
4423    ///
4424    /// Reuses a cached/seeded [`TrackedMapping`] for `token` if present;
4425    /// otherwise discovers it from a single `balanceOf` simulation (using the
4426    /// first holder as the probe) and caches it. Returns the reusable descriptor
4427    /// plus `(holder, slot)` pairs — feed the slots to a
4428    /// [`FreshnessRegistry`](crate::freshness::FreshnessRegistry)
4429    /// (`pin_slot`/`mark_volatile_slot`) or a
4430    /// [`PrefetchRegistry`](crate::prefetch_registry::PrefetchRegistry) to keep
4431    /// them warm and fresh. Returns `None` if the layout can't be discovered
4432    /// (e.g. an empty `holders` set with no cached descriptor, or a token with no
4433    /// hashed balance read).
4434    pub fn track_erc20_balances(
4435        &mut self,
4436        token: Address,
4437        holders: impl IntoIterator<Item = Address>,
4438    ) -> Result<Option<TrackedBalances>> {
4439        let holders: Vec<Address> = holders.into_iter().collect();
4440
4441        let tracked = if let Some(t) = self.erc20_balance_slots.get(&token).copied() {
4442            t
4443        } else {
4444            let Some(&probe_holder) = holders.first() else {
4445                return Ok(None);
4446            };
4447            let Some(tracked) = self
4448                .discover_erc20_balance_slot(token, probe_holder)?
4449                .and_then(|access| access.as_tracked(token))
4450            else {
4451                return Ok(None);
4452            };
4453            self.erc20_balance_slots.insert(token, tracked);
4454            tracked
4455        };
4456
4457        let pairs = tracked
4458            .slots_for(holders.iter().map(|h| h.into_word()))
4459            .into_iter()
4460            .map(|(key, slot)| (Address::from_word(key), slot))
4461            .collect();
4462        Ok(Some((tracked, pairs)))
4463    }
4464
4465    /// Forge an ERC-20 allowance: discover the (nested) `allowance` mapping entry
4466    /// for `(owner, spender)` from a single traced `allowance` call, write
4467    /// `amount` to the exact slot, and verify.
4468    ///
4469    /// This is the approval counterpart to
4470    /// [`set_erc20_balance_with_slot_scan`](Self::set_erc20_balance_with_slot_scan)
4471    /// — newly feasible because nested-mapping discovery can locate
4472    /// `keccak(spender ‖ keccak(owner ‖ base))` (and its Vyper/packed variants)
4473    /// without a scan. Pass `U256::MAX` for an "unlimited" approval.
4474    ///
4475    /// Returns `Ok(true)` if set and verified, `Ok(false)` if the token exposes
4476    /// no discoverable hashed allowance entry keyed by `(owner, spender)`.
4477    pub fn set_erc20_allowance(
4478        &mut self,
4479        token: Address,
4480        owner: Address,
4481        spender: Address,
4482        amount: U256,
4483    ) -> Result<bool> {
4484        let calldata = Bytes::from(IERC20::allowanceCall { owner, spender }.abi_encode());
4485        let known = [owner.into_word(), spender.into_word()];
4486        let (owner_word, spender_word) = (owner.into_word(), spender.into_word());
4487
4488        // The allowance entry is the hashed read keyed by BOTH owner and spender;
4489        // prefer the deepest/highest-confidence such access.
4490        let target = self
4491            .trace_hashed_slots(owner, token, calldata, &known)?
4492            .into_iter()
4493            .filter(|a| a.keyed_by(owner_word) && a.keyed_by(spender_word))
4494            .max_by_key(|a| (a.depth, a.confidence));
4495
4496        let Some(target) = target else {
4497            return Ok(false);
4498        };
4499
4500        self.insert_storage_slot(token, U256::from_be_slice(target.slot.as_slice()), amount)?;
4501        Ok(self.erc20_allowance(token, owner, spender)? == amount)
4502    }
4503
4504    /// Write `value` into a mapping entry using a **discovered**
4505    /// [`TrackedMapping`] layout, returning the exact storage slot written.
4506    ///
4507    /// Unlike [`insert_mapping_storage_slot`](Self::insert_mapping_storage_slot),
4508    /// which always assumes Solidity `keccak(key‖slot)` order, this honors the
4509    /// tracked layout, so it writes the correct slot for Vyper and Solady tokens
4510    /// too. The `(contract, layout, base slot)` all come from the `tracked`
4511    /// descriptor.
4512    pub fn write_mapping_entry(
4513        &mut self,
4514        tracked: &TrackedMapping,
4515        key: B256,
4516        value: U256,
4517    ) -> Result<B256> {
4518        let slot = tracked
4519            .slot_for(key)
4520            .ok_or_else(|| CacheError::StorageInsert {
4521                address: tracked.contract,
4522                slot: U256::ZERO,
4523                details: format!(
4524                    "layout {} does not support single-key slot derivation",
4525                    tracked.layout
4526                ),
4527            })?;
4528        self.insert_storage_slot(
4529            tracked.contract,
4530            U256::from_be_slice(slot.as_slice()),
4531            value,
4532        )?;
4533        Ok(slot)
4534    }
4535
4536    /// Create a throwaway [`EvmOverlay`] over the current snapshot, wired to this
4537    /// cache's backend for lazy fetch.
4538    ///
4539    /// This is the entry point for **overlay-scoped mocking**: mock balances,
4540    /// approvals, and getter returns on the returned overlay
4541    /// ([`EvmOverlay::mock_balance`], [`EvmOverlay::mock_allowance`],
4542    /// [`EvmOverlay::mock_call`]) and run your simulations *on that overlay*. The
4543    /// mocks live only in the overlay's dirty layer and are dropped with it — the
4544    /// cache is never mutated, so a mocked balance can't leak into a later
4545    /// simulation. (For a persistent cache-level balance override, use
4546    /// [`set_erc20_balance_with_slot_scan`](Self::set_erc20_balance_with_slot_scan).)
4547    ///
4548    /// ```no_run
4549    /// # use alloy_primitives::{Address, U256};
4550    /// # use evm_fork_cache::cache::{EvmCache, TxConfig};
4551    /// # use alloy_primitives::Bytes;
4552    /// # fn ex(cache: &mut EvmCache, usdc: Address, alice: Address, router: Address, swap: Bytes)
4553    /// #     -> Result<(), Box<dyn std::error::Error>> {
4554    /// let mut sim = cache.mock_overlay();
4555    /// sim.mock_balance(usdc, alice, U256::from(1_000_000_000_000u64))?; // 1M USDC (6 dp)
4556    /// sim.mock_allowance(usdc, alice, router, U256::MAX)?;              // unlimited approve
4557    /// let out = sim.call_raw(alice, router, swap)?;  // simulate against the mocked state
4558    /// // drop `sim` → mocks discarded; `cache` is untouched.
4559    /// # let _ = out; Ok(()) }
4560    /// ```
4561    pub fn mock_overlay(&mut self) -> EvmOverlay {
4562        EvmOverlay::new(self.snapshot(), Some(self.backend.clone()))
4563    }
4564
4565    /// Set an ERC20 balance, discovering the token's balance mapping slot and
4566    /// layout if not already known, then writing `amount` there.
4567    ///
4568    /// Resolution order:
4569    /// 1. A cached/seeded [`TrackedMapping`] for the token (fast path).
4570    /// 2. **Trace-based discovery** — a single simulated `balanceOf(owner)`,
4571    ///    layout-agnostic (Solidity / Vyper / Solady) and unbounded by `max_slot`
4572    ///    (see [`discover_erc20_balance_slot`](Self::discover_erc20_balance_slot)).
4573    /// 3. The legacy brute-force **scan** of `0..=max_slot` (Solidity order only),
4574    ///    kept as a fallback for the rare token whose `balanceOf` reads no hashed
4575    ///    slot the trace can attribute.
4576    ///
4577    /// Every path verifies the write via `balanceOf` before caching, so a wrong
4578    /// guess is self-correcting. Returns `Ok(true)` if set and verified,
4579    /// `Ok(false)` if nothing worked, and `Err` on EVM/cache failures.
4580    pub fn set_erc20_balance_with_slot_scan(
4581        &mut self,
4582        token: Address,
4583        owner: Address,
4584        amount: U256,
4585        max_slot: u16,
4586    ) -> Result<bool> {
4587        let owner_word = owner.into_word();
4588
4589        // 1. Cached/seeded descriptor — write with its (layout-aware) slot.
4590        if let Some(tracked) = self.erc20_balance_slots.get(&token).copied() {
4591            self.write_mapping_entry(&tracked, owner_word, amount)?;
4592            if self.erc20_balance_of(token, owner)? == amount {
4593                return Ok(true);
4594            }
4595            self.erc20_balance_slots.remove(&token);
4596        }
4597
4598        // 2. Trace-based discovery: one sim, layout-aware, no max_slot bound.
4599        if let Some(tracked) = self
4600            .discover_erc20_balance_slot(token, owner)?
4601            .and_then(|access| access.as_tracked(token))
4602        {
4603            self.write_mapping_entry(&tracked, owner_word, amount)?;
4604            if self.erc20_balance_of(token, owner)? == amount {
4605                self.erc20_balance_slots.insert(token, tracked);
4606                return Ok(true);
4607            }
4608            // Discovered slot didn't drive the balance (rebasing/computed getter,
4609            // or a same-key non-balance mapping): fall through to the scan.
4610        }
4611
4612        // 3. Legacy brute-force scan (Solidity order only) as a last resort.
4613        let Some(discovered_slot) =
4614            self.discover_erc20_balance_slot_with_scan(token, owner, max_slot)?
4615        else {
4616            return Ok(false);
4617        };
4618
4619        let tracked = TrackedMapping::new(token, discovered_slot, SlotLayout::SolidityMapping);
4620        self.write_mapping_entry(&tracked, owner_word, amount)?;
4621        let verified = self.erc20_balance_of(token, owner)? == amount;
4622        if verified {
4623            self.erc20_balance_slots.insert(token, tracked);
4624        } else {
4625            self.erc20_balance_slots.remove(&token);
4626        }
4627        Ok(verified)
4628    }
4629
4630    fn discover_erc20_balance_slot_with_scan(
4631        &mut self,
4632        token: Address,
4633        owner: Address,
4634        max_slot: u16,
4635    ) -> Result<Option<U256>> {
4636        if let Some(tracked) = self.erc20_balance_slots.get(&token) {
4637            return Ok(Some(tracked.base_slot));
4638        }
4639
4640        let baseline_snapshot = self.checkpoint();
4641        let baseline_balance = self.erc20_balance_of(token, owner)?;
4642
4643        // Choose a probe value distinct from baseline to avoid false positives.
4644        let mut probe = U256::from(0xDEAD_BEEF_u64);
4645        if probe == baseline_balance {
4646            probe = baseline_balance.saturating_add(U256::from(1u64));
4647        }
4648        if probe == baseline_balance {
4649            probe = U256::MAX;
4650        }
4651
4652        for slot_idx in 0..=max_slot {
4653            self.restore(baseline_snapshot.clone());
4654            let slot = U256::from(slot_idx);
4655            self.insert_mapping_storage_slot(token, slot, owner, probe)?;
4656            if self.erc20_balance_of(token, owner)? == probe {
4657                self.restore(baseline_snapshot);
4658                self.erc20_balance_slots.insert(
4659                    token,
4660                    TrackedMapping::new(token, slot, SlotLayout::SolidityMapping),
4661                );
4662                return Ok(Some(slot));
4663            }
4664        }
4665
4666        self.restore(baseline_snapshot);
4667        Ok(None)
4668    }
4669
4670    /// Execute a call with automatic account/storage fetching.
4671    ///
4672    /// Unlike the old implementation, this does NOT prefetch via access lists.
4673    /// The SharedBackend lazily fetches any missing data during execution.
4674    #[instrument(level = "debug", skip(self, calldata), fields(calldata_len = calldata.len()))]
4675    pub fn call(
4676        &mut self,
4677        from: Address,
4678        to: Address,
4679        calldata: Bytes,
4680        commit: bool,
4681    ) -> Result<ExecutionResult> {
4682        self.call_raw(from, to, calldata, commit)
4683    }
4684
4685    /// Execute a call without any prefetching.
4686    ///
4687    /// Data is fetched lazily by the SharedBackend as needed during execution.
4688    #[instrument(level = "debug", skip(self, calldata), fields(calldata_len = calldata.len()))]
4689    pub fn call_raw(
4690        &mut self,
4691        from: Address,
4692        to: Address,
4693        calldata: Bytes,
4694        commit: bool,
4695    ) -> Result<ExecutionResult> {
4696        self.call_raw_with(from, to, calldata, commit, &TxConfig::default())
4697    }
4698
4699    /// Execute a non-committing typed Solidity call from [`Address::ZERO`].
4700    ///
4701    /// This is the typed equivalent of encoding a [`SolCall`], passing it to
4702    /// [`call_raw`](Self::call_raw) with `commit = false`, and decoding the
4703    /// successful return data with [`SolCall::abi_decode_returns`].
4704    ///
4705    /// ```no_run
4706    /// # use alloy_primitives::Address;
4707    /// # use alloy_sol_types::sol;
4708    /// # use evm_fork_cache::cache::EvmCache;
4709    /// # fn example(cache: &mut EvmCache, token: Address, owner: Address) -> Result<(), Box<dyn std::error::Error>> {
4710    /// sol! {
4711    ///     function balanceOf(address account) external view returns (uint256);
4712    /// }
4713    ///
4714    /// let balance = cache.call_sol(token, balanceOfCall { account: owner })?;
4715    /// # let _ = balance;
4716    /// # Ok(())
4717    /// # }
4718    /// ```
4719    pub fn call_sol<C>(&mut self, to: Address, call: C) -> Result<C::Return>
4720    where
4721        C: SolCall,
4722    {
4723        self.call_sol_from(Address::ZERO, to, call)
4724    }
4725
4726    /// Execute a non-committing typed Solidity call from an explicit sender.
4727    ///
4728    /// Uses the default [`TxConfig`], so native value, gas limit/price, nonce,
4729    /// and access list are left at the same defaults as [`call_raw`](Self::call_raw).
4730    pub fn call_sol_from<C>(&mut self, from: Address, to: Address, call: C) -> Result<C::Return>
4731    where
4732        C: SolCall,
4733    {
4734        self.call_sol_with_commit(from, to, call, &TxConfig::default(), false)
4735    }
4736
4737    /// Execute a non-committing typed Solidity call with explicit tx overrides.
4738    ///
4739    /// This is the typed equivalent of [`call_raw_with`](Self::call_raw_with)
4740    /// with `commit = false`.
4741    pub fn call_sol_with<C>(
4742        &mut self,
4743        from: Address,
4744        to: Address,
4745        call: C,
4746        tx: &TxConfig,
4747    ) -> Result<C::Return>
4748    where
4749        C: SolCall,
4750    {
4751        self.call_sol_with_commit(from, to, call, tx, false)
4752    }
4753
4754    /// Execute a typed Solidity call and commit its state changes.
4755    ///
4756    /// This is the typed equivalent of [`call_raw_with`](Self::call_raw_with)
4757    /// with `commit = true`; the call's state changes are persisted through the
4758    /// same path as the raw committing API before the return data is decoded.
4759    pub fn transact_sol<C>(
4760        &mut self,
4761        from: Address,
4762        to: Address,
4763        call: C,
4764        tx: &TxConfig,
4765    ) -> Result<C::Return>
4766    where
4767        C: SolCall,
4768    {
4769        self.call_sol_with_commit(from, to, call, tx, true)
4770    }
4771
4772    /// Execute a call with explicit transaction-environment overrides
4773    /// ([`TxConfig`]): native `value`, gas limit/price, nonce, and an input
4774    /// access list. This is the entry point for value-bearing and gas-bounded
4775    /// simulation; [`call_raw`](Self::call_raw) is the zero-value shorthand.
4776    #[instrument(level = "debug", skip(self, calldata, tx), fields(calldata_len = calldata.len()))]
4777    pub fn call_raw_with(
4778        &mut self,
4779        from: Address,
4780        to: Address,
4781        calldata: Bytes,
4782        commit: bool,
4783        tx: &TxConfig,
4784    ) -> Result<ExecutionResult> {
4785        let tx_env = Self::build_tx_env_with(from, to, calldata, tx)?;
4786        let mut evm = self.build_evm();
4787
4788        if commit {
4789            return evm.transact_commit(tx_env).map_err(CacheError::transact);
4790        }
4791
4792        let checkpoint = evm.journaled_state.checkpoint();
4793        let result = evm.transact_one(tx_env);
4794        evm.journaled_state.checkpoint_revert(checkpoint);
4795        result.map_err(CacheError::transact)
4796    }
4797
4798    /// Execute a non-committing call and extract the access list of touched
4799    /// accounts and storage slots before reverting.
4800    ///
4801    /// Used for EIP-2929 marginal gas estimation in batched simulations.
4802    pub fn call_raw_with_access_list(
4803        &mut self,
4804        from: Address,
4805        to: Address,
4806        calldata: Bytes,
4807    ) -> Result<(ExecutionResult, StorageAccessList)> {
4808        let tx = Self::build_tx_env(from, to, calldata)?;
4809        let mut evm = self.build_evm();
4810
4811        let checkpoint = evm.journaled_state.checkpoint();
4812        match evm.transact_one(tx) {
4813            Ok(result) => {
4814                // Extract access list from journaled state before reverting. After
4815                // transact_one, journaled_state.state holds all touched accounts/slots.
4816                let mut access_list = StorageAccessList::default();
4817                for (address, account) in evm.journaled_state.state.iter() {
4818                    if account.is_touched() {
4819                        access_list.accounts.insert(*address);
4820                        for (slot_key, _) in account.storage.iter() {
4821                            access_list.slots.insert((*address, *slot_key));
4822                        }
4823                    }
4824                }
4825                evm.journaled_state.checkpoint_revert(checkpoint);
4826                Ok((result, access_list))
4827            }
4828            Err(e) => {
4829                // Revert the checkpoint even on a host/transact error so the EVM
4830                // journal is not left dirty (mirrors `call_raw`).
4831                evm.journaled_state.checkpoint_revert(checkpoint);
4832                Err(CacheError::transact(e))
4833            }
4834        }
4835    }
4836
4837    /// Execute a call and return its emitted logs and gas used.
4838    ///
4839    /// A thin wrapper over [`call`](Self::call) that requires success and
4840    /// discards the return data. When `commit` is true the call's state changes
4841    /// are persisted to the CacheDB overlay; otherwise they are reverted.
4842    ///
4843    /// # Errors
4844    /// Returns an error if the underlying transact fails, or if the call did not
4845    /// `Success` (i.e. it reverted or halted).
4846    pub fn call_logs(
4847        &mut self,
4848        from: Address,
4849        to: Address,
4850        calldata: Bytes,
4851        commit: bool,
4852    ) -> Result<(Vec<Log>, u64)> {
4853        let result = self.call(from, to, calldata, commit)?;
4854        if let ExecutionResult::Success { logs, gas_used, .. } = result {
4855            Ok((logs, gas_used))
4856        } else {
4857            Err(CacheError::CallNotSuccessful {
4858                result: format!("{result:?}"),
4859            })
4860        }
4861    }
4862
4863    /// Read an ERC20 token balance by simulating a `balanceOf(owner)` call.
4864    ///
4865    /// Non-committing: the read is reverted, so it never mutates cache state.
4866    ///
4867    /// # Errors
4868    /// Returns an error if the simulated call fails or does not `Success` (e.g.
4869    /// `token` is not a contract or reverts), or if the returned data cannot be
4870    /// ABI-decoded as a `uint256`.
4871    pub fn erc20_balance_of(&mut self, token: Address, owner: Address) -> Result<U256> {
4872        let call = IERC20::balanceOfCall { target: owner };
4873        let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
4874
4875        match result {
4876            ExecutionResult::Success { output, .. } => {
4877                let out = output.into_data();
4878                let balance = IERC20::balanceOfCall::abi_decode_returns(&out).map_err(|e| {
4879                    CacheError::Decode {
4880                        what: "ERC20 balanceOf return data",
4881                        details: format!("{e:?}"),
4882                    }
4883                })?;
4884                Ok(balance)
4885            }
4886            _ => Err(CacheError::CallNotSuccessful {
4887                result: format!("{result:?}"),
4888            }),
4889        }
4890    }
4891
4892    /// Read an ERC20 allowance by simulating an `allowance(owner, spender)` call.
4893    ///
4894    /// Non-committing: the read is reverted, so it never mutates cache state.
4895    ///
4896    /// # Errors
4897    /// Returns an error if the simulated call fails or does not `Success` (e.g.
4898    /// `token` is not a contract or reverts), or if the returned data cannot be
4899    /// ABI-decoded as a `uint256`.
4900    pub fn erc20_allowance(
4901        &mut self,
4902        token: Address,
4903        owner: Address,
4904        spender: Address,
4905    ) -> Result<U256> {
4906        let call = IERC20::allowanceCall { owner, spender };
4907        let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
4908
4909        match result {
4910            ExecutionResult::Success { output, .. } => {
4911                let out = output.into_data();
4912                let allowance = IERC20::allowanceCall::abi_decode_returns(&out).map_err(|e| {
4913                    CacheError::Decode {
4914                        what: "ERC20 allowance return data",
4915                        details: format!("{e:?}"),
4916                    }
4917                })?;
4918                Ok(allowance)
4919            }
4920            _ => Err(CacheError::CallNotSuccessful {
4921                result: format!("{result:?}"),
4922            }),
4923        }
4924    }
4925
4926    /// Read an ERC20 token's decimals by simulating a `decimals()` call.
4927    ///
4928    /// Memoized: a hit in the in-memory token-decimals map returns immediately
4929    /// without simulating. On a miss the value is resolved by a non-committing
4930    /// `decimals()` call.
4931    ///
4932    /// # Side effects
4933    /// On a miss the resolved value is cached in **both** the in-memory
4934    /// token-decimals map (process lifetime) **and** the immutable data cache
4935    /// (so it is persisted to disk on the next [`flush`](Self::flush)).
4936    ///
4937    /// # Errors
4938    /// Returns an error if the simulated call fails or does not `Success` (e.g.
4939    /// `token` is not a contract or reverts), or if the returned data cannot be
4940    /// ABI-decoded as a `uint8`.
4941    pub fn erc20_decimals(&mut self, token: Address) -> Result<u8> {
4942        if let Some(decimals) = self.token_decimals.get(&token) {
4943            return Ok(*decimals);
4944        }
4945
4946        let call = IERC20::decimalsCall {};
4947        let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
4948
4949        match result {
4950            ExecutionResult::Success { output, .. } => {
4951                let out = output.into_data();
4952                let decimals = IERC20::decimalsCall::abi_decode_returns(&out).map_err(|e| {
4953                    CacheError::Decode {
4954                        what: "ERC20 decimals return data",
4955                        details: format!("{e:?}"),
4956                    }
4957                })?;
4958                self.token_decimals.insert(token, decimals);
4959                // Also update immutable cache for persistence
4960                self.immutable_cache.set_token_decimals(token, decimals);
4961                Ok(decimals)
4962            }
4963            _ => Err(CacheError::CallNotSuccessful {
4964                result: format!("{result:?}"),
4965            }),
4966        }
4967    }
4968
4969    /// Get a reference to the immutable data cache (token decimals).
4970    pub fn immutable_cache(&self) -> &ImmutableDataCache {
4971        &self.immutable_cache
4972    }
4973
4974    /// Get a mutable reference to the immutable data cache.
4975    ///
4976    /// Use this to pre-populate token decimals that would otherwise be discovered
4977    /// lazily. Entries are persisted on the next [`flush`](Self::flush) (and on
4978    /// drop) when a [`CacheConfig`] is set.
4979    pub fn immutable_cache_mut(&mut self) -> &mut ImmutableDataCache {
4980        &mut self.immutable_cache
4981    }
4982
4983    /// Check if an address has storage slots pre-loaded in the BlockchainDb.
4984    ///
4985    /// This is useful to determine if we loaded the EVM state from the unified
4986    /// `evm_state.bin` cache and an address already has reusable storage.
4987    ///
4988    /// # Arguments
4989    /// * `address` - The contract address to check
4990    ///
4991    /// # Returns
4992    /// `true` if the address has any storage slots in the underlying BlockchainDb,
4993    /// `false` otherwise
4994    pub fn has_contract_storage(&self, address: Address) -> bool {
4995        let storage = self.blockchain_db.storage().read();
4996        storage
4997            .get(&address)
4998            .map(|slots| !slots.is_empty())
4999            .unwrap_or(false)
5000    }
5001
5002    /// Get the number of storage slots loaded for a contract address.
5003    ///
5004    /// Useful for debugging and logging to understand cache state.
5005    pub fn contract_storage_slot_count(&self, address: Address) -> usize {
5006        let storage = self.blockchain_db.storage().read();
5007        storage.get(&address).map(|slots| slots.len()).unwrap_or(0)
5008    }
5009
5010    /// Get memory statistics for the shared memory buffer used during EVM simulations.
5011    ///
5012    /// Returns a tuple of (current_capacity_bytes, current_length_bytes).
5013    ///
5014    /// The capacity represents the high-water mark of memory usage across all
5015    /// simulations since the buffer grows but doesn't shrink. The length is
5016    /// typically 0 between simulations (cleared after each use).
5017    ///
5018    /// # Use Case
5019    /// Call this after running a batch of simulations to understand memory usage
5020    /// and inform the optimal initial capacity for `SharedMemory`.
5021    ///
5022    /// # Example
5023    /// ```ignore
5024    /// let (capacity, _len) = cache.shared_memory_stats();
5025    /// println!("Peak memory usage: {} KB", capacity / 1024);
5026    /// ```
5027    pub fn shared_memory_stats(&self) -> (usize, usize) {
5028        let buffer = self.shared_memory_buffer.borrow();
5029        (buffer.capacity(), buffer.len())
5030    }
5031
5032    /// Log the current shared memory buffer statistics.
5033    ///
5034    /// Useful for profiling after running a batch of simulations.
5035    pub fn log_shared_memory_stats(&self) {
5036        let (capacity, len) = self.shared_memory_stats();
5037        debug!(
5038            capacity_bytes = capacity,
5039            capacity_kb = capacity / 1024,
5040            current_len = len,
5041            "Shared memory buffer stats (peak capacity across simulations)"
5042        );
5043    }
5044
5045    /// Pre-allocate the shared memory buffer to a specific capacity.
5046    ///
5047    /// Use this after measuring peak usage to avoid reallocation overhead
5048    /// during simulations. The buffer will grow beyond this if needed,
5049    /// but pre-sizing to the expected peak eliminates allocations.
5050    ///
5051    /// # Arguments
5052    /// * `capacity` - The capacity in bytes to reserve
5053    ///
5054    /// # Example
5055    /// ```ignore
5056    /// // After profiling shows peak usage is ~32KB
5057    /// cache.reserve_shared_memory(32 * 1024);
5058    /// ```
5059    pub fn reserve_shared_memory(&mut self, capacity: usize) {
5060        let mut buffer = self.shared_memory_buffer.borrow_mut();
5061        let current_capacity = buffer.capacity();
5062        if current_capacity < capacity {
5063            buffer.reserve(capacity - current_capacity);
5064            debug!(
5065                new_capacity = buffer.capacity(),
5066                requested = capacity,
5067                "Reserved shared memory buffer capacity"
5068            );
5069        }
5070        drop(buffer);
5071        // Record the high-water mark so snapshots taken afterwards propagate it to
5072        // their overlays (snapshots copy the capacity at creation time).
5073        self.shared_memory_capacity = self.shared_memory_capacity.max(capacity);
5074    }
5075
5076    /// The resolved per-context EVM shared-memory pre-allocation, in bytes.
5077    ///
5078    /// This is the [`SharedMemoryCapacity`] configured on the
5079    /// [`EvmCacheBuilder`] resolved to a concrete size (with
5080    /// [`SharedMemoryCapacity::Auto`] resolved against the state loaded at
5081    /// construction), raised by any later [`reserve_shared_memory`](Self::reserve_shared_memory).
5082    /// Each [`snapshot`](Self::snapshot) copies it onto the snapshot
5083    /// so snapshot-backed [`EvmOverlay`]s pre-allocate the same amount.
5084    pub fn shared_memory_capacity(&self) -> usize {
5085        self.shared_memory_capacity
5086    }
5087
5088    /// The cache-side storage batch-fetch configuration for this instance.
5089    pub fn storage_batch_config(&self) -> StorageBatchConfig {
5090        self.storage_batch_config
5091    }
5092
5093    /// Purge all storage slots for a specific contract from both cache layers.
5094    ///
5095    /// This clears:
5096    /// 1. **CacheDB overlay** (`self.db.cache.accounts[addr].storage`) - the in-memory
5097    ///    layer that caches storage slots fetched during EVM execution. Without clearing
5098    ///    this layer, subsequent EVM calls return stale values even after the backend
5099    ///    is purged.
5100    /// 2. **BlockchainDb backend** (`self.blockchain_db.storage()`) - the persistent
5101    ///    layer that caches RPC responses and is loaded from `evm_state.bin`.
5102    ///
5103    /// After purging both layers, the next EVM read for this contract's storage will
5104    /// go all the way to the RPC for fresh data.
5105    pub fn purge_contract_storage(&mut self, address: Address) -> usize {
5106        // Thin wrapper over the unified purge primitive; returns the backend slot
5107        // count the `AllStorage` scope removed.
5108        self.apply_update(&StateUpdate::purge(address, PurgeScope::AllStorage))
5109            .purged
5110            .first()
5111            .map(|rec| rec.slots_removed)
5112            .unwrap_or(0)
5113    }
5114
5115    /// `AllStorage`-scope purge layer logic. Clears the overlay storage for
5116    /// `address` and removes its backend storage map. Returns the number of
5117    /// backend slots removed.
5118    fn purge_contract_storage_inner(&mut self, address: Address) -> usize {
5119        // Layer 1: Clear CacheDB overlay
5120        let cache_db_cleared = if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
5121            let count = db_account.storage.len();
5122            db_account.storage.clear();
5123            count
5124        } else {
5125            0
5126        };
5127
5128        // Layer 2: Clear BlockchainDb backend
5129        let backend_cleared = {
5130            let mut storage = self.blockchain_db.storage().write();
5131            if let Some(slots) = storage.remove(&address) {
5132                slots.len()
5133            } else {
5134                0
5135            }
5136        };
5137
5138        if cache_db_cleared > 0 || backend_cleared > 0 {
5139            debug!(
5140                contract = %address,
5141                cache_db_slots = cache_db_cleared,
5142                backend_slots = backend_cleared,
5143                "purged contract storage from both cache layers"
5144            );
5145        }
5146
5147        // Layer-2 storage for this address was removed → invalidate base.
5148        self.mark_base_dirty(address);
5149        backend_cleared
5150    }
5151
5152    /// Purge specific storage slots for a contract from both cache layers.
5153    ///
5154    /// Unlike `purge_contract_storage()` which removes ALL storage, this only removes
5155    /// the specified slots. This is useful when only a narrow subset of hot storage
5156    /// became stale and the rest of the contract's cached storage should be kept.
5157    ///
5158    /// Returns the number of slots removed from the BlockchainDb backend.
5159    pub fn purge_contract_slots(&mut self, address: Address, slots: &[U256]) -> usize {
5160        // Thin wrapper over the unified purge primitive; returns the backend slot
5161        // count the `Slots` scope removed.
5162        self.apply_update(&StateUpdate::purge(
5163            address,
5164            PurgeScope::Slots(slots.to_vec()),
5165        ))
5166        .purged
5167        .first()
5168        .map(|rec| rec.slots_removed)
5169        .unwrap_or(0)
5170    }
5171
5172    /// `Slots`-scope purge layer logic. Removes the listed slots from the overlay
5173    /// and the backend storage map. Returns the number of backend slots removed.
5174    fn purge_contract_slots_inner(&mut self, address: Address, slots: &[U256]) -> usize {
5175        let mut cache_db_removed = 0usize;
5176        let mut backend_removed = 0usize;
5177
5178        // Layer 1: Remove specific slots from CacheDB overlay
5179        if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
5180            for slot in slots {
5181                if db_account.storage.remove(slot).is_some() {
5182                    cache_db_removed += 1;
5183                }
5184            }
5185        }
5186
5187        // Layer 2: Remove specific slots from BlockchainDb backend
5188        {
5189            let mut storage = self.blockchain_db.storage().write();
5190            if let Some(address_storage) = storage.get_mut(&address) {
5191                for slot in slots {
5192                    if address_storage.remove(slot).is_some() {
5193                        backend_removed += 1;
5194                    }
5195                }
5196            }
5197        }
5198
5199        if cache_db_removed > 0 || backend_removed > 0 {
5200            trace!(
5201                contract = %address,
5202                requested = slots.len(),
5203                cache_db_removed,
5204                backend_removed,
5205                "selectively purged contract storage slots from both cache layers"
5206            );
5207        }
5208
5209        // Layer-2 storage for this address changed (slots dropped) → invalidate
5210        // base. The growth scan only catches length changes; mark explicitly.
5211        self.mark_base_dirty(address);
5212        backend_removed
5213    }
5214
5215    /// Purge storage slots for multiple contracts from both cache layers.
5216    ///
5217    /// See `purge_contract_storage()` for details on what each layer contains.
5218    pub fn purge_contracts_storage(
5219        &mut self,
5220        addresses: impl IntoIterator<Item = Address>,
5221    ) -> usize {
5222        let mut total_purged = 0usize;
5223
5224        for address in addresses {
5225            // Layer 1: Clear CacheDB overlay
5226            if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
5227                db_account.storage.clear();
5228            }
5229
5230            // Layer 2: Clear BlockchainDb backend
5231            let mut storage = self.blockchain_db.storage().write();
5232            if let Some(slots) = storage.remove(&address) {
5233                let count = slots.len();
5234                if count > 0 {
5235                    debug!(
5236                        contract = %address,
5237                        slots_removed = count,
5238                        "purged contract storage from both cache layers"
5239                    );
5240                }
5241                total_purged += count;
5242            }
5243        }
5244
5245        if total_purged > 0 {
5246            debug!(
5247                total_slots_purged = total_purged,
5248                "purged contract storage from both cache layers"
5249            );
5250        }
5251        // Multiple layer-2 contracts changed → full base rebuild (coarse but
5252        // correct; cheaper than enumerating each touched address here).
5253        self.invalidate_base();
5254        total_purged
5255    }
5256
5257    /// Purge ALL storage slots from both cache layers while preserving bytecodes.
5258    ///
5259    /// Use this for periodic full cache refresh (e.g., every 48 hours) to ensure
5260    /// any stale data like strategy swap paths, proxy implementations, reward rates,
5261    /// etc. are re-fetched from the actual on-chain state.
5262    ///
5263    /// This preserves:
5264    /// - Account info (nonce, balance, code hash)
5265    /// - Contract bytecodes (immutable)
5266    ///
5267    /// This purges:
5268    /// - All storage slots from CacheDB overlay (layer 1)
5269    /// - All storage slots from BlockchainDb backend (layer 2)
5270    ///
5271    /// # Returns
5272    /// The total number of storage slots that were removed from the BlockchainDb
5273    pub fn purge_all_storage(&mut self) -> usize {
5274        // Layer 1: Clear all storage in CacheDB overlay
5275        let mut cache_db_cleared = 0usize;
5276        for db_account in self.db.cache.accounts.values_mut() {
5277            cache_db_cleared += db_account.storage.len();
5278            db_account.storage.clear();
5279        }
5280
5281        // Layer 2: Clear BlockchainDb backend
5282        let (total_slots, contract_count) = {
5283            let mut storage = self.blockchain_db.storage().write();
5284            let total_slots: usize = storage.values().map(|s| s.len()).sum();
5285            let contract_count = storage.len();
5286            storage.clear();
5287            (total_slots, contract_count)
5288        };
5289
5290        if total_slots > 0 || cache_db_cleared > 0 {
5291            warn!(
5292                contracts_cleared = contract_count,
5293                backend_slots_purged = total_slots,
5294                cache_db_slots_purged = cache_db_cleared,
5295                "purged ALL storage from both cache layers (full refresh)"
5296            );
5297        }
5298        // All layer-2 storage was cleared → full base rebuild.
5299        self.invalidate_base();
5300        total_slots
5301    }
5302
5303    /// Enumerate all cached storage slots for a contract address.
5304    ///
5305    /// Returns the union of slot keys from both CacheDB overlay (layer 1) and
5306    /// BlockchainDb backend (layer 2). Used by the slot observation tracker to
5307    /// selectively purge only slots likely to have changed.
5308    pub fn enumerate_contract_slots(&self, address: Address) -> Vec<U256> {
5309        let mut slots: HashSet<U256> = HashSet::new();
5310
5311        // Layer 1: CacheDB overlay
5312        if let Some(db_account) = self.db.cache.accounts.get(&address) {
5313            slots.extend(db_account.storage.keys().copied());
5314        }
5315
5316        // Layer 2: BlockchainDb backend
5317        let storage = self.blockchain_db.storage().read();
5318        if let Some(backend_slots) = storage.get(&address) {
5319            slots.extend(backend_slots.keys().copied());
5320        }
5321
5322        slots.into_iter().collect()
5323    }
5324
5325    /// Return all contract addresses that have cached storage in either layer.
5326    ///
5327    /// Used by the observation-aware full purge to enumerate what needs checking.
5328    pub fn all_cached_contract_addresses(&self) -> Vec<Address> {
5329        let mut addrs: HashSet<Address> = HashSet::new();
5330
5331        // Layer 1: CacheDB overlay
5332        for (addr, account) in &self.db.cache.accounts {
5333            if !account.storage.is_empty() {
5334                addrs.insert(*addr);
5335            }
5336        }
5337
5338        // Layer 2: BlockchainDb backend
5339        let storage = self.blockchain_db.storage().read();
5340        for addr in storage.keys() {
5341            addrs.insert(*addr);
5342        }
5343
5344        addrs.into_iter().collect()
5345    }
5346
5347    /// Get the number of storage slots in the CacheDB overlay for a contract.
5348    ///
5349    /// This is useful for diagnostics: if a contract has slots in the CacheDB
5350    /// overlay, they will be served on EVM reads without going to the backend.
5351    pub fn cache_db_storage_slot_count(&self, address: Address) -> usize {
5352        self.db
5353            .cache
5354            .accounts
5355            .get(&address)
5356            .map(|a| a.storage.len())
5357            .unwrap_or(0)
5358    }
5359
5360    /// Simulate a call and compute `owner`'s net balance change for each token
5361    /// in `tokens` by reading `balanceOf(owner)` immediately before and after.
5362    ///
5363    /// Each delta is the signed `post - pre` difference (see
5364    /// [`CallSimulationResult::token_deltas`]). When `commit` is true the call's
5365    /// state changes are persisted to the CacheDB overlay; otherwise they are
5366    /// reverted. Unlike
5367    /// [`simulate_with_transfer_tracking`](Self::simulate_with_transfer_tracking),
5368    /// this measures deltas via pre/post balance reads (not transfer-event
5369    /// inspection). The returned [`access_list`](CallSimulationResult::access_list)
5370    /// includes the accounts and slots touched by the pre/post `balanceOf` reads
5371    /// and the simulated call.
5372    ///
5373    /// # Errors
5374    /// Returns an error if building the tx env fails, if a pre/post
5375    /// `balanceOf` read fails, or if the call does not `Success` (i.e. it
5376    /// reverted or halted). On error the simulation is reverted.
5377    pub fn simulate_call_with_balance_deltas(
5378        &mut self,
5379        from: Address,
5380        to: Address,
5381        calldata: Bytes,
5382        owner: Address,
5383        tokens: impl IntoIterator<Item = Address>,
5384        commit: bool,
5385    ) -> Result<CallSimulationResult> {
5386        let token_list: Vec<Address> = tokens.into_iter().collect();
5387
5388        let mut pre_balances = HashMap::with_capacity(token_list.len());
5389        let mut access_lists = Vec::with_capacity(token_list.len().saturating_mul(2) + 1);
5390        for token in &token_list {
5391            let mut evm = self.build_evm();
5392            let synthetic_beneficiary = Self::seed_synthetic_beneficiary(&mut evm);
5393            let (balance, access_list) =
5394                Self::erc20_balance_of_in_evm_isolated(&mut evm, from, *token, owner)?;
5395            Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
5396            pre_balances.insert(*token, balance);
5397            access_lists.push(access_list);
5398        }
5399
5400        let tx = Self::build_tx_env(from, to, calldata)?;
5401        let mut evm = self.build_evm();
5402        let synthetic_beneficiary = Self::seed_synthetic_beneficiary(&mut evm);
5403        let target_checkpoint = evm.journaled_state.checkpoint();
5404        let result = evm.transact_one(tx).map_err(CacheError::transact)?;
5405        let (logs, gas_used, output) = match result {
5406            ExecutionResult::Success {
5407                logs,
5408                gas_used,
5409                output,
5410                ..
5411            } => (logs, gas_used, output.into_data()),
5412            _ => {
5413                evm.journaled_state.checkpoint_revert(target_checkpoint);
5414                Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
5415                return Err(CacheError::CallNotSuccessful {
5416                    result: format!("{result:?}"),
5417                });
5418            }
5419        };
5420        access_lists.push(extract_access_list(&evm.journaled_state.state));
5421
5422        let mut token_deltas = HashMap::with_capacity(token_list.len());
5423        for token in &token_list {
5424            let (post, access_list) =
5425                match Self::erc20_balance_of_in_evm_isolated(&mut evm, from, *token, owner) {
5426                    Ok(result) => result,
5427                    Err(err) => {
5428                        evm.journaled_state.checkpoint_revert(target_checkpoint);
5429                        Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
5430                        return Err(err);
5431                    }
5432                };
5433            let pre = pre_balances.get(token).copied().unwrap_or_default();
5434            token_deltas.insert(*token, I256::from_raw(post) - I256::from_raw(pre));
5435            access_lists.push(access_list);
5436        }
5437
5438        let access_list = merge_access_lists(access_lists);
5439        if commit {
5440            Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
5441            evm.commit_inner();
5442        } else {
5443            evm.journaled_state.checkpoint_revert(target_checkpoint);
5444            Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
5445        }
5446
5447        Ok(CallSimulationResult {
5448            status: SimStatus::Success,
5449            gas_used,
5450            token_deltas,
5451            logs,
5452            access_list,
5453            output,
5454        })
5455    }
5456
5457    /// Simulate a call and track token balance changes using a TransferInspector.
5458    ///
5459    /// This method uses EVM inspection to capture ERC20 Transfer events during execution,
5460    /// eliminating the need for manual balance reads before/after the transaction.
5461    ///
5462    /// Returns:
5463    /// - `Ok(CallSimulationResult)` on successful execution
5464    /// - `Err(SimError::Revert(_))` when the transaction reverts (graceful failure)
5465    /// - `Err(SimError::Other(_))` for unexpected errors (should be propagated)
5466    #[instrument(level = "debug", skip(self, calldata, tokens), fields(calldata_len = calldata.len()))]
5467    pub fn simulate_with_transfer_tracking(
5468        &mut self,
5469        from: Address,
5470        to: Address,
5471        calldata: Bytes,
5472        owner: Address,
5473        tokens: Option<impl IntoIterator<Item = Address>>,
5474        commit: bool,
5475    ) -> SimulationResult<CallSimulationResult> {
5476        let tx = Self::build_tx_env(from, to, calldata).map_err(SimError::from)?;
5477        let inspector = TransferInspector::new();
5478        let mut evm = self.build_evm_with_inspector(inspector);
5479        let checkpoint = evm.journaled_state.checkpoint();
5480
5481        let result = evm
5482            .inspect_one_tx(tx)
5483            .map_err(|e| SimError::Other(SimHostError::transact(e)));
5484
5485        match result {
5486            Ok(ExecutionResult::Success {
5487                logs,
5488                gas_used,
5489                output,
5490                ..
5491            }) => {
5492                // Compute balance deltas from captured transfers
5493                let token_deltas = if let Some(token_list) = tokens {
5494                    evm.inspector.balance_deltas_for_tokens(owner, token_list)
5495                } else {
5496                    evm.inspector.balance_deltas(owner)
5497                };
5498
5499                // Log shared memory buffer capacity for profiling
5500                let memory_capacity = evm.ctx.local.shared_memory_buffer.borrow().capacity();
5501                trace!(
5502                    memory_capacity_bytes = memory_capacity,
5503                    memory_capacity_kb = memory_capacity / 1024,
5504                    "EVM shared memory buffer capacity after simulation"
5505                );
5506
5507                // Extract EIP-2930 access list from journaled state before commit/revert.
5508                // After inspect_one_tx, state contains all touched accounts and storage slots.
5509                let access_list = extract_access_list(&evm.journaled_state.state);
5510
5511                if commit {
5512                    evm.commit_inner();
5513                } else {
5514                    evm.journaled_state.checkpoint_revert(checkpoint);
5515                }
5516
5517                Ok(CallSimulationResult {
5518                    status: SimStatus::Success,
5519                    gas_used,
5520                    token_deltas,
5521                    logs,
5522                    access_list,
5523                    output: output.into_data(),
5524                })
5525            }
5526            Ok(ExecutionResult::Revert { gas_used, output }) => {
5527                evm.journaled_state.checkpoint_revert(checkpoint);
5528                Err(SimulationError::from_revert(gas_used, output).into())
5529            }
5530            Ok(ExecutionResult::Halt { reason, gas_used }) => {
5531                evm.journaled_state.checkpoint_revert(checkpoint);
5532                Err(SimError::Halt {
5533                    reason: format!("{reason:?}"),
5534                    gas_used,
5535                })
5536            }
5537            Err(err) => {
5538                evm.journaled_state.checkpoint_revert(checkpoint);
5539                Err(err)
5540            }
5541        }
5542    }
5543
5544    /// Simulate a call with transfer tracking without any prefetching.
5545    ///
5546    /// This is identical to `simulate_with_transfer_tracking` since we no longer
5547    /// do access list prefetching. Kept for API compatibility.
5548    pub fn simulate_with_transfer_tracking_raw(
5549        &mut self,
5550        from: Address,
5551        to: Address,
5552        calldata: Bytes,
5553        owner: Address,
5554        tokens: Option<impl IntoIterator<Item = Address>>,
5555        commit: bool,
5556    ) -> SimulationResult<CallSimulationResult> {
5557        self.simulate_with_transfer_tracking(from, to, calldata, owner, tokens, commit)
5558    }
5559
5560    /// Simulate an ordered transaction **bundle** over cumulative block state,
5561    /// with a revert policy and coinbase/miner-payment accounting (Phase 6
5562    /// Track A+B).
5563    ///
5564    /// This is a convenience wrapper: it snapshots the cache and runs the bundle
5565    /// on a fresh transient [`EvmOverlay`] via
5566    /// [`EvmOverlay::simulate_bundle`](crate::cache::EvmOverlay::simulate_bundle),
5567    /// which carries the full semantics (ordered cumulative state, the
5568    /// [`RevertPolicy`](crate::bundle::RevertPolicy), and coinbase accounting).
5569    ///
5570    /// The cache itself is **never** mutated — even when `opts.commit` is `true`.
5571    /// `commit` controls only whether the bundle's cumulative state is folded
5572    /// into the transient overlay (and is therefore moot here, since that overlay
5573    /// is dropped when this call returns). Snapshot the cache yourself and drive
5574    /// [`EvmOverlay::simulate_bundle`] directly when you need the committed
5575    /// overlay state to outlive the call (e.g. to chain a follow-up read).
5576    ///
5577    /// # Errors
5578    ///
5579    /// Returns [`SimError`] if a transaction environment cannot be built or revm
5580    /// fails to transact. A transaction reverting is reported through the
5581    /// per-transaction outcome and the revert policy, not as an error.
5582    pub fn simulate_bundle(
5583        &mut self,
5584        txs: &[crate::bundle::BundleTx],
5585        opts: &crate::bundle::BundleOptions,
5586    ) -> SimulationResult<crate::bundle::BundleResult> {
5587        let snapshot = self.snapshot();
5588        let mut overlay = EvmOverlay::new(snapshot, None);
5589        overlay.simulate_bundle(txs, opts)
5590    }
5591
5592    /// Deploy a contract via CREATE transaction and return the deployed address.
5593    ///
5594    /// The `creation_code` should include the init code with ABI-encoded constructor
5595    /// arguments appended. Nonce checks are disabled, so any `from` address works.
5596    ///
5597    /// Note: This commits the deployment to the CacheDB. Use a throw-away deployer
5598    /// address (e.g., `Address::ZERO`) to avoid side effects on real accounts.
5599    ///
5600    /// # Errors
5601    /// Returns an error if the CREATE tx env cannot be built, if the deployment
5602    /// reverts or halts, or if it succeeds but the EVM returns no contract
5603    /// address.
5604    pub fn deploy_contract(&mut self, from: Address, creation_code: Bytes) -> Result<Address> {
5605        let tx = TxEnv::builder()
5606            .caller(from)
5607            .kind(TxKind::Create)
5608            .data(creation_code)
5609            .value(U256::ZERO)
5610            .build()
5611            .map_err(CacheError::tx_env)?;
5612
5613        // Use a relaxed contract size limit for deployment. Arbitrum supports
5614        // larger contracts than the EIP-170 24576-byte limit via ArbOS.
5615        let mut evm = self.build_evm();
5616        evm.cfg.limit_contract_code_size = Some(usize::MAX);
5617        let result = evm.transact_commit(tx).map_err(CacheError::transact)?;
5618
5619        match result {
5620            ExecutionResult::Success { output, .. } => {
5621                let address = output
5622                    .address()
5623                    .copied()
5624                    .ok_or(CacheError::DeploymentMissingAddress)?;
5625                // A locally-deployed contract is divergence by construction:
5626                // record it so `etched_accounts` reports every non-chain code
5627                // site. The committed create left the runtime code in the
5628                // overlay; hash from there.
5629                let code_hash = self
5630                    .db
5631                    .cache
5632                    .accounts
5633                    .get(&address)
5634                    .map(|account| account.info.code_hash)
5635                    .unwrap_or(revm::primitives::KECCAK_EMPTY);
5636                self.code_seeds
5637                    .insert(address, CodeSeedState::Etched { code_hash });
5638                Ok(address)
5639            }
5640            ExecutionResult::Revert { output, .. } => Err(CacheError::DeploymentReverted {
5641                output_hex: alloy_primitives::hex::encode(&output),
5642            }),
5643            ExecutionResult::Halt { reason, .. } => Err(CacheError::DeploymentHalted {
5644                reason: format!("{reason:?}"),
5645            }),
5646        }
5647    }
5648
5649    /// Override the bytecode at `target` address with bytecode from `source` address.
5650    ///
5651    /// Copies only non-empty runtime code and code_hash; storage, balance, and nonce
5652    /// at `target` remain unchanged. `target` must already have non-empty runtime
5653    /// bytecode. Both the CacheDB overlay and BlockchainDb backend are updated,
5654    /// ensuring the override is visible to parallel EVM tasks sharing the same backend.
5655    ///
5656    /// # Errors
5657    /// Returns an error if `source` has no cached bytecode or its code is empty,
5658    /// if `target` cannot be loaded (it must already exist on the backend), or
5659    /// if `target` has no existing runtime bytecode to override. For synthetic
5660    /// `target` addresses that may not exist, use
5661    /// [`override_or_create_account_code`](Self::override_or_create_account_code).
5662    pub fn override_account_code(&mut self, source: Address, target: Address) -> Result<()> {
5663        self.override_account_code_with_missing_target(source, target, MissingTargetBehavior::Error)
5664    }
5665
5666    /// Override the bytecode at `target`, creating a default target account when absent.
5667    ///
5668    /// Use this for synthetic addresses in local simulations. For live forked
5669    /// accounts where storage/balance/nonce must be preserved, prefer
5670    /// [`Self::override_account_code`].
5671    pub fn override_or_create_account_code(
5672        &mut self,
5673        source: Address,
5674        target: Address,
5675    ) -> Result<()> {
5676        self.override_account_code_with_missing_target(
5677            source,
5678            target,
5679            MissingTargetBehavior::Create,
5680        )
5681    }
5682
5683    /// Override code at `target`, with explicit behavior for missing target accounts.
5684    ///
5685    /// This is intentionally **not** folded onto
5686    /// [`apply_update`](Self::apply_update)'s `Account` code patch: it copies code
5687    /// from a `source` account, preserves the target's existing balance/nonce/
5688    /// storage, and **unconditionally materializes** the target in the CacheDB
5689    /// overlay (the primary read path for EVM execution, required for the
5690    /// `Create` synthetic-target case). The generic primitive writes the overlay
5691    /// only when an account is already present, so the two are not
5692    /// behavior-equivalent. For a plain code overwrite that follows the
5693    /// dual-layer write-through policy, use
5694    /// `apply_update(StateUpdate::Account { patch: AccountPatch::default().code(..) })`.
5695    pub fn override_account_code_with_missing_target(
5696        &mut self,
5697        source: Address,
5698        target: Address,
5699        missing_target: MissingTargetBehavior,
5700    ) -> Result<()> {
5701        // Read deployed bytecode from source (in CacheDB overlay after deploy_contract)
5702        let source_code = self
5703            .db
5704            .cache
5705            .accounts
5706            .get(&source)
5707            .and_then(|a| a.info.code.clone())
5708            .ok_or(CacheError::MissingSourceBytecode {
5709                source_address: source,
5710            })?;
5711        Self::ensure_runtime_code(source, Some(&source_code), "source")?;
5712
5713        let code_hash = source_code.hash_slow();
5714        debug!(
5715            source = %source,
5716            target = %target,
5717            code_size = source_code.len(),
5718            "Overriding account bytecode"
5719        );
5720
5721        let mut target_info = self.target_account_info(target, missing_target)?;
5722
5723        if matches!(missing_target, MissingTargetBehavior::Error) {
5724            Self::ensure_runtime_code(target, target_info.code.as_ref(), "target")?;
5725        }
5726
5727        target_info.code = Some(source_code);
5728        target_info.code_hash = code_hash;
5729
5730        // Update CacheDB overlay (primary read path for EVM execution).
5731        self.db.insert_account_info(target, target_info.clone());
5732
5733        // Update BlockchainDb backend (shared with parallel tasks)
5734        {
5735            let mut accounts = self.blockchain_db.accounts().write();
5736            accounts.insert(target, target_info);
5737        }
5738
5739        // Layer 2 changed → invalidate the memoized base for `target`. The layer-1
5740        // `insert_account_info` above currently shadows it on every snapshot read,
5741        // but we dirty unconditionally for uniformity with every other layer-2 write
5742        // site (D2), so base correctness never relies on that shadowing invariant.
5743        self.mark_base_dirty(target);
5744
5745        // Every locally-divergent code write is visible in one place: the
5746        // override target joins the etched set (see `etched_accounts`).
5747        self.code_seeds
5748            .insert(target, CodeSeedState::Etched { code_hash });
5749
5750        Ok(())
5751    }
5752
5753    /// Verify every [`CodeSeedState::Pending`] canonical code claim against
5754    /// the chain at the pinned block — one bulk `eth_call` for the whole set.
5755    ///
5756    /// Per-address outcomes (see [`CodeVerifyReport`]):
5757    /// - **match** ⇒ marked [`CodeSeedState::Verified`] (never re-checked;
5758    ///   post-EIP-6780 code is immutable) and the account's real balance is
5759    ///   patched in from the same response — pure materialization of
5760    ///   pinned-block truth, so it does **not** bump the
5761    ///   [snapshot generation](Self::snapshot_generation);
5762    /// - **mismatch / not-deployed / code-less** ⇒
5763    ///   [`purge_account`](Self::purge_account) (both layers **and** the
5764    ///   mark; the purge path bumps the generation) — the next touch
5765    ///   refetches authoritative chain state;
5766    /// - **transport failure** (the whole call, an omitted address, or the
5767    ///   `MULTICALL3_ADDRESS` extractor-host caveat) ⇒ still `Pending`,
5768    ///   reported `unverifiable` — a failed read proves nothing, so it never
5769    ///   promotes and never destroys a seed.
5770    ///
5771    /// With no pending seeds this is a no-op that needs no fetcher. Verified
5772    /// seeds are skipped forever, so calling this repeatedly (or from every
5773    /// cold-start round) costs nothing once the set is settled.
5774    ///
5775    /// # Errors
5776    /// [`CacheError::MissingAccountFieldsFetcher`] when pending seeds exist
5777    /// but no [`AccountFieldsFetchFn`] is installed (a
5778    /// [`from_backend`](Self::from_backend) cache without
5779    /// [`set_account_fields_fetcher`](Self::set_account_fields_fetcher)).
5780    pub fn verify_code_seeds(&mut self) -> Result<CodeVerifyReport> {
5781        let pending = self.pending_code_seeds();
5782        if pending.is_empty() {
5783            return Ok(CodeVerifyReport::default());
5784        }
5785        let fetcher = self
5786            .account_fields_fetcher
5787            .clone()
5788            .ok_or(CacheError::MissingAccountFieldsFetcher)?;
5789
5790        let mut report = CodeVerifyReport::default();
5791
5792        // The extractor is hosted at MULTICALL3_ADDRESS under the eth_call
5793        // override, so querying that address would report the extractor's own
5794        // hash — a seed there is unverifiable by this path (use eth_getProof).
5795        let (host, query): (Vec<Address>, Vec<Address>) = pending
5796            .into_iter()
5797            .partition(|address| *address == crate::multicall::MULTICALL3_ADDRESS);
5798        for address in host {
5799            report.unverifiable.push((
5800                address,
5801                "the account-fields extractor is hosted at this address under the eth_call \
5802                 override; verify it via the eth_getProof path instead"
5803                    .to_string(),
5804            ));
5805        }
5806        if query.is_empty() {
5807            return Ok(report);
5808        }
5809
5810        let samples = match (fetcher)(query.clone(), self.block) {
5811            Ok(samples) => samples,
5812            Err(error) => {
5813                // Fail-safe on transport: every seed stays Pending.
5814                let reason = error.to_string();
5815                report
5816                    .unverifiable
5817                    .extend(query.into_iter().map(|address| (address, reason.clone())));
5818                return Ok(report);
5819            }
5820        };
5821        let by_address: HashMap<Address, AccountFieldsSample> = samples.into_iter().collect();
5822
5823        let verified_at_block = self.block_number.unwrap_or_default();
5824        for address in query {
5825            let Some(CodeSeedState::Pending {
5826                code_hash: expected,
5827            }) = self.code_seeds.get(&address).cloned()
5828            else {
5829                // Unreachable in practice (the set was snapshotted above);
5830                // skip rather than misclassify.
5831                continue;
5832            };
5833            let Some(sample) = by_address.get(&address) else {
5834                report.unverifiable.push((
5835                    address,
5836                    "account-fields fetcher returned no sample for this address".to_string(),
5837                ));
5838                continue;
5839            };
5840
5841            if sample.code_hash == expected {
5842                self.code_seeds.insert(
5843                    address,
5844                    CodeSeedState::Verified {
5845                        code_hash: expected,
5846                        verified_at_block,
5847                    },
5848                );
5849                self.materialize_verified_balance(address, sample.balance);
5850                report.verified.push(address);
5851            } else if sample.code_hash == B256::ZERO {
5852                self.purge_account(address);
5853                report.not_deployed.push(address);
5854            } else if sample.code_hash == revm::primitives::KECCAK_EMPTY {
5855                self.purge_account(address);
5856                report.codeless.push(address);
5857            } else {
5858                self.purge_account(address);
5859                report.mismatched.push(CodeMismatch {
5860                    address,
5861                    expected,
5862                    actual: sample.code_hash,
5863                });
5864            }
5865        }
5866        Ok(report)
5867    }
5868
5869    /// Patch a just-verified seed's balance to the on-chain value from the
5870    /// verification sample — in both layers, **without** a
5871    /// snapshot-generation bump: confirming a claim and materializing
5872    /// pinned-block truth is the prefetch class of write, not a mutation.
5873    /// The overlay is only patched when the account already has an entry
5874    /// there (it always does for a seeded account), mirroring the layer
5875    /// policy of [`inject_storage_batch_fresh`](Self::inject_storage_batch_fresh).
5876    fn materialize_verified_balance(&mut self, address: Address, balance: U256) {
5877        if let Some(account) = self.db.cache.accounts.get_mut(&address) {
5878            account.info.balance = balance;
5879        }
5880        {
5881            let mut accounts = self.blockchain_db.accounts().write();
5882            if let Some(info) = accounts.get_mut(&address) {
5883                info.balance = balance;
5884            }
5885        }
5886        self.mark_base_dirty(address);
5887    }
5888
5889    /// Local (already-materialized) account info for `address` — CacheDB
5890    /// overlay first, then the BlockchainDb backend. Never fetches: code-seed
5891    /// decisions are made strictly against what the cache already holds.
5892    fn local_account_info(&self, address: Address) -> Option<AccountInfo> {
5893        if let Some(account) = self.db.cache.accounts.get(&address) {
5894            return Some(account.info.clone());
5895        }
5896        self.blockchain_db.accounts().read().get(&address).cloned()
5897    }
5898
5899    /// Dual-layer account write shared by [`seed_account_code_with`](Self::seed_account_code_with)
5900    /// and [`etch_account_code`](Self::etch_account_code): CacheDB overlay
5901    /// (the primary EVM read path) plus the BlockchainDb backend (shared with
5902    /// parallel tasks), base invalidation, and a snapshot-generation bump —
5903    /// a code write changes executable state (see
5904    /// [`snapshot_generation`](Self::snapshot_generation)).
5905    fn write_marked_code(&mut self, address: Address, info: AccountInfo) {
5906        self.db.insert_account_info(address, info.clone());
5907        {
5908            let mut accounts = self.blockchain_db.accounts().write();
5909            accounts.insert(address, info);
5910        }
5911        self.mark_base_dirty(address);
5912        self.bump_snapshot_generation();
5913    }
5914
5915    /// Seed canonical runtime code for `address` without fetching it.
5916    ///
5917    /// The claim is marked [`CodeSeedState::Pending`] until
5918    /// [`verify_code_seeds`](Self::verify_code_seeds) confirms it against the
5919    /// on-chain `EXTCODEHASH` (or the cold-start driver's `verify_code` phase
5920    /// does). Because the account is materialized in both cache layers, the
5921    /// lazy backend never fires its balance/nonce/code RPC triple for it.
5922    ///
5923    /// Defaults: nonce 1 (the EIP-161 contract minimum — exact for any
5924    /// contract that never `CREATE`s) and balance `ZERO` until verification
5925    /// patches the real value from the same response. Use
5926    /// [`seed_account_code_with`](Self::seed_account_code_with) to supply
5927    /// both explicitly.
5928    ///
5929    /// Conflict rules (chain-fetched state is authoritative over templates):
5930    /// seeding an **unmarked** address that already holds RPC-origin code
5931    /// with the same hash marks it `Verified` immediately (zero RPC — the
5932    /// warm-cache fast path); a differing hash (including a code-less EOA) is
5933    /// [`CacheError::CodeSeedConflict`] and leaves the cached code untouched.
5934    /// Re-seeding a marked address overwrites and restarts the claim as
5935    /// `Pending`.
5936    ///
5937    /// Returns the keccak256 hash recorded for the claim.
5938    ///
5939    /// # Errors
5940    /// [`CacheError::CodeSeedEmpty`] for empty `code`;
5941    /// [`CacheError::CodeSeedConflict`] as above.
5942    pub fn seed_account_code(&mut self, address: Address, code: Bytes) -> Result<B256> {
5943        self.seed_account_code_with(address, code, 1, U256::ZERO)
5944    }
5945
5946    /// [`seed_account_code`](Self::seed_account_code) with explicit `nonce`
5947    /// and provisional `balance` for the materialized account. Verification
5948    /// still overwrites the balance with the on-chain value on a match; the
5949    /// nonce keeps the supplied value (an exact nonce needs the
5950    /// `eth_getProof` path and only matters for contracts that `CREATE`).
5951    pub fn seed_account_code_with(
5952        &mut self,
5953        address: Address,
5954        code: Bytes,
5955        nonce: u64,
5956        balance: U256,
5957    ) -> Result<B256> {
5958        if code.is_empty() {
5959            return Err(CacheError::CodeSeedEmpty { address });
5960        }
5961        let bytecode = Bytecode::new_raw(code);
5962        let code_hash = bytecode.hash_slow();
5963
5964        // Unmarked + locally present ⇒ RPC-origin, which is authoritative.
5965        if !self.code_seeds.contains_key(&address)
5966            && let Some(existing) = self.local_account_info(address)
5967        {
5968            if existing.code_hash == code_hash {
5969                // Hash equality proves byte equality: the claim is already
5970                // confirmed by chain-fetched state, zero RPC. If the restored
5971                // account is missing its code *bytes* (binary state without a
5972                // bytecodes.bin entry), the seed supplies exactly the bytes
5973                // the recorded hash committed to — a free repair.
5974                if existing
5975                    .code
5976                    .as_ref()
5977                    .is_none_or(|existing_code| existing_code.is_empty())
5978                {
5979                    let mut info = existing;
5980                    info.code = Some(bytecode);
5981                    info.code_hash = code_hash;
5982                    self.write_marked_code(address, info);
5983                }
5984                self.code_seeds.insert(
5985                    address,
5986                    CodeSeedState::Verified {
5987                        code_hash,
5988                        verified_at_block: self.block_number.unwrap_or_default(),
5989                    },
5990                );
5991                return Ok(code_hash);
5992            }
5993            return Err(CacheError::CodeSeedConflict {
5994                address,
5995                cached: existing.code_hash,
5996                seeded: code_hash,
5997            });
5998        }
5999
6000        // Absent, or an existing mark being re-seeded: write the claim.
6001        // A marked account keeps its current balance/nonce; a fresh one gets
6002        // the caller's provisional values.
6003        let mut info = self.local_account_info(address).unwrap_or(AccountInfo {
6004            balance,
6005            nonce,
6006            code_hash: revm::primitives::KECCAK_EMPTY,
6007            code: None,
6008            account_id: None,
6009        });
6010        info.code = Some(bytecode);
6011        info.code_hash = code_hash;
6012        self.write_marked_code(address, info);
6013        self.code_seeds
6014            .insert(address, CodeSeedState::Pending { code_hash });
6015        Ok(code_hash)
6016    }
6017
6018    /// Etch deliberately-local runtime code at `address` — the raw-bytes
6019    /// sibling of [`override_or_create_account_code`](Self::override_or_create_account_code),
6020    /// with no source account needed.
6021    ///
6022    /// Marks [`CodeSeedState::Etched`]: never verified, excluded from all
6023    /// canonical machinery, and reported via
6024    /// [`etched_accounts`](Self::etched_accounts) so local divergence stays
6025    /// visible. Preserves the existing balance/nonce/storage when the account
6026    /// is already present; creates a default account otherwise. Overwrites
6027    /// any prior code or mark — divergence is the caller's explicit intent.
6028    ///
6029    /// Returns the keccak256 hash of the etched code.
6030    ///
6031    /// # Errors
6032    /// [`CacheError::CodeSeedEmpty`] for empty `code`.
6033    pub fn etch_account_code(&mut self, address: Address, code: Bytes) -> Result<B256> {
6034        if code.is_empty() {
6035            return Err(CacheError::CodeSeedEmpty { address });
6036        }
6037        let bytecode = Bytecode::new_raw(code);
6038        let code_hash = bytecode.hash_slow();
6039        let mut info = self.local_account_info(address).unwrap_or_default();
6040        info.code = Some(bytecode);
6041        info.code_hash = code_hash;
6042        self.write_marked_code(address, info);
6043        self.code_seeds
6044            .insert(address, CodeSeedState::Etched { code_hash });
6045        Ok(code_hash)
6046    }
6047
6048    /// The code-seed mark for `address`, if any. `None` means RPC-origin:
6049    /// the code (if present) was fetched from the provider and is trusted as
6050    /// chain state.
6051    pub fn code_seed_state(&self, address: &Address) -> Option<&CodeSeedState> {
6052        self.code_seeds.get(address)
6053    }
6054
6055    /// Addresses whose canonical code claims still await verification
6056    /// ([`CodeSeedState::Pending`]), sorted for deterministic iteration.
6057    /// This is the implicit work set of
6058    /// [`verify_code_seeds`](Self::verify_code_seeds) and the cold-start
6059    /// `verify_code` phase.
6060    pub fn pending_code_seeds(&self) -> Vec<Address> {
6061        let mut pending: Vec<Address> = self
6062            .code_seeds
6063            .iter()
6064            .filter_map(|(addr, state)| {
6065                matches!(state, CodeSeedState::Pending { .. }).then_some(*addr)
6066            })
6067            .collect();
6068        pending.sort();
6069        pending
6070    }
6071
6072    /// Addresses whose code deliberately diverges from the chain
6073    /// ([`CodeSeedState::Etched`]), sorted for deterministic iteration. This
6074    /// is the health surface for local divergence: everything written through
6075    /// [`etch_account_code`](Self::etch_account_code),
6076    /// [`override_account_code`](Self::override_account_code) and friends, or
6077    /// [`deploy_contract`](Self::deploy_contract) appears here.
6078    pub fn etched_accounts(&self) -> Vec<Address> {
6079        let mut etched: Vec<Address> = self
6080            .code_seeds
6081            .iter()
6082            .filter_map(|(addr, state)| {
6083                matches!(state, CodeSeedState::Etched { .. }).then_some(*addr)
6084            })
6085            .collect();
6086        etched.sort();
6087        etched
6088    }
6089
6090    pub(crate) fn require_contract_target(&self, target: Address) -> Result<()> {
6091        let target_info = self.target_account_info(target, MissingTargetBehavior::Error)?;
6092        Self::ensure_runtime_code(target, target_info.code.as_ref(), "target")
6093    }
6094
6095    fn target_account_info(
6096        &self,
6097        target: Address,
6098        missing_target: MissingTargetBehavior,
6099    ) -> Result<AccountInfo> {
6100        if let Some(account) = self.db.cache.accounts.get(&target) {
6101            // A NotExisting overlay account is absent to the EVM (revm
6102            // `DbAccount::info()` returns None); treat it as a missing target
6103            // rather than returning its stale/default info.
6104            if !matches!(account.account_state, AccountState::NotExisting) {
6105                return Ok(account.info.clone());
6106            }
6107        }
6108
6109        match missing_target {
6110            MissingTargetBehavior::Create => Ok(AccountInfo::default()),
6111            MissingTargetBehavior::Error => {
6112                use revm::database_interface::DatabaseRef;
6113                self.backend
6114                    .basic_ref(target)
6115                    .map_err(|e| CacheError::TargetAccountFetch {
6116                        target,
6117                        details: format!("{e:?}"),
6118                    })?
6119                    .ok_or(CacheError::MissingTargetAccount { target })
6120            }
6121        }
6122    }
6123
6124    fn ensure_runtime_code(address: Address, code: Option<&Bytecode>, role: &str) -> Result<()> {
6125        if code.is_some_and(|code| !code.is_empty()) {
6126            return Ok(());
6127        }
6128
6129        Err(CacheError::MissingRuntimeCode {
6130            role: match role {
6131                "source" => "source",
6132                "target" => "target",
6133                _ => "account",
6134            },
6135            address,
6136        })
6137    }
6138}
6139
6140/// Read-only state view for the event pipeline (Pillar B.2): a decoder reads the
6141/// current cached value of a slot through [`cached_storage_value`](EvmCache::cached_storage_value),
6142/// which never touches RPC and is `account_state`-aware (a cold slot reads
6143/// `None`).
6144impl crate::events::StateView for EvmCache {
6145    fn storage(&self, address: Address, slot: U256) -> Option<U256> {
6146        self.cached_storage_value(address, slot)
6147    }
6148}
6149
6150impl EvmCache {
6151    /// Create a LocalContext that reuses the shared memory buffer.
6152    ///
6153    /// The buffer is cleared (length set to 0) but capacity is preserved,
6154    /// avoiding repeated allocations across simulations.
6155    fn make_local_context(&self) -> LocalContext {
6156        // Clear the buffer but preserve capacity. `Vec::clear` sets the length
6157        // to 0 without releasing the allocation, so the buffer is reused across
6158        // simulations.
6159        self.shared_memory_buffer.borrow_mut().clear();
6160
6161        LocalContext {
6162            shared_memory_buffer: self.shared_memory_buffer.clone(),
6163            precompile_error_message: None,
6164        }
6165    }
6166
6167    fn build_evm(&mut self) -> CacheEvm<'_> {
6168        let local = self.make_local_context();
6169        let chain_id = self.chain_id;
6170        let mut evm = Context::mainnet()
6171            .with_db(&mut self.db)
6172            .with_local(local)
6173            .modify_cfg_chained(|cfg| {
6174                cfg.disable_nonce_check = true;
6175                cfg.disable_eip3607 = true;
6176                cfg.disable_base_fee = true;
6177                cfg.disable_balance_check = true;
6178                cfg.chain_id = chain_id;
6179                cfg.limit_contract_code_size = None;
6180                cfg.tx_chain_id_check = false;
6181                cfg.spec = self.spec_id;
6182            })
6183            .build_mainnet();
6184
6185        let timestamp = self
6186            .timestamp_override
6187            .unwrap_or_else(|| unix_timestamp_secs_saturating(SystemTime::now()));
6188        evm.block.timestamp = U256::from(timestamp);
6189        if let Some(number) = self.block_number {
6190            evm.block.number = U256::from(number);
6191        }
6192        if let Some(basefee) = self.basefee {
6193            evm.block.basefee = basefee;
6194        }
6195        if let Some(coinbase) = self.coinbase {
6196            evm.block.beneficiary = coinbase;
6197        }
6198        if let Some(prevrandao) = self.prevrandao {
6199            evm.block.prevrandao = Some(prevrandao);
6200        }
6201        if let Some(gas_limit) = self.block_gas_limit {
6202            evm.block.gas_limit = gas_limit;
6203        }
6204        evm
6205    }
6206
6207    fn build_evm_with_inspector<INSP>(&mut self, inspector: INSP) -> InspectorCacheEvm<'_, INSP> {
6208        let local = self.make_local_context();
6209        let chain_id = self.chain_id;
6210        let mut evm = Context::mainnet()
6211            .with_db(&mut self.db)
6212            .with_local(local)
6213            .modify_cfg_chained(|cfg| {
6214                cfg.disable_nonce_check = true;
6215                cfg.disable_eip3607 = true;
6216                cfg.disable_base_fee = true;
6217                cfg.disable_balance_check = true;
6218                cfg.chain_id = chain_id;
6219                cfg.limit_contract_code_size = None;
6220                cfg.tx_chain_id_check = false;
6221                cfg.spec = self.spec_id;
6222            })
6223            .build_mainnet_with_inspector(inspector);
6224
6225        let timestamp = self
6226            .timestamp_override
6227            .unwrap_or_else(|| unix_timestamp_secs_saturating(SystemTime::now()));
6228        evm.block.timestamp = U256::from(timestamp);
6229        if let Some(number) = self.block_number {
6230            evm.block.number = U256::from(number);
6231        }
6232        if let Some(basefee) = self.basefee {
6233            evm.block.basefee = basefee;
6234        }
6235        if let Some(coinbase) = self.coinbase {
6236            evm.block.beneficiary = coinbase;
6237        }
6238        if let Some(prevrandao) = self.prevrandao {
6239            evm.block.prevrandao = Some(prevrandao);
6240        }
6241        if let Some(gas_limit) = self.block_gas_limit {
6242            evm.block.gas_limit = gas_limit;
6243        }
6244        evm
6245    }
6246
6247    fn build_tx_env(from: Address, to: Address, calldata: Bytes) -> Result<TxEnv> {
6248        Self::build_tx_env_with(from, to, calldata, &TxConfig::default())
6249    }
6250
6251    fn build_tx_env_with(
6252        from: Address,
6253        to: Address,
6254        calldata: Bytes,
6255        tx: &TxConfig,
6256    ) -> Result<TxEnv> {
6257        let mut builder = TxEnv::builder()
6258            .caller(from)
6259            .kind(TxKind::Call(to))
6260            .data(calldata)
6261            .value(tx.value);
6262        if let Some(gas_limit) = tx.gas_limit {
6263            builder = builder.gas_limit(gas_limit);
6264        }
6265        if let Some(gas_price) = tx.gas_price {
6266            builder = builder.gas_price(gas_price);
6267        }
6268        if let Some(nonce) = tx.nonce {
6269            builder = builder.nonce(nonce);
6270        }
6271        if let Some(access_list) = &tx.access_list {
6272            builder = builder.access_list(access_list.clone());
6273        }
6274        builder.build().map_err(CacheError::tx_env)
6275    }
6276
6277    fn call_sol_with_commit<C>(
6278        &mut self,
6279        from: Address,
6280        to: Address,
6281        call: C,
6282        tx: &TxConfig,
6283        commit: bool,
6284    ) -> Result<C::Return>
6285    where
6286        C: SolCall,
6287    {
6288        let calldata = Bytes::from(call.abi_encode());
6289        let result = self.call_raw_with(from, to, calldata, commit, tx)?;
6290        Self::decode_sol_call_result::<C>(from, to, result)
6291    }
6292
6293    fn decode_sol_call_result<C>(
6294        from: Address,
6295        to: Address,
6296        result: ExecutionResult,
6297    ) -> Result<C::Return>
6298    where
6299        C: SolCall,
6300    {
6301        match result {
6302            ExecutionResult::Success { output, .. } => {
6303                let output = output.into_data();
6304                C::abi_decode_returns(&output).map_err(|error| CacheError::SolCallDecode {
6305                    signature: C::SIGNATURE,
6306                    from,
6307                    to,
6308                    output_len: output.len(),
6309                    details: format!("{error:?}"),
6310                })
6311            }
6312            other => Err(CacheError::SolCallFailed {
6313                signature: C::SIGNATURE,
6314                from,
6315                to,
6316                result: format!("{other:?}"),
6317            }),
6318        }
6319    }
6320
6321    fn erc20_balance_of_in_evm(
6322        evm: &mut CacheEvm<'_>,
6323        caller: Address,
6324        token: Address,
6325        owner: Address,
6326    ) -> Result<U256> {
6327        let call = IERC20::balanceOfCall { target: owner };
6328        let tx = Self::build_tx_env(caller, token, Bytes::from(call.abi_encode()))?;
6329        let result = evm.transact_one(tx).map_err(CacheError::transact)?;
6330
6331        match result {
6332            ExecutionResult::Success { output, .. } => {
6333                let out = output.into_data();
6334                let balance = IERC20::balanceOfCall::abi_decode_returns(&out).map_err(|e| {
6335                    CacheError::Decode {
6336                        what: "ERC20 balanceOf return data",
6337                        details: format!("{e:?}"),
6338                    }
6339                })?;
6340                Ok(balance)
6341            }
6342            _ => Err(CacheError::CallNotSuccessful {
6343                result: format!("{result:?}"),
6344            }),
6345        }
6346    }
6347
6348    fn erc20_balance_of_in_evm_isolated(
6349        evm: &mut CacheEvm<'_>,
6350        caller: Address,
6351        token: Address,
6352        owner: Address,
6353    ) -> Result<(U256, AccessList)> {
6354        let state_before = evm.journaled_state.state.clone();
6355        let checkpoint = evm.journaled_state.checkpoint();
6356        let result = Self::erc20_balance_of_in_evm(evm, caller, token, owner);
6357        let access_list = extract_access_list(&evm.journaled_state.state);
6358        evm.journaled_state.checkpoint_revert(checkpoint);
6359        evm.journaled_state.state = state_before;
6360        result.map(|balance| (balance, access_list))
6361    }
6362
6363    fn seed_synthetic_beneficiary(evm: &mut CacheEvm<'_>) -> Option<Address> {
6364        let beneficiary = evm.block.beneficiary;
6365        if evm.journaled_state.state.contains_key(&beneficiary) {
6366            return None;
6367        }
6368        evm.journaled_state
6369            .state
6370            .insert(beneficiary, Account::from(AccountInfo::default()));
6371        Some(beneficiary)
6372    }
6373
6374    fn remove_synthetic_beneficiary(evm: &mut CacheEvm<'_>, beneficiary: Option<Address>) {
6375        if let Some(beneficiary) = beneficiary {
6376            evm.journaled_state.state.remove(&beneficiary);
6377        }
6378    }
6379}
6380
6381/// A session for executing multiple EVM operations without committing to the underlying DB.
6382///
6383/// Changes made within a session are tracked in the EVM's journaled state. Call `commit()` to
6384/// persist changes to the underlying database, or simply drop the session to discard
6385/// all changes.
6386///
6387/// Note: For checkpoint/restore functionality across multiple transactions, use
6388/// `EvmCache::checkpoint()` and `EvmCache::restore()` instead, as the EVM journal
6389/// is cleared after each transaction.
6390pub struct EvmSession<'a> {
6391    evm: CacheEvm<'a>,
6392}
6393
6394impl<'a> EvmSession<'a> {
6395    /// Execute a call within the session.
6396    ///
6397    /// If `commit` is true, changes are persisted to the session's journaled state.
6398    /// If `commit` is false, the call is executed but its effects are immediately reverted.
6399    ///
6400    /// Note: Changes are not persisted to the underlying CacheDB until `commit()` is called
6401    /// on the session itself.
6402    pub fn call_raw(
6403        &mut self,
6404        from: Address,
6405        to: Address,
6406        calldata: Bytes,
6407        commit: bool,
6408    ) -> Result<ExecutionResult> {
6409        let tx = EvmCache::build_tx_env(from, to, calldata)?;
6410
6411        if commit {
6412            self.evm.transact_one(tx).map_err(CacheError::transact)
6413        } else {
6414            let checkpoint = self.evm.journaled_state.checkpoint();
6415            let result = self.evm.transact_one(tx);
6416            self.evm.journaled_state.checkpoint_revert(checkpoint);
6417            result.map_err(CacheError::transact)
6418        }
6419    }
6420
6421    /// Commit all session changes to the underlying database.
6422    ///
6423    /// This persists all changes made during the session to the CacheDB.
6424    pub fn commit(mut self) {
6425        self.evm.commit_inner();
6426    }
6427
6428    /// Get access to the underlying EVM for advanced operations.
6429    ///
6430    /// This exposes revm internals and bypasses the cache's two-layer
6431    /// consistency model: state mutated directly through the journaled EVM
6432    /// lands in the session's journal, not the BlockchainDb backend, and is
6433    /// only flushed to the CacheDB overlay on [`commit`](Self::commit). Use
6434    /// with care.
6435    pub fn evm(&mut self) -> &mut CacheEvm<'a> {
6436        &mut self.evm
6437    }
6438}
6439
6440/// Automatically flush the cache to disk when the EvmCache is dropped.
6441impl Drop for EvmCache {
6442    fn drop(&mut self) {
6443        if self.cache_config.is_some() {
6444            debug!("Flushing EVM cache on drop");
6445            if let Err(e) = self.flush() {
6446                warn!(error = %e, "Failed to flush EVM cache on drop");
6447            }
6448        }
6449    }
6450}
6451
6452#[cfg(test)]
6453mod shared_memory_capacity_tests {
6454    use super::SharedMemoryCapacity as Cap;
6455
6456    #[test]
6457    fn default_is_fixed_64k() {
6458        assert_eq!(Cap::default(), Cap::Fixed(64 * 1024));
6459    }
6460
6461    #[test]
6462    fn fixed_ignores_loaded_slots() {
6463        assert_eq!(Cap::Fixed(8_192).resolve(10_000_000), 8_192);
6464        assert_eq!(Cap::Fixed(0).resolve(123), 0);
6465    }
6466
6467    #[test]
6468    fn auto_floors_clamps_and_scales() {
6469        // Nothing / little loaded → floor.
6470        assert_eq!(Cap::Auto.resolve(0), Cap::MIN_AUTO);
6471        assert_eq!(Cap::Auto.resolve(1_000), Cap::MIN_AUTO); // 16 KiB < 64 KiB floor
6472        // Linear region (16 bytes/slot).
6473        assert_eq!(Cap::Auto.resolve(10_000), 160_000);
6474        assert_eq!(Cap::Auto.resolve(100_000), 1_600_000);
6475        // Ceiling.
6476        assert_eq!(Cap::Auto.resolve(usize::MAX), Cap::MAX_AUTO);
6477        assert_eq!(Cap::Auto.resolve(262_144), Cap::MAX_AUTO); // 262_144 * 16 == 4 MiB
6478    }
6479}
6480
6481/// Tests that exercise the generic cache engine.
6482#[cfg(test)]
6483mod core_tests {
6484    use super::*;
6485
6486    #[test]
6487    fn parses_prestate_diff_trace_values_and_cleared_slots() {
6488        let trace = serde_json::json!([
6489            {
6490                "result": {
6491                    "pre": {
6492                        "0x4242424242424242424242424242424242424242": {
6493                            "storage": {
6494                                "0x01": "0x05",
6495                                "0x02": "0x06"
6496                            }
6497                        }
6498                    },
6499                    "post": {
6500                        "0x4242424242424242424242424242424242424242": {
6501                            "balance": 10,
6502                            "nonce": "0x0a",
6503                            "code": "0x6001",
6504                            "storage": {
6505                                "0x01": "0x0b"
6506                            }
6507                        }
6508                    }
6509                }
6510            }
6511        ]);
6512
6513        let diff = parse_block_state_diff_trace(&trace).unwrap();
6514
6515        assert_eq!(diff.accounts.len(), 1);
6516        let account = &diff.accounts[0];
6517        assert_eq!(account.address, Address::repeat_byte(0x42));
6518        assert_eq!(account.balance, Some(U256::from(10)));
6519        assert_eq!(account.nonce, Some(10));
6520        assert_eq!(account.code, Some(Bytes::from(vec![0x60, 0x01])));
6521        assert_eq!(
6522            account.storage,
6523            vec![
6524                BlockStateStorageDiff {
6525                    slot: U256::from(1),
6526                    value: U256::from(11),
6527                },
6528                BlockStateStorageDiff {
6529                    slot: U256::from(2),
6530                    value: U256::ZERO,
6531                },
6532            ]
6533        );
6534    }
6535
6536    #[test]
6537    fn parses_prestate_diff_trace_account_deletion() {
6538        // A SELFDESTRUCTed account appears in `pre` but is entirely absent
6539        // from `post`. The merged diff must carry its explicit post-deletion
6540        // fields (zero balance/nonce, empty code) — and, when the account had
6541        // storage, zeroed slots — so account-target resyncs resolve from the
6542        // trace instead of falling back to point reads.
6543        let trace = serde_json::json!([
6544            {
6545                "result": {
6546                    "pre": {
6547                        // Deleted WITH storage history in the trace.
6548                        "0x4242424242424242424242424242424242424242": {
6549                            "balance": "0x64",
6550                            "nonce": "0x01",
6551                            "code": "0x6001",
6552                            "storage": { "0x01": "0x05" }
6553                        },
6554                        // Deleted WITHOUT any storage entry (the previously
6555                        // missed case).
6556                        "0x1111111111111111111111111111111111111111": {
6557                            "balance": "0x0a"
6558                        }
6559                    },
6560                    "post": {}
6561                }
6562            }
6563        ]);
6564
6565        let diff = parse_block_state_diff_trace(&trace).unwrap();
6566        assert_eq!(diff.accounts.len(), 2);
6567
6568        let bare = &diff.accounts[0]; // 0x11.. sorts first
6569        assert_eq!(bare.address, Address::repeat_byte(0x11));
6570        assert_eq!(bare.balance, Some(U256::ZERO));
6571        assert_eq!(bare.nonce, Some(0));
6572        assert_eq!(bare.code, Some(Bytes::new()));
6573        assert!(bare.storage.is_empty());
6574
6575        let stored = &diff.accounts[1];
6576        assert_eq!(stored.address, Address::repeat_byte(0x42));
6577        assert_eq!(stored.balance, Some(U256::ZERO));
6578        assert_eq!(stored.nonce, Some(0));
6579        assert_eq!(stored.code, Some(Bytes::new()));
6580        assert_eq!(
6581            stored.storage,
6582            vec![BlockStateStorageDiff {
6583                slot: U256::from(1),
6584                value: U256::ZERO,
6585            }]
6586        );
6587    }
6588
6589    #[test]
6590    fn parses_prestate_diff_trace_deletion_then_recreation_keeps_final_state() {
6591        // tx1 deletes the account; tx2 re-creates it. Entries merge in tx
6592        // order, so the final post-block values must win over the synthesized
6593        // deletion zeros.
6594        let trace = serde_json::json!([
6595            {
6596                "result": {
6597                    "pre": {
6598                        "0x4242424242424242424242424242424242424242": { "balance": "0x64" }
6599                    },
6600                    "post": {}
6601                }
6602            },
6603            {
6604                "result": {
6605                    "pre": {},
6606                    "post": {
6607                        "0x4242424242424242424242424242424242424242": {
6608                            "balance": "0x07",
6609                            "nonce": "0x01",
6610                            "code": "0x6002"
6611                        }
6612                    }
6613                }
6614            }
6615        ]);
6616
6617        let diff = parse_block_state_diff_trace(&trace).unwrap();
6618        assert_eq!(diff.accounts.len(), 1);
6619        let account = &diff.accounts[0];
6620        assert_eq!(account.balance, Some(U256::from(7)));
6621        assert_eq!(account.nonce, Some(1));
6622        assert_eq!(account.code, Some(Bytes::from(vec![0x60, 0x02])));
6623    }
6624
6625    #[test]
6626    fn snapshot_generation_bumps_on_writes_and_repins_not_prefetch() {
6627        use alloy_provider::RootProvider;
6628        use alloy_rpc_client::RpcClient;
6629        use alloy_transport::mock::Asserter;
6630
6631        let asserter = Asserter::new();
6632        let client = RpcClient::mocked(asserter);
6633        let provider = RootProvider::<AnyNetwork>::new(client);
6634        let rt = tokio::runtime::Builder::new_current_thread()
6635            .enable_all()
6636            .build()
6637            .unwrap();
6638        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
6639
6640        let addr = Address::repeat_byte(0x77);
6641        let g0 = cache.snapshot_generation();
6642
6643        // Targeted writes bump (magnitude is opaque; assert monotonic change).
6644        cache.apply_updates(&[StateUpdate::slot(addr, U256::from(1), U256::from(10))]);
6645        let g1 = cache.snapshot_generation();
6646        assert!(g1 > g0, "apply_updates must bump the generation");
6647
6648        cache.apply_update(&StateUpdate::slot(addr, U256::from(2), U256::from(20)));
6649        let g2 = cache.snapshot_generation();
6650        assert!(g2 > g1, "apply_update must bump the generation");
6651
6652        // An empty batch is a no-op, not a mutation.
6653        cache.apply_updates(&[]);
6654        assert_eq!(cache.snapshot_generation(), g2);
6655
6656        // modify_slot on a warm slot bumps.
6657        let change = cache.modify_slot(addr, U256::from(1), |v| {
6658            Some(v.unwrap_or_default() + U256::from(1))
6659        });
6660        assert!(change.is_some());
6661        let g3 = cache.snapshot_generation();
6662        assert!(g3 > g2, "modify_slot must bump the generation");
6663
6664        // Cold prefetch materializes existing chain state — no bump.
6665        cache.inject_storage_batch(&[(addr, U256::from(9), U256::from(90))]);
6666        assert_eq!(
6667            cache.snapshot_generation(),
6668            g3,
6669            "inject_storage_batch is prefetch, not mutation"
6670        );
6671
6672        // Block re-pins bump; a same-block set_block is a no-op.
6673        cache.set_block(BlockId::Number(BlockNumberOrTag::Number(5)));
6674        let g4 = cache.snapshot_generation();
6675        assert!(g4 > g3, "set_block to a new pin must bump the generation");
6676        cache.set_block(BlockId::Number(BlockNumberOrTag::Number(5)));
6677        assert_eq!(
6678            cache.snapshot_generation(),
6679            g4,
6680            "re-pinning to the same block is not a mutation"
6681        );
6682
6683        // advance_block refreshes the env — a spanning snapshot would be
6684        // inconsistent, so it bumps too.
6685        let header = alloy_consensus::Header::default();
6686        cache.advance_block(&header).expect("lenient advance");
6687        assert!(cache.snapshot_generation() > g4);
6688    }
6689
6690    #[test]
6691    fn test_address_to_u256_conversion() {
6692        // Test that address conversion preserves the address bytes correctly
6693        let addr = Address::repeat_byte(0xAB);
6694        let value = U256::from_be_slice(addr.as_slice());
6695
6696        // Address is 20 bytes, should be right-aligned in U256 (32 bytes)
6697        let bytes = value.to_be_bytes::<32>();
6698
6699        // First 12 bytes should be zero (padding)
6700        assert_eq!(&bytes[..12], &[0u8; 12]);
6701
6702        // Last 20 bytes should be the address
6703        assert_eq!(&bytes[12..], addr.as_slice());
6704    }
6705
6706    // ==================== block context tests ====================
6707
6708    #[test]
6709    fn new_defaults_to_latest_block_pin() {
6710        use alloy_provider::RootProvider;
6711        use alloy_rpc_client::RpcClient;
6712        use alloy_transport::mock::Asserter;
6713
6714        let asserter = Asserter::new();
6715        let client = RpcClient::mocked(asserter);
6716        let provider = RootProvider::<AnyNetwork>::new(client);
6717
6718        let rt = tokio::runtime::Builder::new_current_thread()
6719            .enable_all()
6720            .build()
6721            .unwrap();
6722
6723        let cache = rt.block_on(EvmCache::new(Arc::new(provider)));
6724
6725        assert_eq!(
6726            cache.block(),
6727            BlockId::latest(),
6728            "a default cache must carry an explicit latest block pin, not None"
6729        );
6730    }
6731
6732    #[test]
6733    fn test_set_block_context_stores_values() {
6734        use alloy_provider::RootProvider;
6735        use alloy_rpc_client::RpcClient;
6736        use alloy_transport::mock::Asserter;
6737
6738        let asserter = Asserter::new();
6739        let client = RpcClient::mocked(asserter);
6740        let provider = RootProvider::<AnyNetwork>::new(client);
6741
6742        let rt = tokio::runtime::Builder::new_current_thread()
6743            .enable_all()
6744            .build()
6745            .unwrap();
6746
6747        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
6748
6749        // Initially None
6750        assert_eq!(cache.block_number(), None);
6751        assert_eq!(cache.basefee(), None);
6752
6753        // Set values
6754        cache.set_block_context(Some(148_252_680), Some(50));
6755        assert_eq!(cache.block_number(), Some(148_252_680));
6756        assert_eq!(cache.basefee(), Some(50));
6757
6758        // Clear values
6759        cache.set_block_context(None, None);
6760        assert_eq!(cache.block_number(), None);
6761        assert_eq!(cache.basefee(), None);
6762    }
6763
6764    #[test]
6765    fn set_block_latest_clears_stale_block_context() {
6766        use alloy_provider::RootProvider;
6767        use alloy_rpc_client::RpcClient;
6768        use alloy_transport::mock::Asserter;
6769
6770        let asserter = Asserter::new();
6771        let client = RpcClient::mocked(asserter);
6772        let provider = RootProvider::<AnyNetwork>::new(client);
6773
6774        let rt = tokio::runtime::Builder::new_current_thread()
6775            .enable_all()
6776            .build()
6777            .unwrap();
6778
6779        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
6780        cache.set_block_context(Some(148_252_680), Some(50));
6781
6782        cache.set_block(BlockId::latest());
6783
6784        assert_eq!(
6785            cache.block_number(),
6786            None,
6787            "tag pins must not retain a stale NUMBER context"
6788        );
6789        assert_eq!(
6790            cache.basefee(),
6791            None,
6792            "set_block cannot refresh BASEFEE synchronously, so it must clear stale values"
6793        );
6794    }
6795
6796    #[test]
6797    fn set_block_latest_clears_stale_context_even_when_pin_unchanged() {
6798        use alloy_provider::RootProvider;
6799        use alloy_rpc_client::RpcClient;
6800        use alloy_transport::mock::Asserter;
6801
6802        let asserter = Asserter::new();
6803        let client = RpcClient::mocked(asserter);
6804        let provider = RootProvider::<AnyNetwork>::new(client);
6805
6806        let rt = tokio::runtime::Builder::new_current_thread()
6807            .enable_all()
6808            .build()
6809            .unwrap();
6810
6811        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
6812        cache.set_block_context(Some(148_252_680), Some(50));
6813
6814        cache.set_block(BlockId::latest());
6815
6816        assert_eq!(
6817            cache.block_number(),
6818            None,
6819            "latest pins must not retain a stale NUMBER context"
6820        );
6821        assert_eq!(
6822            cache.basefee(),
6823            None,
6824            "latest pins can drift like tags, so stale BASEFEE must be cleared"
6825        );
6826    }
6827
6828    #[test]
6829    fn set_block_number_sets_number_and_clears_stale_basefee() {
6830        use alloy_provider::RootProvider;
6831        use alloy_rpc_client::RpcClient;
6832        use alloy_transport::mock::Asserter;
6833
6834        let asserter = Asserter::new();
6835        let client = RpcClient::mocked(asserter);
6836        let provider = RootProvider::<AnyNetwork>::new(client);
6837
6838        let rt = tokio::runtime::Builder::new_current_thread()
6839            .enable_all()
6840            .build()
6841            .unwrap();
6842
6843        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
6844        cache.set_block_context(Some(100), Some(50));
6845
6846        cache.set_block(BlockId::Number(BlockNumberOrTag::Number(200)));
6847
6848        assert_eq!(cache.block_number(), Some(200));
6849        assert_eq!(
6850            cache.basefee(),
6851            None,
6852            "set_block cannot refresh BASEFEE synchronously, so it must clear stale values"
6853        );
6854    }
6855
6856    #[test]
6857    fn repin_to_block_clears_stale_basefee() {
6858        use alloy_provider::RootProvider;
6859        use alloy_rpc_client::RpcClient;
6860        use alloy_transport::mock::Asserter;
6861
6862        let asserter = Asserter::new();
6863        let client = RpcClient::mocked(asserter);
6864        let provider = RootProvider::<AnyNetwork>::new(client);
6865
6866        let rt = tokio::runtime::Builder::new_current_thread()
6867            .enable_all()
6868            .build()
6869            .unwrap();
6870
6871        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
6872        cache.set_block_context(Some(100), Some(50));
6873
6874        cache.repin_to_block(200);
6875
6876        assert_eq!(cache.block_number(), Some(200));
6877        assert_eq!(
6878            cache.basefee(),
6879            None,
6880            "repin_to_block must not carry stale BASEFEE across blocks"
6881        );
6882    }
6883
6884    #[test]
6885    fn test_build_evm_applies_block_context() {
6886        use alloy_provider::RootProvider;
6887        use alloy_rpc_client::RpcClient;
6888        use alloy_transport::mock::Asserter;
6889
6890        let asserter = Asserter::new();
6891        let client = RpcClient::mocked(asserter);
6892        let provider = RootProvider::<AnyNetwork>::new(client);
6893
6894        let rt = tokio::runtime::Builder::new_current_thread()
6895            .enable_all()
6896            .build()
6897            .unwrap();
6898
6899        let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
6900
6901        let block_num = 148_252_680u64;
6902        let basefee_val = 50u64;
6903        let coinbase = Address::repeat_byte(0xC0);
6904        let prevrandao = B256::repeat_byte(0x77);
6905        let gas_limit = 30_000_000u64;
6906        cache.set_block_context(Some(block_num), Some(basefee_val));
6907        cache.set_coinbase(Some(coinbase));
6908        cache.set_prevrandao(Some(prevrandao));
6909        cache.set_block_gas_limit(Some(gas_limit));
6910
6911        let evm = cache.build_evm();
6912        assert_eq!(evm.block.number, U256::from(block_num));
6913        assert_eq!(evm.block.basefee, basefee_val);
6914        assert_eq!(evm.block.beneficiary, coinbase);
6915        assert_eq!(evm.block.prevrandao, Some(prevrandao));
6916        assert_eq!(evm.block.gas_limit, gas_limit);
6917    }
6918
6919    #[test]
6920    fn test_from_backend_propagates_block_context() {
6921        use alloy_provider::RootProvider;
6922        use alloy_rpc_client::RpcClient;
6923        use alloy_transport::mock::Asserter;
6924
6925        let asserter = Asserter::new();
6926        let client = RpcClient::mocked(asserter);
6927        let provider = RootProvider::<AnyNetwork>::new(client);
6928
6929        let rt = tokio::runtime::Builder::new_current_thread()
6930            .enable_all()
6931            .build()
6932            .unwrap();
6933
6934        let parent = rt.block_on(EvmCache::new(Arc::new(provider)));
6935
6936        let block_num = Some(148_252_680u64);
6937        let basefee_val = Some(50u64);
6938        let child = EvmCache::from_backend(
6939            parent.unchecked_backend().clone(),
6940            parent.unchecked_blockchain_db().clone(),
6941            parent.block(),
6942            42161,
6943            block_num,
6944            basefee_val,
6945            SpecId::CANCUN,
6946        );
6947
6948        assert_eq!(child.block_number(), block_num);
6949        assert_eq!(child.basefee(), basefee_val);
6950    }
6951
6952    #[test]
6953    fn unix_timestamp_secs_saturating_handles_pre_epoch() {
6954        let before_epoch = std::time::UNIX_EPOCH - std::time::Duration::from_secs(5);
6955        assert_eq!(
6956            unix_timestamp_secs_saturating(before_epoch),
6957            0,
6958            "pre-epoch system times must saturate instead of panicking"
6959        );
6960    }
6961}