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