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