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