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