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