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