Skip to main content

evm_fork_cache/cache/
mod.rs

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