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