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