evm_fork_cache/cache/mod.rs
1mod binary_state;
2mod bytecode;
3mod journal_access_list;
4mod metadata;
5pub mod overlay;
6pub mod slot_observations;
7pub mod snapshot;
8pub(crate) mod versioned;
9
10pub use binary_state::{load_binary_state, save_binary_state};
11pub use metadata::{CacheConfig, ImmutableDataCache};
12pub use overlay::EvmOverlay;
13pub use slot_observations::SlotObservationTracker;
14pub use snapshot::EvmSnapshot;
15
16use std::{
17 cell::RefCell,
18 collections::{HashMap, HashSet},
19 fs,
20 rc::Rc,
21 sync::{
22 Arc, Mutex,
23 atomic::{AtomicU8, Ordering},
24 },
25 time::{SystemTime, UNIX_EPOCH},
26};
27
28use alloy_consensus::BlockHeader;
29use alloy_eips::eip2930::AccessList;
30use alloy_eips::{BlockId, BlockNumberOrTag};
31use alloy_network::BlockResponse;
32use alloy_primitives::{Address, B256, Bytes, I256, Log, TxKind, U256, keccak256};
33use alloy_provider::{Provider, network::AnyNetwork};
34use alloy_rpc_types_eth::TransactionRequest;
35use alloy_sol_types::{SolCall, SolValue, sol};
36use anyhow::{Context as _, Result, anyhow};
37use foundry_fork_db::{BlockchainDb, SharedBackend, cache::BlockchainDbMeta};
38use revm::{
39 Context, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainBuilder, MainContext,
40 context::{BlockEnv, CfgEnv, Journal, LocalContext, TxEnv, result::ExecutionResult},
41 context_interface::JournalTr,
42 database::{AccountState, CacheDB},
43 primitives::hardfork::SpecId,
44 state::{Account, AccountInfo, Bytecode},
45};
46use tracing::{debug, instrument, trace, warn};
47
48use crate::access_set::StorageAccessList;
49use crate::errors::{SimError, SimulationError, SimulationResult};
50use crate::freshness::{SlotChange, SlotFetch, SlotOutcome};
51use crate::inspector::TransferInspector;
52use crate::state_update::{
53 AccountChange, AccountPatch, PurgeRecord, PurgeScope, SkippedAccountPatch, SkippedBalanceDelta,
54 SkippedDelta, SkippedMask, SlotDelta, StateDiff, StateUpdate,
55};
56
57use bytecode::BytecodeCache;
58use journal_access_list::{extract_access_list, merge_access_lists};
59
60/// Re-export AnyNetwork for callers that need to construct providers.
61pub use alloy_provider::network::AnyNetwork as AnyNetworkType;
62
63/// The database type used by the EVM cache.
64/// CacheDB wraps SharedBackend which lazily fetches data from RPC on-demand.
65pub type ForkCacheDB = CacheDB<SharedBackend>;
66
67/// Callback for making direct RPC `eth_call` requests, bypassing revm simulation.
68/// Used when batch-querying many contracts where revm's lazy storage fetching would
69/// be prohibitively slow (e.g. querying 500+ gauge contracts).
70pub type RpcCallFn = Arc<dyn Fn(Address, Bytes) -> Result<Bytes> + Send + Sync>;
71
72/// Callback for batch-fetching storage slots directly from RPC, bypassing SharedBackend.
73///
74/// Used by callers that need bulk storage reads without many individual channel
75/// round-trips through SharedBackend. Fires concurrent `eth_getStorageAt` calls
76/// directly via the provider and returns results for bulk injection into
77/// BlockchainDb.
78///
79/// The second argument pins the fetch to a specific block: `Some(block)` fetches
80/// at exactly that block, while `None` uses the fetcher's configured block (the
81/// cache's currently-pinned block). The freshness validator passes the block its
82/// snapshot was built from, so a concurrent [`EvmCache::set_block`] cannot make
83/// the deferred fetch read a *different* block than the snapshot it is compared
84/// against.
85///
86/// **Contract:** an implementation must return **exactly one** result tuple per
87/// requested `(address, slot)` (order does not matter). Callers — `verify_slots`,
88/// `reconcile_slots`, and the cold-start verify/probe paths — derive their
89/// per-slot outcomes from the returned tuples, so a fetcher that drops, dedups,
90/// reorders-and-truncates, or duplicates entries breaks the "one outcome per
91/// requested slot" guarantee those APIs document.
92pub type StorageBatchFetchFn = Arc<
93 dyn Fn(Vec<(Address, U256)>, Option<BlockId>) -> Vec<(Address, U256, Result<U256>)>
94 + Send
95 + Sync,
96>;
97
98/// Return a tokio runtime [`Handle`] suitable for `block_in_place` + `block_on`,
99/// or an error describing why one is unavailable.
100///
101/// The RPC-backed callbacks ([`RpcCallFn`], [`StorageBatchFetchFn`]) drive async
102/// work synchronously via `tokio::task::block_in_place`. That helper panics on a
103/// current-thread runtime, and `Handle::current()` panics when no runtime is
104/// present. To avoid panicking deep inside a callback, callers use this guard to
105/// degrade to a typed error instead.
106///
107/// Requires a **multi-thread** tokio runtime.
108pub(crate) fn block_in_place_handle() -> Result<tokio::runtime::Handle> {
109 match tokio::runtime::Handle::try_current() {
110 Ok(handle) => match handle.runtime_flavor() {
111 tokio::runtime::RuntimeFlavor::CurrentThread => Err(anyhow!(
112 "evm-fork-cache RPC operations require a multi-thread tokio runtime; \
113 found a current-thread runtime (block_in_place is not supported there). \
114 Build the runtime with `tokio::runtime::Builder::new_multi_thread()` \
115 or annotate with `#[tokio::main(flavor = \"multi_thread\")]`"
116 )),
117 _ => Ok(handle),
118 },
119 Err(e) => Err(anyhow!(
120 "evm-fork-cache RPC operations require a running multi-thread tokio runtime: {e}"
121 )),
122 }
123}
124
125pub(crate) fn unix_timestamp_secs_saturating(time: SystemTime) -> u64 {
126 time.duration_since(UNIX_EPOCH)
127 .map(|duration| duration.as_secs())
128 .unwrap_or(0)
129}
130
131/// Read a storage slot from already-borrowed layers (`account_state`-aware),
132/// mirroring [`EvmCache::cached_storage_value`] but operating on a held backend
133/// storage guard rather than re-locking. Shared by the batched slot-run fast-path
134/// ([`EvmCache::apply_slot_run`]) so the same EVM-SLOAD semantics hold inside the
135/// held guard: the overlay slot wins; a `StorageCleared`/`NotExisting` overlay
136/// account reads a missing slot as ZERO (the backend is **not** consulted);
137/// otherwise it falls through to the backend.
138fn read_slot_account_state_aware<S1, S2>(
139 overlay: &std::collections::HashMap<Address, revm::database::DbAccount, S1>,
140 storage: &std::collections::HashMap<Address, foundry_fork_db::cache::StorageInfo, S2>,
141 address: Address,
142 slot: U256,
143) -> Option<U256>
144where
145 S1: std::hash::BuildHasher,
146 S2: std::hash::BuildHasher,
147{
148 if let Some(db_account) = overlay.get(&address) {
149 if let Some(value) = db_account.storage.get(&slot) {
150 return Some(*value);
151 }
152 if matches!(
153 db_account.account_state,
154 AccountState::StorageCleared | AccountState::NotExisting
155 ) {
156 return Some(U256::ZERO);
157 }
158 }
159 storage.get(&address).and_then(|s| s.get(&slot).copied())
160}
161
162/// Write a storage slot into already-borrowed layers, mirroring
163/// [`EvmCache::write_slot_through`] but operating on a held backend storage guard.
164/// Backend (layer 2) is always written; the overlay (layer 1) is written only if
165/// an overlay account already exists (never materialize a new overlay account).
166fn write_slot_into<S1, S2>(
167 overlay: &mut std::collections::HashMap<Address, revm::database::DbAccount, S1>,
168 storage: &mut std::collections::HashMap<Address, foundry_fork_db::cache::StorageInfo, S2>,
169 address: Address,
170 slot: U256,
171 value: U256,
172) where
173 S1: std::hash::BuildHasher,
174 S2: std::hash::BuildHasher + Default,
175{
176 storage.entry(address).or_default().insert(slot, value);
177 if let Some(db_account) = overlay.get_mut(&address) {
178 db_account.storage.insert(slot, value);
179 }
180}
181
182fn account_patch_is_empty(patch: &AccountPatch) -> bool {
183 patch.balance.is_none() && patch.nonce.is_none() && patch.code.is_none()
184}
185
186static CACHE_SPEED_MODE: AtomicU8 = AtomicU8::new(CacheSpeedMode::Slow as u8);
187
188/// Runtime tuning profile for cache-side batch storage fetches.
189///
190/// Selects the per-batch size and concurrency used by [`StorageBatchFetchFn`]:
191/// faster modes send larger batches with more in-flight HTTP requests, slower
192/// modes throttle to avoid RPC rate-limiting (e.g. HTTP 429 on Base). The
193/// selected mode is **process-global** state, set via [`set_cache_speed_mode`]
194/// and read via [`cache_speed_mode`]; it affects every cache in the process.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196#[repr(u8)]
197pub enum CacheSpeedMode {
198 /// Largest batches, highest concurrency — fastest, most likely to trip rate limits.
199 Fast = 0,
200 /// Moderate batch size and concurrency.
201 Normal = 1,
202 /// Conservative batch size and concurrency. The default.
203 Slow = 2,
204 /// Smallest batches, single in-flight request — slowest, gentlest on the RPC provider.
205 XSlow = 3,
206}
207
208impl CacheSpeedMode {
209 fn from_u8(value: u8) -> Self {
210 match value {
211 0 => Self::Fast,
212 1 => Self::Normal,
213 2 => Self::Slow,
214 3 => Self::XSlow,
215 _ => Self::Slow,
216 }
217 }
218}
219
220/// Set the process-global cache batch-fetch speed profile.
221///
222/// This mutates a single static shared by every cache in the process, so it
223/// affects all in-flight and future batch fetches, not just one [`EvmCache`].
224/// Read the current value with [`cache_speed_mode`].
225pub fn set_cache_speed_mode(mode: CacheSpeedMode) {
226 CACHE_SPEED_MODE.store(mode as u8, Ordering::Relaxed);
227}
228
229/// Return the current process-global cache batch-fetch speed profile.
230///
231/// Defaults to [`CacheSpeedMode::Slow`] until changed via
232/// [`set_cache_speed_mode`]. The value is shared across all caches in the
233/// process.
234pub fn cache_speed_mode() -> CacheSpeedMode {
235 CacheSpeedMode::from_u8(CACHE_SPEED_MODE.load(Ordering::Relaxed))
236}
237
238/// Behavior when overriding code at a target account that is not known to the cache/backend.
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum MissingTargetBehavior {
241 /// Return an error if the target account cannot be loaded.
242 Error,
243 /// Create a default account with the replacement code.
244 Create,
245}
246
247/// Per-call transaction-environment overrides for a simulation.
248///
249/// `Default` reproduces the read-only behavior of the plain `call_raw`
250/// (zero value, default gas/nonce). Use the `*_with` call variants to supply
251/// these — e.g. to simulate a payable function, a native-ETH transfer, or a
252/// gas-bounded call. Balance affordability checks are disabled in the
253/// simulator, so a non-zero `value` does not require the caller to be funded.
254#[derive(Debug, Clone, Default)]
255pub struct TxConfig {
256 /// Native value (wei) sent with the call. Set this to simulate a payable
257 /// function or a native-ETH transfer. Balance checks are disabled in the
258 /// simulator, so the caller need not be funded for a non-zero value.
259 pub value: U256,
260 /// Gas limit for the call. `None` uses revm's default. Set this to model a
261 /// gas-bounded call (e.g. to observe out-of-gas behavior).
262 pub gas_limit: Option<u64>,
263 /// Gas price (wei) for the call. `None` uses revm's default. Rarely needed
264 /// because base-fee checks are disabled in the simulator.
265 pub gas_price: Option<u128>,
266 /// Sender nonce. `None` lets the simulator pick; nonce checks are disabled,
267 /// so this is only worth setting when a contract reads the nonce explicitly.
268 pub nonce: Option<u64>,
269 /// EIP-2930 access list to pre-warm accounts and storage slots for this
270 /// call. Pre-warming changes EIP-2929 gas accounting; supply it when
271 /// reproducing the gas cost of a transaction that carried an access list.
272 pub access_list: Option<AccessList>,
273}
274
275/// Fluent builder for [`EvmCache`].
276///
277/// A readable alternative to the positional [`EvmCache::with_cache`]
278/// constructor. Defaults: latest block, no disk cache, [`SpecId::CANCUN`].
279///
280/// ```no_run
281/// # use std::sync::Arc;
282/// # use alloy_provider::{ProviderBuilder, network::AnyNetwork};
283/// # use revm::primitives::hardfork::SpecId;
284/// # use evm_fork_cache::cache::EvmCache;
285/// # async fn example() -> anyhow::Result<()> {
286/// let provider = ProviderBuilder::new()
287/// .network::<AnyNetwork>()
288/// .connect_http("https://example-rpc.invalid".parse()?);
289/// let cache = EvmCache::builder(Arc::new(provider))
290/// .latest_block()
291/// .spec(SpecId::CANCUN)
292/// .build()
293/// .await;
294/// # let _ = cache;
295/// # Ok(())
296/// # }
297/// ```
298pub struct EvmCacheBuilder<P> {
299 provider: Arc<P>,
300 block: BlockId,
301 cache_config: Option<CacheConfig>,
302 spec_id: SpecId,
303 shared_memory_capacity: SharedMemoryCapacity,
304 chain_id: Option<u64>,
305}
306
307impl<P> EvmCacheBuilder<P>
308where
309 P: Provider<AnyNetwork> + 'static,
310{
311 /// Start a builder over the given provider.
312 pub fn new(provider: Arc<P>) -> Self {
313 Self {
314 provider,
315 block: BlockId::latest(),
316 cache_config: None,
317 spec_id: SpecId::CANCUN,
318 shared_memory_capacity: SharedMemoryCapacity::default(),
319 chain_id: None,
320 }
321 }
322
323 /// Pin simulations and RPC fetches to a specific block.
324 ///
325 /// Use this to fork at a fixed height for reproducible simulation. Without
326 /// a call to [`block`](Self::block) or [`latest_block`](Self::latest_block)
327 /// the builder defaults to the latest block at [`build`](Self::build) time.
328 pub fn block(mut self, block: BlockId) -> Self {
329 self.block = block;
330 self
331 }
332
333 /// Pin to the latest block.
334 ///
335 /// The height is resolved when [`build`](Self::build) fetches the block
336 /// header, so the cache forks at whatever was latest at construction. Use
337 /// [`block`](Self::block) instead to pin a fixed, reproducible height.
338 pub fn latest_block(mut self) -> Self {
339 self.block = BlockId::latest();
340 self
341 }
342
343 /// Set the EVM hardfork spec (must match the chain's execution layer).
344 pub fn spec(mut self, spec_id: SpecId) -> Self {
345 self.spec_id = spec_id;
346 self
347 }
348
349 /// Set the chain ID reported to simulations via the `CHAINID` opcode.
350 ///
351 /// **Recommended.** This is the explicit, authoritative way to set the chain
352 /// ID. If left unset, [`build`](Self::build) infers it from the provider
353 /// (`eth_chainId`), falling back to `1` (Ethereum mainnet) only if that query
354 /// fails. A disk [`cache_config`](Self::cache_config) also carries a
355 /// `chain_id` (which additionally namespaces the on-disk cache directory);
356 /// when both are set, the value passed here wins for the `CHAINID` opcode, so
357 /// keep them consistent.
358 pub fn chain_id(mut self, chain_id: u64) -> Self {
359 self.chain_id = Some(chain_id);
360 self
361 }
362
363 /// Enable disk-backed caching with the given configuration.
364 ///
365 /// Supplying a [`CacheConfig`] turns on persistence of EVM state, bytecodes,
366 /// and immutable data under the configured chain directory; the cache is
367 /// loaded on [`build`](Self::build) and flushed on drop. Omit it for a
368 /// purely in-memory cache backed solely by RPC.
369 pub fn cache_config(mut self, cache_config: CacheConfig) -> Self {
370 self.cache_config = Some(cache_config);
371 self
372 }
373
374 /// Set how much EVM shared memory to pre-allocate per simulation context.
375 ///
376 /// Defaults to [`SharedMemoryCapacity::Fixed`] with `64 * 1024` bytes
377 /// (65,536 bytes).
378 /// Use `Fixed(n)` to pin a size, or [`SharedMemoryCapacity::Auto`] to size it
379 /// from the chain state loaded at [`build`](Self::build) time (e.g. a bincode
380 /// state file supplied via [`cache_config`](Self::cache_config)). See
381 /// [`SharedMemoryCapacity`] for the trade-offs.
382 pub fn shared_memory_capacity(mut self, capacity: SharedMemoryCapacity) -> Self {
383 self.shared_memory_capacity = capacity;
384 self
385 }
386
387 /// Build the [`EvmCache`], fetching the pinned block's header for context.
388 ///
389 /// If a chain ID was not set via [`chain_id`](Self::chain_id), it is inferred
390 /// from the provider (`eth_chainId`); see [`chain_id`](Self::chain_id) for the
391 /// full resolution order.
392 pub async fn build(self) -> EvmCache {
393 let explicit_chain_id = self.chain_id;
394 let mut cache = EvmCache::with_cache_capacity(
395 self.provider,
396 self.block,
397 self.cache_config,
398 self.spec_id,
399 self.shared_memory_capacity,
400 )
401 .await;
402 // An explicit builder value is authoritative for the `CHAINID` opcode and
403 // overrides both the inferred value and any `cache_config` chain id.
404 if let Some(chain_id) = explicit_chain_id {
405 cache.set_chain_id(chain_id);
406 }
407 cache
408 }
409}
410
411type CacheEvm<'a> = revm::MainnetEvm<
412 Context<BlockEnv, TxEnv, CfgEnv, &'a mut ForkCacheDB, Journal<&'a mut ForkCacheDB>, ()>,
413>;
414type InspectorCacheEvm<'a, INSP> = revm::MainnetEvm<
415 Context<BlockEnv, TxEnv, CfgEnv, &'a mut ForkCacheDB, Journal<&'a mut ForkCacheDB>, ()>,
416 INSP,
417>;
418
419/// Default initial capacity for the EVM shared-memory (working-memory) buffer.
420/// 64 KiB (65,536 bytes), chosen from profiling a state-heavy workload (16x the
421/// revm default of 4 KiB) so simulations rarely reallocate. Exposed for tuning via
422/// [`SharedMemoryCapacity`].
423const DEFAULT_SHARED_MEMORY_CAPACITY: usize = 64 * 1024;
424
425/// How much EVM shared memory (per-context working memory) to pre-allocate for
426/// simulations.
427///
428/// revm grows its shared memory on demand during execution; pre-allocating just
429/// avoids repeated reallocations when simulations touch a lot of memory — the
430/// original motivation was a state-heavy workload where resizing was hot. The
431/// trade-off cuts both ways: a wide parallel fan-out of *small* simulations pays
432/// this much memory per overlay, so general users may want a smaller `Fixed` size,
433/// while state-heavy users can raise it or let it auto-size from the loaded state.
434///
435/// The default is `Fixed(64 * 1024)` (65,536 bytes). Configure it on
436/// [`EvmCacheBuilder::shared_memory_capacity`].
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub enum SharedMemoryCapacity {
439 /// Pre-allocate exactly this many bytes. The [`Default`] is
440 /// `Fixed(64 * 1024)`.
441 Fixed(usize),
442 /// Size the buffer from the amount of chain state loaded into the cache at
443 /// construction (e.g. from a bincode state file via
444 /// [`CacheConfig`]/[`EvmCacheBuilder::cache_config`]), clamped to a sane
445 /// floor/ceiling. Falls back to the floor when nothing is loaded.
446 ///
447 /// This is a heuristic proxy — persisted state size loosely correlates with the
448 /// working-set size of simulations over it, not an exact peak-memory model. Use
449 /// `Fixed` when you have profiled your workload.
450 Auto,
451}
452
453impl Default for SharedMemoryCapacity {
454 fn default() -> Self {
455 Self::Fixed(DEFAULT_SHARED_MEMORY_CAPACITY)
456 }
457}
458
459impl SharedMemoryCapacity {
460 /// Floor for [`Auto`](Self::Auto) (and the default fixed size): 64 KiB
461 /// (65,536 bytes).
462 pub const MIN_AUTO: usize = DEFAULT_SHARED_MEMORY_CAPACITY;
463 /// Ceiling for [`Auto`](Self::Auto): 4 MiB. A simulation that needs more than
464 /// this still works — revm grows the buffer past it on demand.
465 pub const MAX_AUTO: usize = 4 * 1024 * 1024;
466 /// Heuristic proxy: bytes of pre-allocated working memory per loaded storage
467 /// slot. Tune if profiling warrants.
468 const AUTO_BYTES_PER_SLOT: usize = 16;
469
470 /// Resolve to a concrete byte capacity. `loaded_slots` is the number of layer-2
471 /// storage slots present in the cache at construction (0 when nothing is
472 /// loaded); it is consulted only for [`Auto`](Self::Auto).
473 pub(crate) fn resolve(self, loaded_slots: usize) -> usize {
474 match self {
475 Self::Fixed(bytes) => bytes,
476 Self::Auto => loaded_slots
477 .saturating_mul(Self::AUTO_BYTES_PER_SLOT)
478 .clamp(Self::MIN_AUTO, Self::MAX_AUTO),
479 }
480 }
481}
482
483/// EVM cache with lazy-loading RPC backend.
484///
485/// Uses `foundry-fork-db` for intelligent caching and request deduplication.
486/// Storage and account data is fetched on-demand when accessed during EVM execution,
487/// eliminating the need for expensive access list prefetching.
488pub struct EvmCache {
489 backend: SharedBackend,
490 blockchain_db: BlockchainDb,
491 db: ForkCacheDB,
492 token_decimals: HashMap<Address, u8>,
493 block: BlockId,
494 cache_config: Option<CacheConfig>,
495 /// Cache for immutable on-chain data (token decimals).
496 immutable_cache: ImmutableDataCache,
497 /// Optional timestamp override for simulating future blocks.
498 /// When set, EVM simulations use this timestamp instead of the current system time.
499 timestamp_override: Option<u64>,
500 /// Chain ID for EVM simulation (e.g. 42161 for Arbitrum, 1 for Ethereum).
501 chain_id: u64,
502 /// Block number for EVM simulations (NUMBER opcode).
503 /// Fetched from block header during construction. Without this, revm defaults to 0
504 /// which causes contracts that read block.number to execute different code paths.
505 block_number: Option<u64>,
506 /// Base fee per gas for EVM simulations (BASEFEE opcode).
507 /// Fetched from block header during construction.
508 basefee: Option<u64>,
509 /// Block beneficiary for EVM simulations (COINBASE opcode).
510 /// Fetched from the block header; commonly read by MEV/builder tip logic.
511 coinbase: Option<Address>,
512 /// `prevrandao` for EVM simulations (PREVRANDAO opcode), i.e. the header's
513 /// mix hash post-merge. Drives on-chain randomness.
514 prevrandao: Option<B256>,
515 /// Block gas limit for EVM simulations (GASLIMIT opcode).
516 block_gas_limit: Option<u64>,
517 /// Shared memory buffer reused across EVM simulations.
518 /// This avoids repeated allocations and allows measuring peak memory usage.
519 shared_memory_buffer: Rc<RefCell<Vec<u8>>>,
520 /// Optional callback for direct RPC `eth_call` (bypasses revm simulation).
521 /// Set during construction from the provider. Useful for batch operations
522 /// where revm's lazy storage fetching would be too slow.
523 rpc_caller: Option<RpcCallFn>,
524 /// Optional batch storage fetcher that bypasses SharedBackend.
525 /// Captures a provider clone and fires concurrent `eth_getStorageAt` calls directly.
526 storage_batch_fetcher: Option<StorageBatchFetchFn>,
527 /// Shared block ID for the batch storage fetcher closure.
528 /// Updated by `set_block()` so batch fetches always use the current block.
529 batch_block_id: Arc<Mutex<BlockId>>,
530 /// Best-known ERC20 `balanceOf` mapping slot per token contract.
531 ///
532 /// Used by `set_erc20_balance_with_slot_scan` to avoid re-scanning slots
533 /// repeatedly for the same token.
534 erc20_balance_slots: HashMap<Address, U256>,
535 /// EVM hardfork spec for simulations. Must match the chain's current execution
536 /// layer hardfork for accurate gas accounting. Configured per-chain via `evm_spec`
537 /// in `chains.toml`.
538 spec_id: SpecId,
539 /// Memoized, `Arc`-shared flatten of the cold layer-2 index, reused across
540 /// successive [`create_snapshot`](Self::create_snapshot) calls (Pillar A).
541 /// `None` until the first snapshot. Rebuilt copy-on-write by
542 /// [`refresh_base`](Self::refresh_base); never mutated in place once shared.
543 /// Not part of any public API and not serialized.
544 base: Option<Arc<snapshot::BaseState>>,
545 /// Layer-2 addresses changed since `base` was built, folded into the next base
546 /// rebuild. Populated by the base-invalidation sites (write-through, batch
547 /// injects, layer-2 seeding, purges). Not serialized.
548 base_dirty: HashSet<Address>,
549 /// When set, the next [`refresh_base`](Self::refresh_base) rebuilds the base
550 /// from scratch. Set by [`set_block`](Self::set_block) /
551 /// [`repin_to_block`](Self::repin_to_block), which replace layer 2 wholesale.
552 /// Not serialized.
553 base_full_rebuild: bool,
554 /// Per-account layer-2 slot count at the last base build, used by
555 /// [`refresh_base`](Self::refresh_base)'s `O(accounts)` length-scan to detect
556 /// uncontrolled lazy-fetch growth that bypasses the write funnel. Not
557 /// serialized.
558 base_storage_lens: HashMap<Address, usize>,
559 /// Resolved per-context EVM shared-memory pre-allocation (bytes), from the
560 /// [`SharedMemoryCapacity`] at construction (resolving `Auto` against the loaded
561 /// state). Propagated to each [`EvmSnapshot`] so snapshot-backed overlays
562 /// pre-allocate the same amount. See
563 /// [`shared_memory_capacity`](Self::shared_memory_capacity).
564 shared_memory_capacity: usize,
565}
566
567/// Outcome of a balance-delta-tracking simulation.
568///
569/// Produced by [`EvmCache::simulate_call_with_balance_deltas`] and
570/// [`EvmCache::simulate_with_transfer_tracking`]: a successful call together
571/// with the per-token balance changes it caused, its emitted logs, the touched
572/// access list, and its raw return data.
573/// Execution outcome of a simulated call.
574///
575/// Lets a caller distinguish a successful call — even one that emitted no logs,
576/// such as a view call — from a revert or a halt, without guessing from `logs`
577/// or `output`. Revert payloads live in [`CallSimulationResult::output`] and can
578/// be decoded with [`RevertDecoder`](crate::errors::RevertDecoder); only `Halt`
579/// carries extra data here, since its reason has nowhere else to live.
580#[derive(Clone, Debug, PartialEq, Eq)]
581pub enum SimStatus {
582 /// The call returned successfully.
583 Success,
584 /// The call reverted; the revert payload (if any) is in `output`.
585 Revert,
586 /// The call halted (e.g. out of gas, invalid opcode).
587 Halt {
588 /// Debug-formatted halt reason.
589 reason: String,
590 },
591}
592
593#[derive(Clone, Debug)]
594#[non_exhaustive]
595pub struct CallSimulationResult {
596 /// Whether the call succeeded, reverted, or halted.
597 pub status: SimStatus,
598 /// Gas consumed by the (successful) call.
599 pub gas_used: u64,
600 /// Net change in `owner`'s balance per tracked token, as a **signed**
601 /// [`I256`] (`post - pre`): positive means the call increased the balance,
602 /// negative means it decreased it. Tokens not seen by the call may be
603 /// absent or zero.
604 pub token_deltas: HashMap<Address, I256>,
605 /// Logs emitted by the call (in emission order).
606 pub logs: Vec<Log>,
607 /// EIP-2930 access list of all accounts and storage slots touched during simulation.
608 /// Extracted from the EVM journaled state after execution.
609 pub access_list: AccessList,
610 /// Raw return data of the call.
611 ///
612 /// `Success` carries the returned bytes, `Revert` the revert payload, and
613 /// `Halt` an empty slice. This makes a corrected view-call result observable:
614 /// when a re-run reads a changed slot, the new return value differs here even
615 /// if both runs succeed.
616 pub output: Bytes,
617}
618
619sol!(
620 #[sol(rpc)]
621 contract IERC20 {
622 function balanceOf(address target) returns (uint256);
623 function decimals() returns (uint8);
624 function allowance(address owner, address spender) returns (uint256);
625 }
626);
627
628/// Parse an EVM hardfork spec name (e.g. from TOML config) into a revm [`SpecId`].
629///
630/// Accepts revm's canonical names (e.g. `"Cancun"`, `"Shanghai"`, `"Prague"`)
631/// case-insensitively. Falls back to [`SpecId::CANCUN`] for unrecognized values.
632pub fn parse_evm_spec(spec: &str) -> SpecId {
633 // SpecId::from_str expects title-case (e.g. "Cancun"), so normalize the input.
634 let mut chars = spec.chars();
635 let title_case: String = match chars.next() {
636 Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
637 None => String::new(),
638 };
639 title_case.parse::<SpecId>().unwrap_or_else(|_| {
640 warn!(spec, "Unknown EVM spec, defaulting to Cancun");
641 SpecId::CANCUN
642 })
643}
644
645impl EvmCache {
646 /// Start a fluent [`EvmCacheBuilder`] over the given provider.
647 ///
648 /// Preferred over the positional [`with_cache`](Self::with_cache) /
649 /// [`new`](Self::new) constructors for readability.
650 pub fn builder<P>(provider: Arc<P>) -> EvmCacheBuilder<P>
651 where
652 P: Provider<AnyNetwork> + 'static,
653 {
654 EvmCacheBuilder::new(provider)
655 }
656
657 /// Create a new EvmCache with a SharedBackend that lazily fetches from RPC.
658 ///
659 /// The backend spawns a background handler task that manages RPC requests
660 /// and deduplicates concurrent requests for the same data.
661 ///
662 /// # Runtime requirement
663 /// RPC-backed operation requires a **multi-thread** tokio runtime
664 /// (`#[tokio::main(flavor = "multi_thread")]` or
665 /// `tokio::runtime::Builder::new_multi_thread()`). The direct RPC callbacks
666 /// (`eth_call` and batch `eth_getStorageAt`) drive async work synchronously
667 /// via `tokio::task::block_in_place`, which is unsupported on a
668 /// current-thread runtime. On a current-thread runtime those callbacks
669 /// degrade to typed errors rather than panicking.
670 pub async fn new<P>(provider: Arc<P>) -> Self
671 where
672 P: Provider<AnyNetwork> + 'static,
673 {
674 Self::at_block(provider, BlockId::latest()).await
675 }
676
677 /// Create a new EvmCache pinned to an explicit block.
678 ///
679 /// Prefer this over [`new`](Self::new) when reproducibility matters and the
680 /// caller has already chosen the fork block.
681 pub async fn at_block<P>(provider: Arc<P>, block: BlockId) -> Self
682 where
683 P: Provider<AnyNetwork> + 'static,
684 {
685 Self::with_cache(provider, block, None, SpecId::CANCUN).await
686 }
687
688 /// Create a new EvmCache with disk-based caching.
689 ///
690 /// This enables several caching features:
691 /// 1. Unified EVM state: Accounts + storage loaded from `evm_state.bin` (bincode)
692 /// 2. Bytecode caching: Contract bytecodes from `bytecodes.bin`
693 /// 3. Immutable data: Token decimals
694 ///
695 /// # Runtime requirement
696 /// RPC-backed operation requires a **multi-thread** tokio runtime
697 /// (`#[tokio::main(flavor = "multi_thread")]` or
698 /// `tokio::runtime::Builder::new_multi_thread()`). The direct RPC callbacks
699 /// (`eth_call` and batch `eth_getStorageAt`) drive async work synchronously
700 /// via `tokio::task::block_in_place`, which is unsupported on a
701 /// current-thread runtime. On a current-thread runtime those callbacks
702 /// degrade to typed errors rather than panicking.
703 pub async fn with_cache<P>(
704 provider: Arc<P>,
705 block: BlockId,
706 cache_config: Option<CacheConfig>,
707 spec_id: SpecId,
708 ) -> Self
709 where
710 P: Provider<AnyNetwork> + 'static,
711 {
712 Self::with_cache_capacity(
713 provider,
714 block,
715 cache_config,
716 spec_id,
717 SharedMemoryCapacity::default(),
718 )
719 .await
720 }
721
722 /// Like [`with_cache`](Self::with_cache) but takes an explicit
723 /// [`SharedMemoryCapacity`] controlling per-context EVM working-memory
724 /// pre-allocation. This is what [`EvmCacheBuilder::build`] calls; prefer the
725 /// builder. With [`SharedMemoryCapacity::Auto`] the buffer is sized from the
726 /// layer-2 storage loaded at construction (e.g. a bincode state file).
727 pub async fn with_cache_capacity<P>(
728 provider: Arc<P>,
729 block: BlockId,
730 cache_config: Option<CacheConfig>,
731 spec_id: SpecId,
732 shared_memory_capacity: SharedMemoryCapacity,
733 ) -> Self
734 where
735 P: Provider<AnyNetwork> + 'static,
736 {
737 let block_id = block;
738
739 // Fetch the pinned block header for accurate block context (NUMBER,
740 // BASEFEE, COINBASE, PREVRANDAO, GASLIMIT opcodes). Without this, revm
741 // defaults to 0/default values, causing contracts that read block
742 // context to execute different code paths. Use the concrete BlockId the
743 // cache is pinned to so hash pins do not accidentally inherit latest
744 // header context.
745 let (block_number, basefee, coinbase, prevrandao, block_gas_limit) =
746 match provider.get_block(block_id).await {
747 Ok(Some(blk)) => {
748 let h = blk.header();
749 (
750 Some(h.number()),
751 h.base_fee_per_gas(),
752 Some(h.beneficiary()),
753 h.mix_hash(),
754 Some(h.gas_limit()),
755 )
756 }
757 Ok(None) => {
758 debug!("Block header not found for block context initialization");
759 (None, None, None, None, None)
760 }
761 Err(e) => {
762 debug!(error = %e, "Failed to fetch block header for block context");
763 (None, None, None, None, None)
764 }
765 };
766
767 // Ensure cache directory exists
768 if let Some(cfg) = &cache_config {
769 let _ = fs::create_dir_all(cfg.chain_dir());
770 }
771
772 // Try to load EVM state from binary cache (bincode format)
773 let blockchain_db = if let Some(cfg) = &cache_config {
774 let binary_path = cfg.binary_state_cache_path();
775
776 if binary_path.exists() {
777 let meta = BlockchainDbMeta::default();
778 let db = BlockchainDb::new(meta, None);
779 if binary_state::load_binary_state(&db, &binary_path) {
780 db
781 } else {
782 let meta = BlockchainDbMeta::default();
783 BlockchainDb::new(meta, None)
784 }
785 } else {
786 let meta = BlockchainDbMeta::default();
787 BlockchainDb::new(meta, None)
788 }
789 } else {
790 let meta = BlockchainDbMeta::default();
791 BlockchainDb::new(meta, None)
792 };
793
794 // Filter storage by maintain list (if configured)
795 if let Some(cfg) = &cache_config {
796 let has_filter = !cfg.maintain_addresses.is_empty() || !cfg.maintain_slots.is_empty();
797 if has_filter {
798 let mut storage = blockchain_db.storage().write();
799 let before_contracts = storage.len();
800 let before_slots: usize = storage.values().map(|s| s.len()).sum();
801
802 // Remove addresses not in any maintain list
803 let addrs_to_remove: Vec<Address> = storage
804 .keys()
805 .filter(|addr| {
806 !cfg.maintain_addresses.contains(*addr)
807 && !cfg.maintain_slots.contains_key(*addr)
808 })
809 .copied()
810 .collect();
811 for addr in &addrs_to_remove {
812 storage.remove(addr);
813 }
814
815 // For maintain_slots addresses: keep only the specified slots
816 for (addr, allowed_slots) in &cfg.maintain_slots {
817 if let Some(addr_storage) = storage.get_mut(addr) {
818 addr_storage.retain(|slot, _| allowed_slots.contains(slot));
819 }
820 }
821
822 let after_contracts = storage.len();
823 let after_slots: usize = storage.values().map(|s| s.len()).sum();
824 drop(storage);
825
826 debug!(
827 contracts_removed = before_contracts.saturating_sub(after_contracts),
828 slots_removed = before_slots.saturating_sub(after_slots),
829 contracts_kept = after_contracts,
830 slots_kept = after_slots,
831 "Filtered cached storage by maintain list"
832 );
833 }
834 }
835
836 // Seed bytecodes from the bytecodes.bin cache.
837 // The binary EVM state cache stores accounts without bytecode,
838 // so this is always needed when a cache config is present.
839 if let Some(cfg) = &cache_config {
840 let bytecode_path = cfg.bytecode_cache_path();
841 if let Some(bytecode_cache) = BytecodeCache::load(&bytecode_path) {
842 let loaded_count = Self::seed_bytecodes_from_cache(&blockchain_db, &bytecode_cache);
843 if loaded_count > 0 {
844 debug!(
845 count = loaded_count,
846 path = ?bytecode_path,
847 "Loaded contract bytecodes from cache"
848 );
849 }
850 }
851 }
852
853 // Load immutable data cache (token decimals).
854 // This is still needed for validation and metadata lookups
855 let immutable_cache = cache_config
856 .as_ref()
857 .and_then(|cfg| {
858 let path = cfg.immutable_cache_path();
859 ImmutableDataCache::load(&path).inspect(|cache| {
860 debug!(
861 token_decimals = cache.token_decimals.len(),
862 path = ?path,
863 "Loaded immutable data from cache"
864 );
865 })
866 })
867 .unwrap_or_default();
868
869 // Pre-populate in-memory token decimals from immutable cache
870 let token_decimals = immutable_cache.token_decimals.clone();
871
872 // Create an RPC callback for direct eth_call before moving provider into backend.
873 // This bypasses revm simulation for batch queries where lazy storage fetching is too slow.
874 let provider_for_rpc = provider.clone();
875 let rpc_caller: RpcCallFn = Arc::new(move |to: Address, calldata: Bytes| {
876 // Guard against panicking inside `block_in_place` on a current-thread
877 // runtime (or when no runtime is present): degrade to a typed error.
878 let handle = block_in_place_handle()?;
879 tokio::task::block_in_place(|| {
880 handle.block_on(async {
881 let tx = TransactionRequest::default()
882 .to(to)
883 .input(alloy_primitives::Bytes::from(calldata.to_vec()).into());
884 provider_for_rpc
885 .call(tx.into())
886 .await
887 .map_err(|e| anyhow!("{}", e))
888 })
889 })
890 });
891
892 // Create a batch storage fetcher that bypasses SharedBackend for bulk prefetch.
893 // Uses JSON-RPC batch requests to send multiple eth_getStorageAt calls in a
894 // single HTTP request, dramatically reducing round-trip overhead.
895 let provider_for_batch = provider.clone();
896 let batch_block_id = Arc::new(Mutex::new(block_id));
897 let batch_block_ref = batch_block_id.clone();
898 let storage_batch_fetcher: StorageBatchFetchFn = Arc::new(
899 move |requests: Vec<(Address, U256)>, block: Option<BlockId>| {
900 use futures::stream::{self, StreamExt};
901 // Max items per JSON-RPC batch. RPC providers typically limit batch
902 // size to ~1000 items. Reduced from 200 to avoid 429s on Base.
903 let batch_size: usize = match cache_speed_mode() {
904 CacheSpeedMode::Fast => 150,
905 CacheSpeedMode::Normal => 100,
906 CacheSpeedMode::Slow => 75,
907 CacheSpeedMode::XSlow => 25,
908 };
909 // Max concurrent HTTP batch requests. Each batch contains batch_size
910 // individual eth_getStorageAt calls. Limiting concurrency prevents
911 // thundering herd when prefetching thousands of storage slots.
912 let max_concurrent: usize = match cache_speed_mode() {
913 CacheSpeedMode::Fast => 8,
914 CacheSpeedMode::Normal => 6,
915 CacheSpeedMode::Slow => 4,
916 CacheSpeedMode::XSlow => 1,
917 };
918
919 // Guard against panicking inside `block_in_place` on a
920 // current-thread runtime (or when no runtime is present): return
921 // an `Err` result for every requested slot instead.
922 let handle = match block_in_place_handle() {
923 Ok(handle) => handle,
924 Err(e) => {
925 let msg = e.to_string();
926 return requests
927 .into_iter()
928 .map(|(addr, slot)| (addr, slot, Err(anyhow!("{}", msg))))
929 .collect();
930 }
931 };
932 // Pin to the explicitly-requested block when given, else the
933 // cache's currently-pinned block. Capturing the block at the call
934 // site is what lets the deferred freshness validator fetch at the
935 // snapshot's block despite a later `set_block`.
936 let current_block = block.unwrap_or_else(|| *batch_block_ref.lock().unwrap());
937 tokio::task::block_in_place(|| {
938 handle.block_on(async {
939 let mut results = Vec::with_capacity(requests.len());
940
941 // Build and send JSON-RPC batches (each batch = one HTTP request)
942 let batch_futs: Vec<_> = requests
943 .chunks(batch_size)
944 .map(|chunk| {
945 let client = provider_for_batch.client();
946 let mut batch = alloy_rpc_client::BatchRequest::new(client);
947 let mut waiters = Vec::with_capacity(chunk.len());
948
949 for &(addr, slot) in chunk {
950 let params = (addr, slot, current_block);
951 match batch.add_call::<_, U256>("eth_getStorageAt", ¶ms) {
952 Ok(waiter) => waiters.push((addr, slot, Some(waiter))),
953 Err(e) => {
954 // Serialization error — rare, treat as failure
955 tracing::warn!(
956 ?addr,
957 ?slot,
958 "batch request serialization failed: {}",
959 e
960 );
961 waiters.push((addr, slot, None));
962 }
963 }
964 }
965
966 async move {
967 // Send the batch as a single HTTP request
968 let send_result = batch.send().await;
969 let mut chunk_results = Vec::with_capacity(waiters.len());
970
971 for (addr, slot, waiter) in waiters {
972 if let Some(waiter) = waiter {
973 if send_result.is_ok() {
974 match waiter.await {
975 Ok(value) => {
976 chunk_results.push((addr, slot, Ok(value)));
977 }
978 Err(e) => {
979 chunk_results.push((
980 addr,
981 slot,
982 Err(anyhow!("{}", e)),
983 ));
984 }
985 }
986 } else {
987 chunk_results.push((
988 addr,
989 slot,
990 Err(anyhow!("batch send failed")),
991 ));
992 }
993 } else {
994 chunk_results.push((
995 addr,
996 slot,
997 Err(anyhow!("serialization failed")),
998 ));
999 }
1000 }
1001 chunk_results
1002 }
1003 })
1004 .collect();
1005
1006 // Fire batches with bounded concurrency (`max_concurrent`) to avoid
1007 // a thundering herd; per-batch size is the speed-mode `batch_size`
1008 // chosen above, so throughput scales without overwhelming RPC providers.
1009 let all_batch_results: Vec<Vec<_>> = stream::iter(batch_futs)
1010 .buffer_unordered(max_concurrent)
1011 .collect()
1012 .await;
1013 for batch_results in all_batch_results {
1014 results.extend(batch_results);
1015 }
1016 results
1017 })
1018 })
1019 },
1020 );
1021
1022 // Resolve the chain ID reported to simulations (the `CHAINID` opcode). A
1023 // disk `CacheConfig` is authoritative (its `chain_id` also namespaces the
1024 // on-disk cache directory); otherwise infer it from the provider via
1025 // `eth_chainId`, falling back to 1 (Ethereum mainnet) only if that query
1026 // fails. Resolved before `provider` is moved into the backend below.
1027 // Prefer setting it explicitly through `EvmCacheBuilder::chain_id`.
1028 let chain_id = match cache_config.as_ref() {
1029 Some(cfg) => cfg.chain_id,
1030 None => match provider.get_chain_id().await {
1031 Ok(id) => id,
1032 Err(e) => {
1033 debug!(
1034 error = %e,
1035 "Failed to infer chain ID from provider; defaulting to 1 (Ethereum mainnet). Set it explicitly via EvmCacheBuilder::chain_id."
1036 );
1037 1
1038 }
1039 },
1040 };
1041
1042 // Spawn the backend handler on a background task
1043 let backend =
1044 SharedBackend::spawn_backend(provider, blockchain_db.clone(), Some(block_id)).await;
1045
1046 let db = CacheDB::new(backend.clone());
1047
1048 // Resolve the shared-memory pre-allocation. For `Auto` we size from the
1049 // amount of layer-2 chain state actually loaded (post-filter), so a large
1050 // bincode state file yields a larger buffer; `Fixed` ignores the count.
1051 let loaded_slots = match shared_memory_capacity {
1052 SharedMemoryCapacity::Auto => blockchain_db
1053 .storage()
1054 .read()
1055 .values()
1056 .map(|s| s.len())
1057 .sum(),
1058 SharedMemoryCapacity::Fixed(_) => 0,
1059 };
1060 let shared_memory_capacity = shared_memory_capacity.resolve(loaded_slots);
1061
1062 Self {
1063 backend,
1064 blockchain_db,
1065 db,
1066 token_decimals,
1067 block,
1068 cache_config,
1069 immutable_cache,
1070 timestamp_override: None,
1071 chain_id,
1072 block_number,
1073 basefee,
1074 coinbase,
1075 prevrandao,
1076 block_gas_limit,
1077 shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(shared_memory_capacity))),
1078 rpc_caller: Some(rpc_caller),
1079 storage_batch_fetcher: Some(storage_batch_fetcher),
1080 batch_block_id,
1081 erc20_balance_slots: HashMap::new(),
1082 spec_id,
1083 base: None,
1084 base_dirty: HashSet::new(),
1085 base_full_rebuild: false,
1086 base_storage_lens: HashMap::new(),
1087 shared_memory_capacity,
1088 }
1089 }
1090
1091 /// Seed contract bytecodes into the BlockchainDb from a bytecode cache.
1092 ///
1093 /// This allows subsequent EVM executions to use cached bytecode instead of
1094 /// fetching from RPC. Storage slots will still be fetched fresh since they
1095 /// may have changed between blocks.
1096 fn seed_bytecodes_from_cache(db: &BlockchainDb, cache: &BytecodeCache) -> usize {
1097 let mut count = 0;
1098 for (addr, entry) in &cache.contracts {
1099 if entry.bytecode.is_empty() {
1100 continue;
1101 }
1102
1103 // Create bytecode and compute hash
1104 let bytecode = Bytecode::new_raw(Bytes::from(entry.bytecode.clone()));
1105 let code_hash: B256 = bytecode.hash_slow();
1106
1107 // Create account info with bytecode but zeroed balance/nonce
1108 // The balance/nonce will be fetched from RPC if needed during execution
1109 let info = AccountInfo {
1110 balance: U256::ZERO,
1111 nonce: 0,
1112 code_hash,
1113 code: Some(bytecode),
1114 account_id: None,
1115 };
1116
1117 db.db().do_insert_account(*addr, info);
1118 count += 1;
1119 }
1120 count
1121 }
1122
1123 /// Create a new EvmCache from an existing SharedBackend.
1124 ///
1125 /// Useful when you want to share a backend between multiple caches
1126 /// (e.g. parallel simulation threads).
1127 ///
1128 /// **Shared pinned block.** A `SharedBackend` owns a single pinned fork
1129 /// height. Calling [`set_block`](Self::set_block) / `repin_to_block` on *any*
1130 /// cache built from the same backend re-pins the RPC fork height for **all**
1131 /// of them. Sibling caches sharing one backend should agree on a block and not
1132 /// re-pin independently; build separate backends if they must fork at
1133 /// different heights.
1134 pub fn from_backend(
1135 backend: SharedBackend,
1136 blockchain_db: BlockchainDb,
1137 block: BlockId,
1138 chain_id: u64,
1139 block_number: Option<u64>,
1140 basefee: Option<u64>,
1141 spec_id: SpecId,
1142 ) -> Self {
1143 let db = CacheDB::new(backend.clone());
1144 Self {
1145 backend,
1146 blockchain_db,
1147 db,
1148 token_decimals: HashMap::new(),
1149 block,
1150 cache_config: None,
1151 immutable_cache: ImmutableDataCache::default(),
1152 timestamp_override: None,
1153 chain_id,
1154 block_number,
1155 basefee,
1156 coinbase: None,
1157 prevrandao: None,
1158 block_gas_limit: None,
1159 shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(
1160 DEFAULT_SHARED_MEMORY_CAPACITY,
1161 ))),
1162 rpc_caller: None,
1163 storage_batch_fetcher: None,
1164 batch_block_id: Arc::new(Mutex::new(block)),
1165 erc20_balance_slots: HashMap::new(),
1166 spec_id,
1167 base: None,
1168 base_dirty: HashSet::new(),
1169 base_full_rebuild: false,
1170 base_storage_lens: HashMap::new(),
1171 shared_memory_capacity: DEFAULT_SHARED_MEMORY_CAPACITY,
1172 }
1173 }
1174
1175 /// Flush the cache state to disk.
1176 ///
1177 /// This persists:
1178 /// 1. Unified EVM state (accounts + storage) to `evm_state.bin` (bincode)
1179 /// 2. Contract bytecodes to `bytecodes.bin`
1180 /// 3. Immutable data (token decimals) to `immutable_data.bin`
1181 ///
1182 /// Call this after loading hot contract state and running simulations to
1183 /// speed up subsequent runs.
1184 /// The cache is also automatically flushed when the EvmCache is dropped.
1185 pub fn flush(&self) -> Result<()> {
1186 if let Some(cfg) = &self.cache_config {
1187 // Save EVM state to binary cache (bincode format)
1188 let binary_path = cfg.binary_state_cache_path();
1189 binary_state::save_binary_state(&self.blockchain_db, &binary_path)
1190 .with_context(|| format!("failed to save binary state cache to {binary_path:?}"))?;
1191
1192 // Save bytecode cache
1193 let bytecode_path = cfg.bytecode_cache_path();
1194 let mut bytecode_cache = BytecodeCache::load(&bytecode_path).unwrap_or_default();
1195 bytecode_cache.merge_from_db(&self.blockchain_db);
1196 bytecode_cache
1197 .save(&bytecode_path)
1198 .with_context(|| format!("failed to save bytecode cache to {bytecode_path:?}"))?;
1199 debug!(
1200 count = bytecode_cache.contracts.len(),
1201 path = ?bytecode_path,
1202 "Updated bytecode cache (binary format)"
1203 );
1204
1205 // Save the immutable data cache
1206 let immutable_path = cfg.immutable_cache_path();
1207 self.immutable_cache
1208 .save(&immutable_path)
1209 .with_context(|| {
1210 format!("failed to save immutable data cache to {immutable_path:?}")
1211 })?;
1212 debug!(
1213 token_decimals = self.immutable_cache.token_decimals.len(),
1214 path = ?immutable_path,
1215 "Updated immutable data cache"
1216 );
1217 }
1218 Ok(())
1219 }
1220
1221 /// Get the cache configuration, if any.
1222 ///
1223 /// Returns `None` when the cache is purely in-memory (no disk persistence),
1224 /// i.e. constructed without a [`CacheConfig`] or via
1225 /// [`from_backend`](Self::from_backend).
1226 pub fn cache_config(&self) -> Option<&CacheConfig> {
1227 self.cache_config.as_ref()
1228 }
1229
1230 /// Run a synchronous direct mutation against the underlying [`BlockchainDb`]
1231 /// and invalidate the memoized snapshot base afterwards.
1232 ///
1233 /// This is the preferred escape hatch for unavoidable layer-2 map writes such
1234 /// as `accounts().write().insert(...)` or `storage().write().insert(...)`.
1235 /// The closure still bypasses the CacheDB overlay and the normal write funnel,
1236 /// so use higher-level mutators when they can express the change. Unlike
1237 /// [`unchecked_blockchain_db`](Self::unchecked_blockchain_db), this wrapper
1238 /// keeps the copy-on-write snapshot base honest automatically after in-place
1239 /// overwrites whose map cardinality does not change.
1240 pub fn with_blockchain_db_mut<R>(&mut self, f: impl FnOnce(&BlockchainDb) -> R) -> R {
1241 let result = f(&self.blockchain_db);
1242 self.invalidate_base();
1243 result
1244 }
1245
1246 /// Get an unchecked reference to the underlying [`BlockchainDb`] (the layer-2
1247 /// backend store of accounts, storage, and bytecodes).
1248 ///
1249 /// This exposes an internal store and bypasses the cache's two-layer
1250 /// consistency model: reads here see only the backend layer, not the CacheDB
1251 /// overlay, and any writes performed through it skip the overlay. Prefer
1252 /// higher-level accessors or [`with_blockchain_db_mut`](Self::with_blockchain_db_mut)
1253 /// for direct synchronous writes.
1254 ///
1255 /// # Snapshot base
1256 /// Writing layer 2 directly through this unchecked handle also bypasses the
1257 /// memoized copy-on-write snapshot base (Pillar A). The next
1258 /// [`create_snapshot`](Self::create_snapshot) only performs a count/absence
1259 /// growth scan over layer 2, which catches lazy RPC-populated accounts/slots
1260 /// because that path only appends at a fixed block. It does **not** catch
1261 /// direct in-place changes where cardinality is unchanged: overwriting an
1262 /// existing storage slot, or changing an existing account's info/code/balance
1263 /// without adding a new account, can leave a stale snapshot base. After such a
1264 /// direct write, call
1265 /// [`invalidate_snapshot_base`](Self::invalidate_snapshot_base) (or re-pin via
1266 /// [`set_block`](Self::set_block)) before the next snapshot. Writes via the
1267 /// crate's own mutators (`inject_storage_batch`, `apply_update`, the `inject_*`
1268 /// helpers, the purges) keep the base honest automatically.
1269 pub fn unchecked_blockchain_db(&self) -> &BlockchainDb {
1270 &self.blockchain_db
1271 }
1272
1273 /// Get an unchecked reference to the underlying [`SharedBackend`] (the lazy
1274 /// RPC-backed fetcher shared across clones).
1275 ///
1276 /// This exposes an internal handle and bypasses the cache's two-layer consistency
1277 /// model: it reads/fetches directly without consulting the CacheDB overlay.
1278 /// Prefer the higher-level accessors; use with care.
1279 ///
1280 /// # Snapshot base
1281 /// Lazy RPC fetches through this backend only append missing accounts/slots at
1282 /// the pinned block, so the snapshot growth scan catches them without an
1283 /// explicit invalidation. Direct `SharedBackend::insert_or_update_storage` /
1284 /// `insert_or_update_address` calls are different: they enqueue a background
1285 /// handler request that can rewrite layer-2 entries **in place**, leaving the
1286 /// memoized copy-on-write base stale at an unchanged slot/account count.
1287 ///
1288 /// If you use those helpers directly, first synchronize with the backend
1289 /// handler by reading back the updated account/slot through `SharedBackend`
1290 /// (for example via `basic_ref` / `storage_ref`), then call
1291 /// [`invalidate_snapshot_base`](Self::invalidate_snapshot_base) before the next
1292 /// [`create_snapshot`](Self::create_snapshot). Calling
1293 /// `invalidate_snapshot_base` immediately after `insert_or_update_*` is not, by
1294 /// itself, a guarantee that the queued update has been applied before the next
1295 /// snapshot.
1296 pub fn unchecked_backend(&self) -> &SharedBackend {
1297 &self.backend
1298 }
1299
1300 /// Get a mutable reference to the underlying [`ForkCacheDB`] (the layer-1
1301 /// CacheDB overlay).
1302 ///
1303 /// This exposes an internal and bypasses the cache's two-layer consistency
1304 /// model: writes made here land only in the overlay and are not mirrored
1305 /// into the BlockchainDb backend, so parallel tasks sharing the backend
1306 /// will not see them. Prefer the higher-level mutators; use with care.
1307 pub fn db_mut(&mut self) -> &mut ForkCacheDB {
1308 &mut self.db
1309 }
1310
1311 /// Make a direct RPC `eth_call` to the node, bypassing revm simulation.
1312 ///
1313 /// This is much faster than `call_raw` for batch operations because the RPC
1314 /// node has all state in memory and doesn't need lazy storage fetching.
1315 /// Returns `None` if no RPC caller is available (e.g. `from_backend` constructor).
1316 ///
1317 /// # Panics
1318 /// Must be called from within a **multi-thread** tokio runtime: the callback
1319 /// drives the async `eth_call` to completion via
1320 /// `tokio::task::block_in_place`. On a current-thread runtime (or with no
1321 /// runtime), the callback degrades to an `Err` rather than panicking, but
1322 /// `block_in_place` itself will panic if invoked from a non-worker thread of
1323 /// a multi-thread runtime.
1324 pub fn rpc_call(&self, to: Address, calldata: Bytes) -> Option<Result<Bytes>> {
1325 self.rpc_caller
1326 .as_ref()
1327 .map(|caller| (caller)(to, calldata))
1328 }
1329
1330 /// Get the batch storage fetcher, if available.
1331 ///
1332 /// Returns `None` when constructed via `from_backend` (no provider available).
1333 ///
1334 /// # Panics
1335 /// The returned [`StorageBatchFetchFn`] must be invoked from within a
1336 /// **multi-thread** tokio runtime: it drives concurrent `eth_getStorageAt`
1337 /// calls to completion via `tokio::task::block_in_place`. On a
1338 /// current-thread runtime (or with no runtime) it degrades to an `Err`
1339 /// result for every requested slot rather than panicking, but
1340 /// `block_in_place` itself will panic if invoked from a non-worker thread of
1341 /// a multi-thread runtime.
1342 pub fn storage_batch_fetcher(&self) -> Option<&StorageBatchFetchFn> {
1343 self.storage_batch_fetcher.as_ref()
1344 }
1345
1346 /// Inject batch-fetched storage values directly into BlockchainDb (layer 2).
1347 ///
1348 /// This bypasses SharedBackend and makes values available for subsequent
1349 /// `storage_ref()` calls and EVM SLOADs. Used after `StorageBatchFetchFn`
1350 /// returns results to populate the cache in bulk.
1351 ///
1352 /// Takes `&mut self` (as of Pillar A) so it can mark each touched address dirty
1353 /// for the memoized copy-on-write base; the write itself is still a direct
1354 /// layer-2 backend write. Overwriting an existing slot at an unchanged slot
1355 /// count is invalidated here too, since the `refresh_base` growth scan only
1356 /// catches length changes.
1357 pub fn inject_storage_batch(&mut self, results: &[(Address, U256, U256)]) {
1358 {
1359 let mut storage = self.blockchain_db.storage().write();
1360 for &(addr, slot, value) in results {
1361 storage.entry(addr).or_default().insert(slot, value);
1362 }
1363 }
1364 for &(addr, _, _) in results {
1365 self.mark_base_dirty(addr);
1366 }
1367 }
1368
1369 /// Inject freshly-fetched storage values, healing **both** cache layers.
1370 ///
1371 /// Like [`inject_storage_batch`](Self::inject_storage_batch) this writes each
1372 /// value into the BlockchainDb backend (layer 2). Additionally, for any
1373 /// address that *already* has a CacheDB overlay entry (layer 1), it writes
1374 /// the slot into that overlay too.
1375 ///
1376 /// This matters because both [`create_snapshot`](Self::create_snapshot) and
1377 /// the synchronous EVM SLOAD path let the overlay win over the backend. A
1378 /// correction written only to layer 2 would be shadowed by a stale layer-1
1379 /// slot, so the cache could never converge — the freshness validator would
1380 /// re-detect the same change and re-correct it every cycle. Writing through
1381 /// the overlay keeps the layer that wins authoritative.
1382 ///
1383 /// It deliberately does **not** create a new overlay account for an address
1384 /// that has none: such a slot is layer-2-only (e.g. cold prefetch), where
1385 /// the backend write is already authoritative and materializing an overlay
1386 /// entry would pollute layer 1 and could shadow later RPC reads.
1387 pub fn inject_storage_batch_fresh(&mut self, results: &[(Address, U256, U256)]) {
1388 // Thin wrapper over the unified write primitive (the F1 fix now lives in
1389 // `apply_slot`). Each tuple becomes a write-through `StateUpdate::Slot`;
1390 // the returned diff is discarded to preserve this method's `-> ()` API.
1391 let updates: Vec<StateUpdate> = results
1392 .iter()
1393 .map(|&(addr, slot, value)| StateUpdate::slot(addr, slot, value))
1394 .collect();
1395 let _ = self.apply_updates(&updates);
1396 }
1397
1398 /// Apply a single targeted [`StateUpdate`], returning a [`StateDiff`] of what
1399 /// actually changed.
1400 ///
1401 /// This is the single primitive that writes the state-update vocabulary
1402 /// across both cache layers with one consistent, documented policy. It is
1403 /// **synchronous and infallible** — a write, not a fetch, so it never touches
1404 /// RPC and never errors. See the [`state_update`](crate::state_update) module
1405 /// for the dual-layer write-through policy and the diff semantics.
1406 ///
1407 /// - [`StateUpdate::Slot`] — write `value` into the backend (layer 2) always,
1408 /// and into the overlay (layer 1) only if an overlay account already
1409 /// exists. Records a [`SlotChange`] only when the value actually changes
1410 /// (`old.unwrap_or(ZERO) != value`).
1411 /// - [`StateUpdate::SlotDelta`] — *relative*, cold-aware. If the slot has a
1412 /// cached value, write the saturating delta through the same path and record
1413 /// a [`SlotChange`] iff it changed; if the slot is cold (absent from both
1414 /// layers), apply nothing and surface a `SkippedDelta` in `diff.skipped`.
1415 /// - [`StateUpdate::BalanceDelta`] — *relative*, cold-aware native-balance
1416 /// update. If the account is present in either layer, apply the saturating
1417 /// delta to its balance (nonce/code preserved) write-through and record an
1418 /// [`AccountChange`] iff it changed; if the account is cold (absent from both
1419 /// layers), apply nothing and surface a [`SkippedBalanceDelta`] in
1420 /// `diff.skipped_balances` (no default account is materialized).
1421 /// - [`StateUpdate::Account`] — load the current `AccountInfo` from the cached
1422 /// layers (no RPC), apply each `Some` patch field (recomputing the code hash
1423 /// when `code` is set), then write through with the same layer policy.
1424 /// Records an [`AccountChange`] with `Some((old, new))` only for fields
1425 /// that changed. If the account is cold (absent from both layers), apply
1426 /// nothing and surface a [`SkippedAccountPatch`] in
1427 /// `diff.skipped_accounts`.
1428 /// - [`StateUpdate::AccountUpsert`] — same patch semantics, but intentionally
1429 /// materializes a cold/default account when absent from both layers.
1430 /// - [`StateUpdate::Purge`] — dispatch to the matching purge layer logic and
1431 /// record a [`PurgeRecord`].
1432 ///
1433 /// # Warning — relative updates can be skipped
1434 ///
1435 /// A cold-aware update targeting a **cold** address is *dropped, not applied*
1436 /// unless it is an explicit [`StateUpdate::AccountUpsert`]. Because a skip
1437 /// produces no change, it is invisible to the changes-only
1438 /// [`StateDiff::is_empty`] / [`StateDiff::len`] success check, so after
1439 /// applying cold-aware updates the caller **must** inspect
1440 /// [`StateDiff::has_skipped`] (or the `skipped_*` fields) and fetch+seed the
1441 /// cold target.
1442 ///
1443 /// ```no_run
1444 /// # use alloy_primitives::{Address, U256};
1445 /// # use evm_fork_cache::StateUpdate;
1446 /// # fn example(cache: &mut evm_fork_cache::cache::EvmCache) {
1447 /// let contract = Address::repeat_byte(0x01);
1448 /// let diff = cache.apply_update(&StateUpdate::slot(contract, U256::from(0), U256::from(42)));
1449 /// assert_eq!(diff.slots.len(), 1);
1450 /// # }
1451 /// ```
1452 pub fn apply_update(&mut self, update: &StateUpdate) -> StateDiff {
1453 let mut diff = StateDiff::default();
1454 match update {
1455 StateUpdate::Slot {
1456 address,
1457 slot,
1458 value,
1459 } => {
1460 if let Some(change) = self.apply_slot(*address, *slot, *value) {
1461 diff.slots.push(change);
1462 }
1463 }
1464 StateUpdate::SlotDelta {
1465 address,
1466 slot,
1467 delta,
1468 } => match self.cached_storage_value(*address, *slot) {
1469 // Hot slot: apply the saturating delta write-through. Build the
1470 // change from the value we already read (do not route through
1471 // `apply_slot`, which would re-read the same slot — §16.9.1).
1472 Some(current) => {
1473 let new = delta.apply(current);
1474 self.write_slot_through(*address, *slot, new);
1475 if current != new {
1476 diff.slots.push(SlotChange {
1477 address: *address,
1478 slot: *slot,
1479 old: current,
1480 new,
1481 });
1482 }
1483 }
1484 // Cold slot: applying `0 ± amount` would corrupt an unknown value,
1485 // so write nothing and surface the skip for the caller to seed.
1486 None => diff.skipped.push(SkippedDelta {
1487 address: *address,
1488 slot: *slot,
1489 delta: *delta,
1490 }),
1491 },
1492 StateUpdate::SlotMasked {
1493 address,
1494 slot,
1495 mask,
1496 value,
1497 } => match self.cached_storage_value(*address, *slot) {
1498 // Hot slot: overwrite only the masked bits, preserving the rest.
1499 // Build the change from the value we already read (mirroring the
1500 // `SlotDelta` arm; do not re-read through `apply_slot`).
1501 Some(old) => {
1502 let new = (old & !*mask) | (*value & *mask);
1503 self.write_slot_through(*address, *slot, new);
1504 if old != new {
1505 diff.slots.push(SlotChange {
1506 address: *address,
1507 slot: *slot,
1508 old,
1509 new,
1510 });
1511 }
1512 }
1513 // Cold slot: the un-masked bits are unknown, so the result cannot
1514 // be computed; write nothing and surface the skip for re-seeding.
1515 None => diff.skipped_masks.push(SkippedMask {
1516 address: *address,
1517 slot: *slot,
1518 mask: *mask,
1519 value: *value,
1520 }),
1521 },
1522 StateUpdate::BalanceDelta { address, delta } => {
1523 match self.apply_balance_delta(*address, *delta) {
1524 // Hot account: the saturating delta was applied.
1525 Ok(Some(change)) => diff.accounts.push(change),
1526 // Hot account but no change (e.g. Sub from 0, Add of 0).
1527 Ok(None) => {}
1528 // Cold account: surface the skip; nothing was materialized.
1529 Err(skipped) => diff.skipped_balances.push(skipped),
1530 }
1531 }
1532 StateUpdate::Account { address, patch } => {
1533 match self.apply_account_patch(*address, patch, false) {
1534 Ok(Some(change)) => diff.accounts.push(change),
1535 Ok(None) => {}
1536 Err(skipped) => diff.skipped_accounts.push(skipped),
1537 }
1538 }
1539 StateUpdate::AccountUpsert { address, patch } => {
1540 if let Some(change) = self
1541 .apply_account_patch(*address, patch, true)
1542 .expect("AccountUpsert never skips cold account patches")
1543 {
1544 diff.accounts.push(change);
1545 }
1546 }
1547 StateUpdate::Purge { address, scope } => {
1548 diff.purged.push(self.apply_purge(*address, scope));
1549 }
1550 }
1551 diff
1552 }
1553
1554 /// Apply a batch of [`StateUpdate`]s left-to-right, merging each per-update
1555 /// [`StateDiff`].
1556 ///
1557 /// Later updates observe the effect of earlier ones: two `Slot` writes to the
1558 /// same key record `old → a` then `a → b`. Like
1559 /// [`apply_update`](Self::apply_update) this is synchronous and infallible.
1560 ///
1561 /// # Performance — batched single-lock fast-path
1562 ///
1563 /// Consecutive `Slot`/`SlotDelta` writes are processed holding the backend
1564 /// storage write-guard **once** for the run (the overlay map is lock-free), so
1565 /// a bulk slot seed pays one lock acquisition instead of one read + one write
1566 /// lock per slot. Apply order is preserved: when an `Account`/`BalanceDelta`/
1567 /// `Purge` update is reached the guard is dropped first (those take the
1568 /// `accounts()` / `storage()` locks themselves — holding the storage
1569 /// write-guard across them would deadlock the non-reentrant `RwLock`), the
1570 /// update is processed via [`apply_update`](Self::apply_update), then the guard
1571 /// is lazily re-acquired on the next slot run. The result is byte-identical to
1572 /// folding [`apply_update`](Self::apply_update) over the batch.
1573 ///
1574 /// # Warning — relative updates can be skipped
1575 ///
1576 /// See [`apply_update`](Self::apply_update): a cold relative update is dropped,
1577 /// not applied, and is invisible to [`StateDiff::is_empty`] /
1578 /// [`StateDiff::len`]. After a batch with relative updates, check
1579 /// [`StateDiff::has_skipped`].
1580 pub fn apply_updates(&mut self, updates: &[StateUpdate]) -> StateDiff {
1581 let mut diff = StateDiff::default();
1582 let mut i = 0;
1583 while i < updates.len() {
1584 match &updates[i] {
1585 // A run of consecutive slot writes: process them under a single
1586 // held storage write-guard, then advance past the run.
1587 StateUpdate::Slot { .. } | StateUpdate::SlotDelta { .. } => {
1588 let run_end = updates[i..]
1589 .iter()
1590 .position(|u| {
1591 !matches!(u, StateUpdate::Slot { .. } | StateUpdate::SlotDelta { .. })
1592 })
1593 .map(|off| i + off)
1594 .unwrap_or(updates.len());
1595 self.apply_slot_run(&updates[i..run_end], &mut diff);
1596 i = run_end;
1597 }
1598 // Account / BalanceDelta / Purge: no held guard (they take their
1599 // own locks), so route through the single-update primitive.
1600 _ => {
1601 diff.merge(self.apply_update(&updates[i]));
1602 i += 1;
1603 }
1604 }
1605 }
1606 diff
1607 }
1608
1609 /// Apply a run of consecutive `Slot`/`SlotDelta` updates under one held backend
1610 /// storage write-guard (§16.9.2), merging each change into `diff`.
1611 ///
1612 /// The backend storage guard is acquired once for the whole run; overlay access
1613 /// is lock-free (`self.db.cache.accounts`). The old-value read stays
1614 /// `account_state`-aware (matching [`cached_storage_value`](Self::cached_storage_value)):
1615 /// for an overlay account whose slot is absent, a `StorageCleared`/`NotExisting`
1616 /// state reads ZERO and the backend is **not** consulted. Behavior is identical
1617 /// to applying each update via [`apply_update`](Self::apply_update); the
1618 /// `apply_updates_batched_equals_sequential` test pins this.
1619 fn apply_slot_run(&mut self, run: &[StateUpdate], diff: &mut StateDiff) {
1620 // Borrow the two layers as disjoint fields: the backend storage guard
1621 // (layer 2) held for the whole run, and the overlay accounts map (layer 1,
1622 // lock-free). Base invalidation is deferred until after the guard is
1623 // dropped (it needs `&mut self`): collect the layer-2 addresses written
1624 // here and mark them dirty below.
1625 let mut dirtied: Vec<Address> = Vec::new();
1626 let overlay = &mut self.db.cache.accounts;
1627 let mut storage = self.blockchain_db.storage().write();
1628
1629 for update in run {
1630 // Resolve `(address, slot, old, new)` for the write; a cold SlotDelta
1631 // is skipped here (write nothing). `old` is the `account_state`-aware
1632 // read (overlay ▸ cleared-as-ZERO ▸ backend), reused for both the write
1633 // gate and the change record so each slot is read at most once.
1634 let (address, slot, old, new) = match update {
1635 StateUpdate::Slot {
1636 address,
1637 slot,
1638 value,
1639 } => {
1640 let old = read_slot_account_state_aware(overlay, &storage, *address, *slot)
1641 .unwrap_or(U256::ZERO);
1642 (*address, *slot, old, *value)
1643 }
1644 StateUpdate::SlotDelta {
1645 address,
1646 slot,
1647 delta,
1648 } => match read_slot_account_state_aware(overlay, &storage, *address, *slot) {
1649 // Hot: apply the saturating delta to the value already read.
1650 Some(current) => (*address, *slot, current, delta.apply(current)),
1651 // Cold: skip and surface (write nothing).
1652 None => {
1653 diff.skipped.push(SkippedDelta {
1654 address: *address,
1655 slot: *slot,
1656 delta: *delta,
1657 });
1658 continue;
1659 }
1660 },
1661 // The caller only ever hands this method slot updates.
1662 _ => unreachable!("apply_slot_run only processes Slot/SlotDelta"),
1663 };
1664
1665 write_slot_into(overlay, &mut storage, address, slot, new);
1666 // Layer 2 was written for this address → it must be re-folded into the
1667 // memoized base. Mirrors `write_slot_through`'s `mark_base_dirty`.
1668 dirtied.push(address);
1669 if old != new {
1670 diff.slots.push(SlotChange {
1671 address,
1672 slot,
1673 old,
1674 new,
1675 });
1676 }
1677 }
1678
1679 // Drop the storage write-guard before taking `&mut self` for invalidation.
1680 drop(storage);
1681 for address in dirtied {
1682 self.mark_base_dirty(address);
1683 }
1684 }
1685
1686 /// Write-through a single storage slot (§5.1). Returns a [`SlotChange`] iff
1687 /// the slot's value actually changes.
1688 fn apply_slot(&mut self, address: Address, slot: U256, value: U256) -> Option<SlotChange> {
1689 // Old value: overlay ▸ backend ▸ None (treated as ZERO).
1690 let old = self
1691 .cached_storage_value(address, slot)
1692 .unwrap_or(U256::ZERO);
1693
1694 self.write_slot_through(address, slot, value);
1695
1696 // Record only an actual change.
1697 (old != value).then_some(SlotChange {
1698 address,
1699 slot,
1700 old,
1701 new: value,
1702 })
1703 }
1704
1705 /// The single dual-layer slot write path (§5.1), shared by [`apply_slot`],
1706 /// the [`StateUpdate::SlotDelta`] handler, and [`modify_slot`](Self::modify_slot).
1707 ///
1708 /// Backend (layer 2) is always written; the overlay (layer 1) is written only
1709 /// if an overlay account already exists. A new overlay account is never
1710 /// materialized: that preserves the layer-2-only invariant (a fresh
1711 /// `StorageCleared` overlay account would read missing slots as ZERO and could
1712 /// shadow later RPC reads), and an absent overlay entry falls through to the
1713 /// backend on reads so the backend write is authoritative.
1714 fn write_slot_through(&mut self, address: Address, slot: U256, value: U256) {
1715 // Backend (layer 2): always write.
1716 {
1717 let mut storage = self.blockchain_db.storage().write();
1718 storage.entry(address).or_default().insert(slot, value);
1719 }
1720
1721 // Overlay (layer 1): write only if an overlay account already exists.
1722 if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
1723 db_account.storage.insert(slot, value);
1724 }
1725
1726 // Layer 2 changed → invalidate the memoized base for this address (D2:
1727 // over-invalidation when also shadowed by layer 1 is safe).
1728 self.mark_base_dirty(address);
1729 }
1730
1731 /// Read-modify-write one storage slot through a caller-supplied transform.
1732 ///
1733 /// The general closure escape hatch behind [`StateUpdate::SlotDelta`] (the
1734 /// data-level form flows through [`apply_update`](Self::apply_update); this is
1735 /// for arbitrary transforms). `f` is called with the current cached value
1736 /// (overlay ▸ backend ▸ `None` when the slot is cold) and decides the new
1737 /// value:
1738 ///
1739 /// - `Some(new)` writes `new` through both layers (the same write path as
1740 /// [`StateUpdate::Slot`]) and returns a [`SlotChange`] iff it changed
1741 /// (`old.unwrap_or(ZERO) != new`);
1742 /// - `None` writes nothing and returns `None`.
1743 ///
1744 /// The caller owns the cold/overflow policy. To skip cold slots (the
1745 /// cold-aware read-modify-write rule), map through the `Option`:
1746 /// `|cur| cur.map(|v| v.saturating_add(amount))` leaves a cold slot untouched.
1747 /// To write an absolute value regardless, ignore the argument: `|_| Some(v)`.
1748 ///
1749 /// ```no_run
1750 /// # use alloy_primitives::{Address, U256};
1751 /// # fn example(cache: &mut evm_fork_cache::cache::EvmCache) {
1752 /// let token = Address::repeat_byte(0x01);
1753 /// let slot = U256::from(0);
1754 /// // Saturating +100, but only if the slot is already hot.
1755 /// let change = cache.modify_slot(token, slot, |cur| cur.map(|v| v.saturating_add(U256::from(100))));
1756 /// # let _ = change;
1757 /// # }
1758 /// ```
1759 pub fn modify_slot(
1760 &mut self,
1761 address: Address,
1762 slot: U256,
1763 f: impl FnOnce(Option<U256>) -> Option<U256>,
1764 ) -> Option<SlotChange> {
1765 let current = self.cached_storage_value(address, slot);
1766 let new = f(current)?;
1767
1768 self.write_slot_through(address, slot, new);
1769
1770 let old = current.unwrap_or(U256::ZERO);
1771 (old != new).then_some(SlotChange {
1772 address,
1773 slot,
1774 old,
1775 new,
1776 })
1777 }
1778
1779 /// Read-modify-write an account's native balance through a caller-supplied
1780 /// transform.
1781 ///
1782 /// The closure analog of [`StateUpdate::BalanceDelta`] (the data-level form
1783 /// flows through [`apply_update`](Self::apply_update); this is for arbitrary
1784 /// transforms). `f` is called with the account's current native balance
1785 /// (overlay ▸ backend ▸ `None` when the account is absent from **both**
1786 /// layers) and decides the new balance:
1787 ///
1788 /// - `Some(new)` writes `new` through both layers — backend always, overlay
1789 /// only if an overlay account already exists — preserving the account's
1790 /// nonce and code, and returns an [`AccountChange`] (balance only) iff the
1791 /// balance changed;
1792 /// - `None` writes nothing (no account is materialized) and returns `None`.
1793 ///
1794 /// "Cold" for a balance is the account being absent from both layers — or
1795 /// present in the overlay as revm `NotExisting` (absent to the EVM), which the
1796 /// internal account read also treats as cold, mirroring `DbAccount::info()`.
1797 /// To skip cold accounts, map through the `Option`:
1798 /// `|cur| cur.map(|v| v.saturating_add(amount))`.
1799 ///
1800 /// ```no_run
1801 /// # use alloy_primitives::{Address, U256};
1802 /// # fn example(cache: &mut evm_fork_cache::cache::EvmCache) {
1803 /// let acct = Address::repeat_byte(0x01);
1804 /// // Saturating +100, but only if the account's balance is already known.
1805 /// let change = cache.modify_account_balance(acct, |cur| cur.map(|v| v.saturating_add(U256::from(100))));
1806 /// # let _ = change;
1807 /// # }
1808 /// ```
1809 pub fn modify_account_balance(
1810 &mut self,
1811 address: Address,
1812 f: impl FnOnce(Option<U256>) -> Option<U256>,
1813 ) -> Option<AccountChange> {
1814 // Load the full info from the cached layers only (overlay ▸ backend); the
1815 // account is "cold" when absent from both.
1816 let base = self.loaded_account_info(address);
1817 let current_balance = base.as_ref().map(|info| info.balance);
1818 let new_balance = f(current_balance)?;
1819
1820 // The closure asked to write `new_balance`. Materialize from the loaded
1821 // base (or a default if the caller chose to write a cold account).
1822 let mut info = base.unwrap_or_default();
1823 let old_balance = info.balance;
1824 info.balance = new_balance;
1825 self.write_account_info_through(address, info);
1826
1827 (old_balance != new_balance).then_some(AccountChange {
1828 address,
1829 balance: Some((old_balance, new_balance)),
1830 nonce: None,
1831 code_hash: None,
1832 })
1833 }
1834
1835 /// Apply a relative (saturating) [`SlotDelta`] to an account's native balance
1836 /// (§16.5). Cold-aware:
1837 ///
1838 /// - `Ok(Some(change))` — present account, balance changed;
1839 /// - `Ok(None)` — present account, balance unchanged (e.g. `Sub` from 0);
1840 /// - `Err(skipped)` — cold account (absent from both layers): nothing applied,
1841 /// nothing materialized.
1842 fn apply_balance_delta(
1843 &mut self,
1844 address: Address,
1845 delta: SlotDelta,
1846 ) -> std::result::Result<Option<AccountChange>, SkippedBalanceDelta> {
1847 let Some(mut info) = self.loaded_account_info(address) else {
1848 // Cold: applying a delta against an unknown balance would corrupt it,
1849 // and materializing a default account would mask the real on-chain one.
1850 return Err(SkippedBalanceDelta { address, delta });
1851 };
1852
1853 let old_balance = info.balance;
1854 let new_balance = delta.apply(old_balance);
1855 info.balance = new_balance;
1856 self.write_account_info_through(address, info);
1857
1858 Ok((old_balance != new_balance).then_some(AccountChange {
1859 address,
1860 balance: Some((old_balance, new_balance)),
1861 nonce: None,
1862 code_hash: None,
1863 }))
1864 }
1865
1866 /// Load an account's `AccountInfo` from the cached layers only (overlay ▸
1867 /// backend), without touching RPC. `None` when the account is absent from
1868 /// both layers.
1869 fn loaded_account_info(&self, address: Address) -> Option<AccountInfo> {
1870 let mut info = if let Some(a) = self.db.cache.accounts.get(&address) {
1871 // Mirror revm `DbAccount::info()` / `basic_ref`: a NotExisting overlay
1872 // account is absent to the EVM (returns None) and does NOT fall through
1873 // to the backend. Without this, a relative balance update / partial
1874 // patch would compute against a stale `info` the EVM never sees.
1875 if matches!(a.account_state, AccountState::NotExisting) {
1876 return None;
1877 }
1878 a.info.clone()
1879 } else {
1880 self.blockchain_db
1881 .accounts()
1882 .read()
1883 .get(&address)
1884 .cloned()?
1885 };
1886 // Normalize like revm `insert_contract`: a ZERO code_hash denotes empty
1887 // code -> KECCAK_EMPTY. Done at load time so a patch's `old_code_hash`
1888 // matches what `write_account_info_through` stores (a self-consistent diff,
1889 // no phantom/under-reported code_hash change).
1890 if info.code_hash == B256::ZERO {
1891 info.code_hash = revm::primitives::KECCAK_EMPTY;
1892 }
1893 Some(info)
1894 }
1895
1896 /// Write an `AccountInfo` through both layers, mirroring the slot policy:
1897 /// backend (layer 2) always; overlay (layer 1) only if an overlay account
1898 /// already exists (never materialize a new overlay account).
1899 fn write_account_info_through(&mut self, address: Address, mut info: AccountInfo) {
1900 // Normalize the code hash the way revm's `insert_contract` (applied on the
1901 // overlay write below) does, so both layers store an identical hash: a ZERO
1902 // code_hash denotes empty code → KECCAK_EMPTY. Otherwise the overlay would
1903 // hold KECCAK_EMPTY while the backend kept ZERO for the same account.
1904 if info.code_hash == B256::ZERO {
1905 info.code_hash = revm::primitives::KECCAK_EMPTY;
1906 }
1907 let overlay_present = self.db.cache.accounts.contains_key(&address);
1908 {
1909 let mut accounts = self.blockchain_db.accounts().write();
1910 accounts.insert(address, info.clone());
1911 }
1912 if overlay_present {
1913 self.db.insert_account_info(address, info);
1914 }
1915 // Layer-2 account info changed → invalidate the memoized base for this
1916 // address (D2: over-invalidation when also in layer 1 is safe).
1917 self.mark_base_dirty(address);
1918 }
1919
1920 /// Apply a partial [`AccountPatch`] write-through (§5.2). Returns an
1921 /// [`AccountChange`] iff any field actually changes.
1922 fn apply_account_patch(
1923 &mut self,
1924 address: Address,
1925 patch: &AccountPatch,
1926 allow_cold_upsert: bool,
1927 ) -> std::result::Result<Option<AccountChange>, SkippedAccountPatch> {
1928 // 1. Current info from the cached layers only (overlay ▸ backend). No RPC:
1929 // apply is a write, not a fetch. A partial patch on a cold account is
1930 // skipped unless the caller explicitly chose AccountUpsert.
1931 let mut info = match self.loaded_account_info(address) {
1932 Some(info) => info,
1933 None if account_patch_is_empty(patch) => return Ok(None),
1934 None if allow_cold_upsert => AccountInfo::default(),
1935 None => {
1936 return Err(SkippedAccountPatch {
1937 address,
1938 patch: patch.clone(),
1939 });
1940 }
1941 };
1942
1943 let old_balance = info.balance;
1944 let old_nonce = info.nonce;
1945 let old_code_hash = info.code_hash;
1946
1947 // 2. Apply each `Some` field.
1948 if let Some(balance) = patch.balance {
1949 info.balance = balance;
1950 }
1951 if let Some(nonce) = patch.nonce {
1952 info.nonce = nonce;
1953 }
1954 if let Some(code) = &patch.code {
1955 let bytecode = Bytecode::new_raw(code.clone());
1956 info.code_hash = bytecode.hash_slow();
1957 info.code = Some(bytecode);
1958 }
1959
1960 // 3. Compute the change first. A no-op patch (every field equals the
1961 // loaded base) must NOT write either layer — otherwise an all-`None`
1962 // patch on an absent address would insert `AccountInfo::default()` into
1963 // the shared backend (masking a future RPC fetch) while returning an
1964 // empty diff. Only a real field change materializes anything.
1965 let change = AccountChange {
1966 address,
1967 balance: (old_balance != info.balance).then_some((old_balance, info.balance)),
1968 nonce: (old_nonce != info.nonce).then_some((old_nonce, info.nonce)),
1969 code_hash: (old_code_hash != info.code_hash).then_some((old_code_hash, info.code_hash)),
1970 };
1971 if change.balance.is_none() && change.nonce.is_none() && change.code_hash.is_none() {
1972 return Ok(None);
1973 }
1974
1975 // 4. Write-through, mirroring the slot policy: backend always; overlay
1976 // only if an overlay account already exists (do not materialize one).
1977 self.write_account_info_through(address, info);
1978
1979 Ok(Some(change))
1980 }
1981
1982 /// Dispatch a [`PurgeScope`] to the matching layer logic (§5.3), returning a
1983 /// [`PurgeRecord`] of what was removed from each layer.
1984 fn apply_purge(&mut self, address: Address, scope: &PurgeScope) -> PurgeRecord {
1985 match scope {
1986 PurgeScope::Account => {
1987 let (slots_removed, account_removed) = self.purge_account_inner(address);
1988 PurgeRecord {
1989 address,
1990 scope: PurgeScope::Account,
1991 slots_removed,
1992 account_removed,
1993 }
1994 }
1995 PurgeScope::AllStorage => {
1996 let slots_removed = self.purge_contract_storage_inner(address);
1997 PurgeRecord {
1998 address,
1999 scope: PurgeScope::AllStorage,
2000 slots_removed,
2001 account_removed: false,
2002 }
2003 }
2004 PurgeScope::Slots(slots) => {
2005 let slots_removed = self.purge_contract_slots_inner(address, slots);
2006 PurgeRecord {
2007 address,
2008 scope: PurgeScope::Slots(slots.clone()),
2009 slots_removed,
2010 account_removed: false,
2011 }
2012 }
2013 }
2014 }
2015
2016 /// Set (or replace) the batch storage fetcher.
2017 ///
2018 /// This is the seam the freshness controller and tests use to drive
2019 /// re-verification without a live provider: a stubbed
2020 /// [`StorageBatchFetchFn`] can be injected over a mocked-provider cache.
2021 pub fn set_storage_batch_fetcher(&mut self, f: StorageBatchFetchFn) {
2022 self.storage_batch_fetcher = Some(f);
2023 }
2024
2025 /// Return the currently-cached value for a storage slot, if any.
2026 ///
2027 /// Mirrors what the EVM would `SLOAD` from the cached layers (it never touches
2028 /// RPC, unlike [`read_storage_slot`](Self::read_storage_slot)):
2029 ///
2030 /// 1. The CacheDB overlay (layer 1) wins: if the overlay account holds the
2031 /// slot, return it.
2032 /// 2. Match revm's `CacheDB::storage_ref`: if the overlay account exists but
2033 /// does **not** hold the slot, and its `account_state` is `StorageCleared`
2034 /// or `NotExisting`, the live EVM reads the slot as ZERO and never consults
2035 /// the backend — so return `Some(U256::ZERO)`, **not** the (shadowed)
2036 /// backend value. Returning the backend value here would let a
2037 /// `SlotDelta`/`modify_slot` compute a delta against a base the EVM never
2038 /// sees (silent corruption) and would mis-record `apply_slot`'s `old`.
2039 /// 3. Otherwise fall through to the BlockchainDb backend (layer 2); `None` when
2040 /// neither layer has seen the slot.
2041 pub fn cached_storage_value(&self, address: Address, slot: U256) -> Option<U256> {
2042 if let Some(db_account) = self.db.cache.accounts.get(&address) {
2043 if let Some(value) = db_account.storage.get(&slot) {
2044 return Some(*value);
2045 }
2046 // A StorageCleared / NotExisting overlay account reads a missing slot
2047 // as ZERO and never consults the backend (matching the EVM SLOAD).
2048 if matches!(
2049 db_account.account_state,
2050 AccountState::StorageCleared | AccountState::NotExisting
2051 ) {
2052 return Some(U256::ZERO);
2053 }
2054 }
2055 let storage = self.blockchain_db.storage().read();
2056 storage.get(&address).and_then(|s| s.get(&slot).copied())
2057 }
2058
2059 /// Re-fetch the given slots via the batch fetcher, compare to the currently
2060 /// cached values, and inject the ones that changed.
2061 ///
2062 /// For each slot whose freshly-fetched value differs from the cached value,
2063 /// the fresh value is written into the cache via
2064 /// [`inject_storage_batch_fresh`](Self::inject_storage_batch_fresh) and a
2065 /// [`SlotChange`] is recorded. Slots that are unchanged, or that the fetcher
2066 /// fails to return, are left as-is. Returns the set of changed slots.
2067 ///
2068 /// Requires a batch fetcher (set at construction or via
2069 /// [`set_storage_batch_fetcher`](Self::set_storage_batch_fetcher)); errors if
2070 /// none is available. This is the synchronous main-thread primitive; the
2071 /// background validator performs the equivalent comparison against a snapshot.
2072 pub fn verify_slots(&mut self, slots: &[(Address, U256)]) -> Result<Vec<SlotChange>> {
2073 Ok(self.verify_slots_inner(slots)?.0)
2074 }
2075
2076 /// Shared implementation for [`verify_slots`](Self::verify_slots) and the
2077 /// pipeline's reconcile path. Returns `(changed, fetched_ok)` where
2078 /// `fetched_ok` is the number of requested slots the fetcher returned a value
2079 /// for (failed per-slot fetches are skipped, not errors). Errors only when no
2080 /// batch fetcher is configured.
2081 fn verify_slots_inner(
2082 &mut self,
2083 slots: &[(Address, U256)],
2084 ) -> Result<(Vec<SlotChange>, usize)> {
2085 let (changed, outcomes) = self.verify_slots_core(slots)?;
2086 let fetched_ok = outcomes
2087 .iter()
2088 .filter(|o| matches!(o.fetch, SlotFetch::Value(_) | SlotFetch::Zero))
2089 .count();
2090 Ok((changed, fetched_ok))
2091 }
2092
2093 /// Classify a single fetched slot value into a [`SlotFetch`].
2094 ///
2095 /// This is purely the *fetch* classification (`Value` / `Zero` /
2096 /// `FetchFailed`); it is independent of change detection, which compares the
2097 /// fetched value to the cached baseline separately. A non-zero `Ok` is
2098 /// [`SlotFetch::Value`], a genuine `Ok(0)` is [`SlotFetch::Zero`], and an
2099 /// `Err` is [`SlotFetch::FetchFailed`] carrying the error string.
2100 ///
2101 /// Shared with the cold-start probe phase
2102 /// ([`execute_cold_start_round`](Self::execute_cold_start_round)) so the
2103 /// single classification is reused rather than duplicated.
2104 pub(crate) fn classify(fetched: Result<U256>) -> SlotFetch {
2105 match fetched {
2106 Ok(v) if v != U256::ZERO => SlotFetch::Value(v),
2107 Ok(_) => SlotFetch::Zero,
2108 Err(e) => SlotFetch::FetchFailed {
2109 reason: e.to_string(),
2110 },
2111 }
2112 }
2113
2114 /// Core slot-verification loop shared by [`verify_slots_inner`](Self::verify_slots_inner)
2115 /// and [`verify_slots_with_outcomes`](Self::verify_slots_with_outcomes).
2116 ///
2117 /// Fetches every slot via the batch fetcher and, for each slot, performs two
2118 /// **independent** reads of the same fetched value:
2119 ///
2120 /// 1. *Fetch classification* — every slot (including failed ones) produces one
2121 /// [`SlotOutcome`] via [`classify`](Self::classify): `Value` / `Zero` /
2122 /// `FetchFailed`.
2123 /// 2. *Change detection* — a successfully-fetched value that differs from the
2124 /// cached baseline (`old`, defaulting to `ZERO` for an unseen slot) is
2125 /// injected via [`inject_storage_batch_fresh`](Self::inject_storage_batch_fresh)
2126 /// and recorded as a [`SlotChange`].
2127 ///
2128 /// These two reads are deliberately not collapsed: a genuine `Ok(0)` on a slot
2129 /// whose cached value was also `0` yields [`SlotFetch::Zero`] **and** no
2130 /// `SlotChange`. The returned `outcomes` vec has exactly one entry per
2131 /// requested slot. An empty `slots` input short-circuits to empty results
2132 /// without requiring a fetcher; otherwise a missing fetcher is an error.
2133 fn verify_slots_core(
2134 &mut self,
2135 slots: &[(Address, U256)],
2136 ) -> Result<(Vec<SlotChange>, Vec<SlotOutcome>)> {
2137 if slots.is_empty() {
2138 return Ok((Vec::new(), Vec::new()));
2139 }
2140 let fetcher = self
2141 .storage_batch_fetcher
2142 .as_ref()
2143 .ok_or_else(|| anyhow!("verify_slots requires a storage batch fetcher"))?
2144 .clone();
2145
2146 // Snapshot the cached values before fetching so we compare against a
2147 // stable baseline.
2148 let cached: HashMap<(Address, U256), Option<U256>> = slots
2149 .iter()
2150 .map(|&(addr, slot)| ((addr, slot), self.cached_storage_value(addr, slot)))
2151 .collect();
2152
2153 let results = (fetcher)(slots.to_vec(), Some(self.block));
2154
2155 let mut changed = Vec::new();
2156 let mut outcomes = Vec::with_capacity(results.len());
2157 let mut to_inject = Vec::new();
2158 for (addr, slot, fetched) in results {
2159 // Read 1: classify the fetch outcome for every slot, failed or not.
2160 let fetch = Self::classify(match &fetched {
2161 Ok(v) => Ok(*v),
2162 Err(e) => Err(anyhow!("{e}")),
2163 });
2164 outcomes.push(SlotOutcome {
2165 address: addr,
2166 slot,
2167 fetch,
2168 });
2169
2170 // Read 2: change detection, independent of the classification above.
2171 let fresh = match fetched {
2172 Ok(value) => value,
2173 Err(e) => {
2174 debug!(%addr, %slot, error = %e, "verify_slots: fetch failed, skipping slot");
2175 continue;
2176 }
2177 };
2178 // A slot the cache never saw is treated as old = ZERO (the value a
2179 // sim would have read), so a non-zero fresh value counts as a change.
2180 let old = cached
2181 .get(&(addr, slot))
2182 .copied()
2183 .flatten()
2184 .unwrap_or(U256::ZERO);
2185 if fresh != old {
2186 to_inject.push((addr, slot, fresh));
2187 changed.push(SlotChange {
2188 address: addr,
2189 slot,
2190 old,
2191 new: fresh,
2192 });
2193 }
2194 }
2195
2196 if !to_inject.is_empty() {
2197 self.inject_storage_batch_fresh(&to_inject);
2198 }
2199 Ok((changed, outcomes))
2200 }
2201
2202 /// Like [`verify_slots`](Self::verify_slots), but additionally returns one
2203 /// [`SlotOutcome`] per requested slot (including slots the fetcher failed to
2204 /// return), classified as `Value` / `Zero` / `FetchFailed`.
2205 ///
2206 /// This is the per-slot surface the cold-start driver consumes: it
2207 /// distinguishes a genuine on-chain zero from a fetch failure for every slot,
2208 /// closing the archive-miss gap. It is a pure alias of
2209 /// [`verify_slots_core`](Self::verify_slots_core) and shares its injection
2210 /// behaviour with [`verify_slots`](Self::verify_slots).
2211 #[cfg(feature = "reactive")]
2212 pub(crate) fn verify_slots_with_outcomes(
2213 &mut self,
2214 slots: &[(Address, U256)],
2215 ) -> Result<(Vec<SlotChange>, Vec<SlotOutcome>)> {
2216 self.verify_slots_core(slots)
2217 }
2218
2219 /// Reconciliation re-read used by [`EventPipeline::reconcile`](crate::events::EventPipeline::reconcile).
2220 ///
2221 /// Like [`verify_slots`](Self::verify_slots) it fetches the requested slots,
2222 /// injects the ones that changed, and returns the changed set — but it is
2223 /// **honest about reachability**: it errors not only when no batch fetcher is
2224 /// configured, but also when a non-empty request could not fetch **any** slot
2225 /// (a total fetch failure — e.g. the default RPC fetcher invoked with no usable
2226 /// runtime, or an unreachable provider). Reconciliation that silently "verified
2227 /// nothing" would be a false all-clear, so it surfaces as an error for the
2228 /// caller to retry. A partially-successful fetch returns `Ok` with whatever
2229 /// changed.
2230 pub fn reconcile_slots(&mut self, slots: &[(Address, U256)]) -> Result<Vec<SlotChange>> {
2231 let (changed, fetched_ok) = self.verify_slots_inner(slots)?;
2232 if !slots.is_empty() && fetched_ok == 0 {
2233 return Err(anyhow!(
2234 "reconcile could not fetch any of the {} requested slot(s) \
2235 (no usable storage fetcher / provider unreachable)",
2236 slots.len()
2237 ));
2238 }
2239 Ok(changed)
2240 }
2241
2242 /// Purge an account fully from both cache layers: its `AccountInfo`
2243 /// (balance/nonce/code hash) **and** all of its storage.
2244 ///
2245 /// Removes `addr` from the CacheDB overlay accounts map, the BlockchainDb
2246 /// accounts map, and the BlockchainDb storage map, so the next access
2247 /// re-fetches a clean account from RPC. This is the account-level
2248 /// counterpart to the storage-only [`purge_contract_storage`](Self::purge_contract_storage):
2249 /// use it when an address is fully volatile (no pinned slots) and even its
2250 /// balance/nonce/code can no longer be trusted.
2251 pub fn purge_account(&mut self, addr: Address) {
2252 // Thin wrapper over the unified purge primitive; the layer logic lives in
2253 // `purge_account_inner` (shared with `apply_update(Purge { Account })`).
2254 let _ = self.apply_update(&StateUpdate::purge(addr, PurgeScope::Account));
2255 }
2256
2257 /// Account-scope purge layer logic. Removes `addr` from the overlay accounts
2258 /// map, the backend accounts map, and the backend storage map. Returns
2259 /// `(backend_slots_removed, account_removed)` where `account_removed` is true
2260 /// if an account entry was removed from either account layer.
2261 fn purge_account_inner(&mut self, addr: Address) -> (usize, bool) {
2262 // Layer 1: CacheDB overlay (accounts + their storage live together).
2263 let overlay_removed = self.db.cache.accounts.remove(&addr).is_some();
2264
2265 // Layer 2: BlockchainDb accounts + storage maps.
2266 let backend_account_removed = self
2267 .blockchain_db
2268 .accounts()
2269 .write()
2270 .remove(&addr)
2271 .is_some();
2272 let backend_storage_removed = self.blockchain_db.storage().write().remove(&addr);
2273 let slots_removed = backend_storage_removed
2274 .map(|slots| slots.len())
2275 .unwrap_or(0);
2276
2277 let account_removed = overlay_removed || backend_account_removed;
2278 if account_removed || slots_removed > 0 {
2279 debug!(
2280 account = %addr,
2281 overlay_removed,
2282 backend_account_removed,
2283 backend_storage_slots = slots_removed,
2284 "purged account from both cache layers"
2285 );
2286 }
2287 // Layer 2 (account + storage) changed for this address → invalidate base.
2288 self.mark_base_dirty(addr);
2289 (slots_removed, account_removed)
2290 }
2291
2292 /// Get the chain ID used for EVM simulations (the `CHAINID` opcode).
2293 pub fn chain_id(&self) -> u64 {
2294 self.chain_id
2295 }
2296
2297 /// Set the chain ID reported to simulations via the `CHAINID` opcode.
2298 ///
2299 /// Prefer setting this at construction through
2300 /// [`EvmCacheBuilder::chain_id`]. This setter exists for cases where the
2301 /// chain ID must change after construction. It takes effect on the next
2302 /// [`create_snapshot`](Self::create_snapshot) / `build_evm`; existing
2303 /// snapshots and overlays keep the chain ID captured when they were created.
2304 pub fn set_chain_id(&mut self, chain_id: u64) {
2305 self.chain_id = chain_id;
2306 }
2307
2308 /// Take a low-level, same-thread snapshot of the CacheDB overlay for
2309 /// in-place restore.
2310 ///
2311 /// Clones the inner [`revm::database::Cache`] (the layer-1 overlay's
2312 /// accounts and storage) only — not the underlying database wrapper or the
2313 /// BlockchainDb backend. Pair with [`restore`](Self::restore) to roll the
2314 /// overlay back on the same `EvmCache` after speculative mutations (this is
2315 /// how the balance-slot scan probes and rewinds).
2316 ///
2317 /// For cross-thread fan-out use [`create_snapshot`](Self::create_snapshot)
2318 /// instead: it merges both layers into an `Arc<`[`EvmSnapshot`]`>` that is
2319 /// `Send + Sync` and can be shared with parallel simulators via
2320 /// [`EvmOverlay`].
2321 pub fn snapshot(&self) -> revm::database::Cache {
2322 self.db.cache.clone()
2323 }
2324
2325 /// Restore the CacheDB overlay from a snapshot taken with
2326 /// [`snapshot`](Self::snapshot).
2327 ///
2328 /// Overwrites the layer-1 overlay wholesale with `snapshot`, discarding any
2329 /// overlay mutations made since it was taken. The BlockchainDb backend is
2330 /// untouched. This is the in-place counterpart to the cross-thread
2331 /// [`create_snapshot`](Self::create_snapshot) / [`EvmOverlay`] path.
2332 pub fn restore(&mut self, snapshot: revm::database::Cache) {
2333 self.db.cache = snapshot;
2334 }
2335
2336 /// Create a new session for executing multiple operations.
2337 ///
2338 /// Changes made within the session are only committed to the underlying database
2339 /// when `session.commit()` is called. Dropping the session without calling commit
2340 /// discards all changes made during the session.
2341 pub fn session(&mut self) -> EvmSession<'_> {
2342 EvmSession {
2343 evm: self.build_evm(),
2344 }
2345 }
2346
2347 /// Create an immutable, `Send + Sync` snapshot of the current EVM state for
2348 /// cross-thread fan-out (the copy-on-write two-tier view, Pillar A).
2349 ///
2350 /// Rather than deep-copying both layers, this memoizes the cold layer-2
2351 /// (`BlockchainDb`) index as an `Arc`-shared base — reused as a cheap
2352 /// `Arc::clone` when layer 2 is unchanged, rebuilt copy-on-write only for the
2353 /// addresses that changed — and folds the hot layer-1 (`CacheDB` overlay)
2354 /// delta over it. Layer-1 values shadow the base on reads, reproducing the
2355 /// live cache's layered semantics; the resulting [`EvmSnapshot`] is shared
2356 /// across threads via `Arc`. Its cost tracks *changed* state, not *total*
2357 /// state. (The retained [`create_snapshot_deep_clone`](Self::create_snapshot_deep_clone)
2358 /// is the read-equivalent O(total) reference, kept for benchmarking/testing.)
2359 ///
2360 /// Takes `&mut self` because it refreshes and memoizes the base. For cheap
2361 /// same-thread save/restore of just the overlay, prefer
2362 /// [`snapshot`](Self::snapshot) / [`restore`](Self::restore) instead.
2363 pub fn create_snapshot(&mut self) -> Arc<snapshot::EvmSnapshot> {
2364 // 1. Refresh / memoize the cold layer-2 base, then take a cheap Arc handle
2365 // (O(1) when layer 2 is unchanged since the last snapshot).
2366 self.refresh_base();
2367 let base = Arc::clone(self.base.as_ref().expect("refresh_base sets base"));
2368
2369 // 2. Fold layer 1 (the hot CacheDB overlay) into the snapshot's overlay
2370 // maps + cleared/not-existing sets, applying the same classification as
2371 // the legacy flatten (O(layer-1)).
2372 let mut overlay_accounts = HashMap::new();
2373 let mut overlay_storage = HashMap::new();
2374 let mut overlay_code_by_hash = HashMap::new();
2375 let mut storage_cleared = std::collections::HashSet::new();
2376 let mut accounts_not_existing = std::collections::HashSet::new();
2377 for (addr, db_account) in &self.db.cache.accounts {
2378 let not_existing = matches!(db_account.account_state, AccountState::NotExisting);
2379 let cleared =
2380 not_existing || matches!(db_account.account_state, AccountState::StorageCleared);
2381
2382 // Account info. Mirror revm `DbAccount::info()` / `loaded_account_info`:
2383 // a NotExisting overlay account is absent to the EVM (`basic` returns
2384 // None), so it must NOT contribute info/code to the overlay — and
2385 // `accounts_not_existing` makes the read short-circuit to None before
2386 // ever consulting the base.
2387 if not_existing {
2388 accounts_not_existing.insert(*addr);
2389 } else {
2390 if let Some(code) = &db_account.info.code {
2391 overlay_code_by_hash.insert(db_account.info.code_hash, code.clone());
2392 }
2393 overlay_accounts.insert(*addr, db_account.info.clone());
2394 }
2395
2396 // Storage. A StorageCleared/NotExisting account's storage is locally
2397 // complete: the overlay holds ONLY its own slots (so a cleared account
2398 // ALWAYS gets an `overlay_storage` entry, possibly empty), an absent
2399 // slot reads ZERO via `storage_cleared`, and the base is never consulted
2400 // for it. A non-cleared overlay account contributes its slots; absent
2401 // slots fall through to the base on a read.
2402 if cleared {
2403 storage_cleared.insert(*addr);
2404 let account_storage: HashMap<U256, U256> =
2405 db_account.storage.iter().map(|(k, v)| (*k, *v)).collect();
2406 overlay_storage.insert(*addr, account_storage);
2407 } else if !db_account.storage.is_empty() {
2408 let account_storage = overlay_storage.entry(*addr).or_default();
2409 for (slot, value) in &db_account.storage {
2410 account_storage.insert(*slot, *value);
2411 }
2412 }
2413 }
2414
2415 Arc::new(snapshot::EvmSnapshot {
2416 base,
2417 overlay_accounts,
2418 overlay_storage,
2419 overlay_code_by_hash,
2420 storage_cleared,
2421 accounts_not_existing,
2422 block_hashes: HashMap::new(),
2423 block_number: self.block_number,
2424 basefee: self.basefee,
2425 coinbase: self.coinbase,
2426 prevrandao: self.prevrandao,
2427 gas_limit: self.block_gas_limit,
2428 chain_id: self.chain_id,
2429 timestamp: self.timestamp_override,
2430 spec_id: self.spec_id,
2431 shared_memory_capacity: self.shared_memory_capacity,
2432 })
2433 }
2434
2435 /// Force the next [`create_snapshot`](Self::create_snapshot) to rebuild the
2436 /// memoized copy-on-write base from scratch (Pillar A).
2437 ///
2438 /// The crate's own mutators keep the base honest automatically. This is the
2439 /// **escape-hatch re-honest hook**: call it after writing layer 2 directly
2440 /// through [`unchecked_blockchain_db`](Self::unchecked_blockchain_db) or
2441 /// [`unchecked_backend`](Self::unchecked_backend) — those bypass the write
2442 /// funnel, and in-place changes at unchanged cardinality are invisible to the
2443 /// snapshot growth scan.
2444 /// That includes overwriting an existing storage slot and changing an existing
2445 /// account's info/code/balance without adding a new account. Lazy RPC-populated
2446 /// data does not need this call because it only appends accounts/slots, which
2447 /// the growth scan catches.
2448 ///
2449 /// When using `SharedBackend::insert_or_update_*` through
2450 /// [`unchecked_backend`](Self::unchecked_backend), remember those helpers only
2451 /// enqueue a background update. Synchronize/read back the update through
2452 /// `SharedBackend` before the next snapshot; `invalidate_snapshot_base` alone
2453 /// is not a backend-handler synchronization point. Once the direct write is
2454 /// present, calling this before the next snapshot guarantees it reflects that
2455 /// write rather than a stale memoized value. Over-invalidation is always safe
2456 /// (Decision D2); the only cost is one full base rebuild on the next snapshot.
2457 pub fn invalidate_snapshot_base(&mut self) {
2458 self.invalidate_base();
2459 }
2460
2461 /// Refresh the memoized cold layer-2 [`BaseState`](snapshot::BaseState),
2462 /// reusing the previous `Arc` wherever layer 2 is unchanged (Pillar A).
2463 ///
2464 /// Called at the top of [`create_snapshot`](Self::create_snapshot). It never
2465 /// mutates an `Arc<BaseState>` that may already be shared with a live
2466 /// snapshot: on any change it builds a *new* `BaseState` that shares the `Arc`
2467 /// handles of unchanged accounts and rebuilds only the changed ones
2468 /// (copy-on-write).
2469 ///
2470 /// Algorithm (see `docs/phase-5-spec.md` §2.3):
2471 /// 1. **Full rebuild** when there is no base yet or `base_full_rebuild` is set
2472 /// (`set_block` / re-pin replaced layer 2): flatten all of layer 2.
2473 /// 2. **Detect uncontrolled growth**: a lazy RPC fetch / prefetch can write
2474 /// layer 2 from inside `foundry-fork-db`, bypassing our write funnel. An
2475 /// `O(accounts)` length-scan over the current layer-2 storage/accounts marks
2476 /// any address whose slot count differs from the recorded length, or any
2477 /// account absent from the base, as dirty.
2478 /// 3. **Nothing dirty** → reuse the existing `Arc<BaseState>` unchanged (the
2479 /// common hot-loop case; the base side of `create_snapshot` is then O(1)).
2480 /// 4. **Some addresses dirty** → build a new `BaseState` sharing the `Arc`s of
2481 /// unchanged accounts and rebuilding only the dirty ones.
2482 fn refresh_base(&mut self) {
2483 // Case 1: full rebuild.
2484 if self.base.is_none() || self.base_full_rebuild {
2485 self.base = Some(Arc::new(self.build_base_full()));
2486 self.base_dirty.clear();
2487 self.base_full_rebuild = false;
2488 return;
2489 }
2490
2491 // Case 2: detect uncontrolled layer-2 growth via an O(accounts) length scan
2492 // (NOT an O(slots) value scan). Any address whose slot count changed, or any
2493 // account that newly appeared in layer 2, is folded into `base_dirty`.
2494 //
2495 // LOAD-BEARING INVARIANT: the count/absence scan is sufficient *only* because
2496 // the one uncontrolled layer-2 writer — the foundry-fork-db `SharedBackend`
2497 // lazy fetch — is append-only at a fixed block (its request handler answers an
2498 // already-cached account/slot from the store and only inserts on a miss; it
2499 // never overwrites an existing entry in place). So an uncontrolled fetch can
2500 // only add a new account (caught by the absence check) or a new slot (caught
2501 // by the count check). An in-place value overwrite at unchanged length is
2502 // invisible here; the controlled writers therefore call `mark_base_dirty`
2503 // explicitly, and a direct out-of-band write via `unchecked_blockchain_db()`/`unchecked_backend()`
2504 // must call `invalidate_snapshot_base`. If a future foundry-fork-db bump makes
2505 // the lazy path overwrite-in-place, this scan must gain a value/version check.
2506 {
2507 let db_storage = self.blockchain_db.storage().read();
2508 for (addr, slots) in db_storage.iter() {
2509 if self.base_storage_lens.get(addr).copied() != Some(slots.len()) {
2510 self.base_dirty.insert(*addr);
2511 }
2512 }
2513 let db_accounts = self.blockchain_db.accounts().read();
2514 let base = self.base.as_ref().expect("base present in case 2/3/4");
2515 for addr in db_accounts.keys() {
2516 if !base.accounts.contains_key(addr) {
2517 self.base_dirty.insert(*addr);
2518 }
2519 }
2520 }
2521
2522 // Case 3: nothing changed → reuse the existing Arc unchanged.
2523 if self.base_dirty.is_empty() {
2524 return;
2525 }
2526
2527 // Case 4: rebuild copy-on-write — clone the outer maps (Arc handles +
2528 // AccountInfo, no per-slot copy) and rebuild only the dirty addresses.
2529 let prev = self.base.as_ref().expect("base present in case 4");
2530 let mut accounts = prev.accounts.clone();
2531 let mut storage = prev.storage.clone();
2532
2533 let db_accounts = self.blockchain_db.accounts().read();
2534 let db_storage = self.blockchain_db.storage().read();
2535 for addr in self.base_dirty.iter().copied() {
2536 // Account info: refresh from the current layer-2 account, or drop it if
2537 // the account no longer exists in layer 2 (e.g. after a purge).
2538 match db_accounts.get(&addr) {
2539 Some(info) => {
2540 accounts.insert(addr, info.clone());
2541 }
2542 None => {
2543 accounts.remove(&addr);
2544 }
2545 }
2546
2547 // Storage: rebuild this account's Arc<HashMap> from the current layer-2
2548 // storage, or drop it if the account has no layer-2 storage anymore.
2549 match db_storage.get(&addr) {
2550 Some(slots) => {
2551 let rebuilt: HashMap<U256, U256> =
2552 slots.iter().map(|(k, v)| (*k, *v)).collect();
2553 self.base_storage_lens.insert(addr, rebuilt.len());
2554 storage.insert(addr, Arc::new(rebuilt));
2555 }
2556 None => {
2557 storage.remove(&addr);
2558 self.base_storage_lens.remove(&addr);
2559 }
2560 }
2561 }
2562 drop(db_accounts);
2563 drop(db_storage);
2564
2565 // Rebuild the code index from the refreshed accounts (NOT cloned from the
2566 // previous base): a purged or recoded dirty account must not leave a stale
2567 // `code_by_hash` entry, which would diverge from `create_snapshot_deep_clone`
2568 // on a direct `code_by_hash(old_hash)` lookup. Rebuilding from scratch also
2569 // handles shared code hashes correctly (a hash survives iff some present
2570 // account still carries it).
2571 let code_by_hash = Self::code_index(&accounts);
2572
2573 self.base = Some(Arc::new(snapshot::BaseState {
2574 accounts,
2575 storage,
2576 code_by_hash,
2577 }));
2578 self.base_dirty.clear();
2579 }
2580
2581 /// Build the bytecode-by-hash index from a set of (layer-2) accounts, matching
2582 /// the deep-clone reference: a hash is present iff some account carries that
2583 /// code inline. Rebuilt from scratch on every base (re)build so a purged or
2584 /// recoded account never leaves a stale entry — preserving read-equivalence
2585 /// with [`create_snapshot_deep_clone`](Self::create_snapshot_deep_clone).
2586 fn code_index(accounts: &HashMap<Address, AccountInfo>) -> HashMap<B256, Bytecode> {
2587 accounts
2588 .values()
2589 .filter_map(|info| {
2590 info.code
2591 .as_ref()
2592 .map(|code| (info.code_hash, code.clone()))
2593 })
2594 .collect()
2595 }
2596
2597 /// Build a fresh [`BaseState`](snapshot::BaseState) by flattening all of layer
2598 /// 2, recording `base_storage_lens`. Shared by `refresh_base`'s full-rebuild
2599 /// path and [`create_snapshot_deep_clone`](Self::create_snapshot_deep_clone).
2600 fn build_base_full(&mut self) -> snapshot::BaseState {
2601 let mut accounts = HashMap::new();
2602 {
2603 let db_accounts = self.blockchain_db.accounts().read();
2604 for (addr, info) in db_accounts.iter() {
2605 accounts.insert(*addr, info.clone());
2606 }
2607 }
2608 let code_by_hash = Self::code_index(&accounts);
2609 let mut storage = HashMap::new();
2610 self.base_storage_lens.clear();
2611 {
2612 let db_storage = self.blockchain_db.storage().read();
2613 for (addr, slots) in db_storage.iter() {
2614 let converted: HashMap<U256, U256> = slots.iter().map(|(k, v)| (*k, *v)).collect();
2615 self.base_storage_lens.insert(*addr, converted.len());
2616 storage.insert(*addr, Arc::new(converted));
2617 }
2618 }
2619 snapshot::BaseState {
2620 accounts,
2621 storage,
2622 code_by_hash,
2623 }
2624 }
2625
2626 /// The retained deep-clone snapshot — today's full flatten, kept reachable for
2627 /// A/B benchmarking and as the read-equivalence reference (Decision D3).
2628 ///
2629 /// Produces the same two-tier [`EvmSnapshot`](snapshot::EvmSnapshot) shape as
2630 /// [`create_snapshot`](Self::create_snapshot), but with `base` set to the
2631 /// fully-merged flatten of **both** layers and **empty** overlay maps (the
2632 /// cleared / not-existing sets still in place). It is read-indistinguishable
2633 /// from `create_snapshot` by construction (the `tests/cow_snapshot.rs`
2634 /// differential gate pins this), at the cost of an O(total state) deep copy
2635 /// every call — exactly the cost `create_snapshot` now amortizes away.
2636 ///
2637 /// Stays `&self`: it does not touch the memoized base.
2638 #[doc(hidden)]
2639 pub fn create_snapshot_deep_clone(&self) -> Arc<snapshot::EvmSnapshot> {
2640 let mut accounts = HashMap::new();
2641 let mut storage: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
2642 let mut code_by_hash = HashMap::new();
2643
2644 // 1. Load from BlockchainDb (persistent cache / Layer 2).
2645 {
2646 let db_accounts = self.blockchain_db.accounts().read();
2647 for (addr, info) in db_accounts.iter() {
2648 if let Some(code) = &info.code {
2649 code_by_hash.insert(info.code_hash, code.clone());
2650 }
2651 accounts.insert(*addr, info.clone());
2652 }
2653 }
2654 {
2655 let db_storage = self.blockchain_db.storage().read();
2656 for (addr, slots) in db_storage.iter() {
2657 let converted: HashMap<U256, U256> = slots.iter().map(|(k, v)| (*k, *v)).collect();
2658 storage.insert(*addr, converted);
2659 }
2660 }
2661
2662 // 2. Overlay from CacheDB (Layer 1, takes precedence). Merge into the same
2663 // flat maps, dropping shadowed entries, exactly as the original
2664 // `create_snapshot` did. A cleared account's storage is routed into
2665 // `overlay_storage` (not the base), because `EvmSnapshot::storage_value`
2666 // only applies the cleared-as-ZERO rule for an address with an
2667 // `overlay_storage` entry — so the cleared semantics must be expressed
2668 // there for both snapshot constructors to read identically.
2669 let mut overlay_storage: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
2670 let mut storage_cleared = std::collections::HashSet::new();
2671 let mut accounts_not_existing = std::collections::HashSet::new();
2672 for (addr, db_account) in &self.db.cache.accounts {
2673 let not_existing = matches!(db_account.account_state, AccountState::NotExisting);
2674 let cleared =
2675 not_existing || matches!(db_account.account_state, AccountState::StorageCleared);
2676
2677 if not_existing {
2678 accounts_not_existing.insert(*addr);
2679 accounts.remove(addr);
2680 } else {
2681 if let Some(code) = &db_account.info.code {
2682 code_by_hash.insert(db_account.info.code_hash, code.clone());
2683 }
2684 accounts.insert(*addr, db_account.info.clone());
2685 }
2686
2687 if cleared {
2688 // Cleared: storage is locally complete. Drop any shadowed base
2689 // slots and keep ONLY the overlay slots, in `overlay_storage`.
2690 storage_cleared.insert(*addr);
2691 storage.remove(addr);
2692 let account_storage: HashMap<U256, U256> =
2693 db_account.storage.iter().map(|(k, v)| (*k, *v)).collect();
2694 overlay_storage.insert(*addr, account_storage);
2695 } else {
2696 // Non-cleared: overlay slots win over base; fold them into base.
2697 let account_storage = storage.entry(*addr).or_default();
2698 for (slot, value) in &db_account.storage {
2699 account_storage.insert(*slot, *value);
2700 }
2701 }
2702 }
2703
2704 let base = snapshot::BaseState {
2705 accounts,
2706 storage: storage
2707 .into_iter()
2708 .map(|(addr, slots)| (addr, Arc::new(slots)))
2709 .collect(),
2710 code_by_hash,
2711 };
2712
2713 Arc::new(snapshot::EvmSnapshot {
2714 base: Arc::new(base),
2715 overlay_accounts: HashMap::new(),
2716 overlay_storage,
2717 overlay_code_by_hash: HashMap::new(),
2718 storage_cleared,
2719 accounts_not_existing,
2720 block_hashes: HashMap::new(),
2721 block_number: self.block_number,
2722 basefee: self.basefee,
2723 coinbase: self.coinbase,
2724 prevrandao: self.prevrandao,
2725 gas_limit: self.block_gas_limit,
2726 chain_id: self.chain_id,
2727 timestamp: self.timestamp_override,
2728 spec_id: self.spec_id,
2729 shared_memory_capacity: self.shared_memory_capacity,
2730 })
2731 }
2732
2733 /// Mark a layer-2 address dirty so the next [`refresh_base`](Self::refresh_base)
2734 /// re-folds it into the memoized base (Pillar A invalidation; see
2735 /// `docs/phase-5-spec.md` §3).
2736 ///
2737 /// Called from every site that can change a layer-2 value a snapshot read
2738 /// would surface (write-through, batch injects, layer-2 seeding, purges).
2739 /// Over-invalidation is safe (Decision D2): marking an address that is also
2740 /// shadowed by layer 1 just re-folds that one account.
2741 fn mark_base_dirty(&mut self, address: Address) {
2742 self.base_dirty.insert(address);
2743 }
2744
2745 /// Force a full rebuild of the memoized base on the next
2746 /// [`refresh_base`](Self::refresh_base) (Pillar A invalidation).
2747 ///
2748 /// Used by layer-2 changes too broad to enumerate per-address efficiently
2749 /// (multi-contract / full-storage purges, block re-pins). Coarser than
2750 /// [`mark_base_dirty`](Self::mark_base_dirty) but always correct.
2751 fn invalidate_base(&mut self) {
2752 self.base_full_rebuild = true;
2753 }
2754
2755 /// Update the block that RPC fetches are pinned to.
2756 ///
2757 /// This re-pins the SharedBackend and the batch storage fetcher to `block`,
2758 /// so subsequent RPC fetches read state at the new block.
2759 ///
2760 /// # Block-context contract
2761 /// To prevent the EVM block context from silently diverging from the pinned
2762 /// block, when `block` is a concrete `BlockId::Number(Number(n))` this also
2763 /// updates `block_number` (the `NUMBER` opcode) to `n`. For tag-based block
2764 /// ids (`latest`, `pending`, hashes, etc.), the height is not
2765 /// statically known, so `block_number` is cleared.
2766 ///
2767 /// `basefee` (the `BASEFEE` opcode) is **cleared on every block change** and
2768 /// on every non-concrete tag/hash pin call because deriving it requires
2769 /// fetching the block header, which this synchronous method cannot do. Callers
2770 /// that change blocks should refresh it via
2771 /// [`set_block_context`](Self::set_block_context) after fetching the new
2772 /// header. Prefer [`repin_to_block`](Self::repin_to_block) when re-pinning to
2773 /// a concrete height, since it keeps `block_number` and the pinned block in
2774 /// lockstep.
2775 pub fn set_block(&mut self, block: BlockId) {
2776 let changed = self.block != block;
2777 let concrete_number = match block {
2778 BlockId::Number(BlockNumberOrTag::Number(n)) => Some(n),
2779 _ => None,
2780 };
2781 if changed {
2782 self.block = block;
2783 // Re-pinning replaces layer 2 wholesale (state at a new block): the
2784 // memoized base must be rebuilt from scratch on the next snapshot.
2785 self.invalidate_base();
2786 let _ = self.backend.set_pinned_block(block);
2787 *self.batch_block_id.lock().unwrap() = block;
2788 }
2789 if changed || concrete_number.is_none() {
2790 self.basefee = None;
2791 }
2792
2793 // Keep the EVM `NUMBER` opcode aligned with the pin. Only a concrete
2794 // height is meaningful; tags and hashes clear it so a stale number from
2795 // an earlier concrete block cannot leak into simulation.
2796 self.block_number = concrete_number;
2797 }
2798
2799 /// Get the block that RPC fetches are currently pinned to.
2800 pub fn block(&self) -> BlockId {
2801 self.block
2802 }
2803
2804 /// Set a custom timestamp for EVM simulations.
2805 ///
2806 /// When set, all EVM executions will use this timestamp instead of the current
2807 /// system time. This is useful for simulating future blocks to predict when
2808 /// time-dependent opportunities (like yield farming rewards) become profitable.
2809 ///
2810 /// Pass `None` to use the current system time (default behavior).
2811 pub fn set_timestamp(&mut self, timestamp: Option<u64>) {
2812 self.timestamp_override = timestamp;
2813 }
2814
2815 /// Get the current timestamp override, if any.
2816 ///
2817 /// Returns `None` if the cache is using the current system time (default).
2818 pub fn timestamp(&self) -> Option<u64> {
2819 self.timestamp_override
2820 }
2821
2822 /// Get the block number used for EVM simulations (the `NUMBER` opcode).
2823 ///
2824 /// Fetched from the pinned block's header at construction. Concrete-number
2825 /// pins set it via [`set_block`](Self::set_block) /
2826 /// [`repin_to_block`](Self::repin_to_block); tag/hash pins clear it
2827 /// because their height is not statically known. `None` means revm falls back
2828 /// to `0`, which can steer contracts that branch on `block.number` down a
2829 /// different code path. Override directly via
2830 /// [`set_block_context`](Self::set_block_context).
2831 pub fn block_number(&self) -> Option<u64> {
2832 self.block_number
2833 }
2834
2835 /// Get the base fee per gas used for EVM simulations (the `BASEFEE` opcode).
2836 ///
2837 /// Fetched from the pinned block's header at construction. `None` means
2838 /// revm falls back to `0`. This is cleared by [`set_block`](Self::set_block)
2839 /// / [`repin_to_block`](Self::repin_to_block) when the pin changes, and by
2840 /// non-concrete tag/hash pin calls because those can drift without a
2841 /// concrete number in the API. Refresh it with
2842 /// [`set_block_context`](Self::set_block_context) after fetching a new header
2843 /// if `BASEFEE` accuracy matters.
2844 pub fn basefee(&self) -> Option<u64> {
2845 self.basefee
2846 }
2847
2848 /// Update the block context for EVM simulations.
2849 ///
2850 /// Call this when the simulation block changes (e.g. at the start of each
2851 /// search cycle) to keep NUMBER and BASEFEE opcodes accurate.
2852 pub fn set_block_context(&mut self, block_number: Option<u64>, basefee: Option<u64>) {
2853 self.block_number = block_number;
2854 self.basefee = basefee;
2855 }
2856
2857 /// Set the block base fee (the `BASEFEE` opcode) for subsequent simulations,
2858 /// propagated into the next [`create_snapshot`](Self::create_snapshot).
2859 ///
2860 /// Offline caches built over a mocked provider have no fetched block header,
2861 /// so the base fee is unset (and the `BASEFEE` opcode reads `0`). Use this to
2862 /// install one explicitly — it determines the priority fee
2863 /// (`gas_price − basefee`) credited to the beneficiary, and thus the
2864 /// `coinbase_payment` a [`simulate_bundle`](Self::simulate_bundle) reports.
2865 ///
2866 /// The cache stores the base fee as a `u64` (matching the block header and the
2867 /// `EvmSnapshot` field), so a `U256` larger than `u64::MAX` is saturated.
2868 pub fn set_basefee(&mut self, basefee: U256) {
2869 self.basefee = Some(basefee.saturating_to::<u64>());
2870 }
2871
2872 /// Override the block beneficiary (the `COINBASE` opcode) for subsequent
2873 /// simulations.
2874 ///
2875 /// Set this when simulating logic that reads `block.coinbase` (e.g.
2876 /// MEV/builder tip accounting). `None` lets revm use its default beneficiary.
2877 pub fn set_coinbase(&mut self, coinbase: Option<Address>) {
2878 self.coinbase = coinbase;
2879 }
2880
2881 /// Override `prevrandao` (the `PREVRANDAO` opcode, the post-merge header mix
2882 /// hash) for subsequent simulations.
2883 ///
2884 /// Set this when reproducing contracts that source on-chain randomness from
2885 /// `block.prevrandao`. `None` leaves revm's default in place.
2886 pub fn set_prevrandao(&mut self, prevrandao: Option<B256>) {
2887 self.prevrandao = prevrandao;
2888 }
2889
2890 /// Override the block gas limit (the `GASLIMIT` opcode) for subsequent
2891 /// simulations.
2892 ///
2893 /// Set this when simulating logic that reads `block.gaslimit`. `None` lets
2894 /// revm use its default.
2895 pub fn set_block_gas_limit(&mut self, gas_limit: Option<u64>) {
2896 self.block_gas_limit = gas_limit;
2897 }
2898
2899 /// Re-pin the cache to a specific block number.
2900 ///
2901 /// Updates the SharedBackend pinned block, the batch fetcher block, and the
2902 /// EVM block context (`NUMBER` opcode) in lockstep. The current `basefee` is
2903 /// cleared because it cannot be refreshed synchronously; callers should set it
2904 /// via [`set_block_context`](Self::set_block_context) after fetching the new
2905 /// block header if `BASEFEE` accuracy matters.
2906 pub fn repin_to_block(&mut self, block_number: u64) {
2907 let old_block = self.block;
2908 self.set_block(BlockId::Number(block_number.into()));
2909
2910 if let BlockId::Number(BlockNumberOrTag::Number(old_num)) = old_block {
2911 let drift = block_number.saturating_sub(old_num);
2912 if drift > 0 {
2913 debug!(
2914 old_block = old_num,
2915 new_block = block_number,
2916 drift,
2917 "Re-pinned cache to current block"
2918 );
2919 }
2920 }
2921 }
2922
2923 /// Ensure an account is loaded into the cache.
2924 ///
2925 /// With the lazy-loading backend, this is optional - accounts are fetched
2926 /// automatically when accessed. However, you can use this to pre-warm
2927 /// the cache for specific accounts.
2928 #[instrument(level = "trace", skip(self))]
2929 pub async fn ensure_account(&mut self, address: Address) -> Result<()> {
2930 if self.db.cache.accounts.contains_key(&address) {
2931 return Ok(());
2932 }
2933
2934 // Load account info via SharedBackend (fetches from RPC if not cached).
2935 // basic_ref populates BlockchainDb; we also insert into the CacheDB
2936 // overlay so the account is immediately available for direct reads.
2937 use revm::database_interface::DatabaseRef;
2938 let info = self
2939 .backend
2940 .basic_ref(address)
2941 .map_err(|e| anyhow!("Failed to fetch account: {:?}", e))?;
2942
2943 if let Some(info) = info {
2944 self.db.insert_account_info(address, info);
2945 }
2946
2947 Ok(())
2948 }
2949
2950 /// Read a single storage slot through the SharedBackend (BlockchainDb -> RPC fallback).
2951 ///
2952 /// After `purge_contract_slots` removes a slot from BlockchainDb, this method fetches
2953 /// fresh data from RPC and caches it in BlockchainDb. Subsequent EVM SLOADs find
2954 /// the value there without additional RPC calls.
2955 pub fn read_storage_slot(&mut self, address: Address, slot: U256) -> Result<U256> {
2956 use revm::database_interface::DatabaseRef;
2957 self.backend
2958 .storage_ref(address, slot)
2959 .map_err(|e| anyhow!("storage read failed for {address} slot {slot}: {e}"))
2960 }
2961
2962 /// Write a raw storage slot value directly into the CacheDB layer.
2963 ///
2964 /// Subsequent EVM SLOADs for this (address, slot) will read the injected value
2965 /// without any RPC call. Used for hot-state injection where we already know the
2966 /// current on-chain value from WebSocket events.
2967 pub fn insert_storage_slot(&mut self, address: Address, slot: U256, value: U256) -> Result<()> {
2968 self.db.insert_account_storage(address, slot, value)?;
2969 Ok(())
2970 }
2971
2972 /// Pre-seed known ERC20 `balanceOf` mapping base slots, keyed by token.
2973 ///
2974 /// Each `(token, slot)` records the storage slot of the token's
2975 /// `mapping(address => uint256) balances`, letting
2976 /// [`set_erc20_balance_with_slot_scan`](Self::set_erc20_balance_with_slot_scan)
2977 /// skip its `0..=max_slot` probing pass for that token and write the balance
2978 /// directly. Seeding a wrong slot is self-correcting: the scan verifies the
2979 /// write and falls back to a fresh probe (evicting the bad seed) if it
2980 /// fails. Later entries overwrite earlier ones for the same token.
2981 pub fn seed_erc20_balance_slots(&mut self, slots: impl IntoIterator<Item = (Address, U256)>) {
2982 for (token, slot) in slots {
2983 self.erc20_balance_slots.insert(token, slot);
2984 }
2985 }
2986
2987 /// Write a value into a Solidity `mapping(address => ...)` entry on
2988 /// `contract`, at the mapping declared at base slot `slot`.
2989 ///
2990 /// Computes the entry's storage key as
2991 /// `keccak256(abi.encode(slot_address, slot))` — Solidity's layout for an
2992 /// address-keyed mapping — and writes `value` there in the CacheDB overlay.
2993 /// Used to forge ERC20 balances and allowances without an on-chain transfer.
2994 ///
2995 /// # Errors
2996 /// Returns an error if the underlying CacheDB storage insert fails (e.g. the
2997 /// account cannot be loaded from the backend).
2998 pub fn insert_mapping_storage_slot(
2999 &mut self,
3000 contract: Address,
3001 slot: U256,
3002 slot_address: Address,
3003 value: U256,
3004 ) -> Result<()> {
3005 let hashed_balance_slot = keccak256((slot_address, slot).abi_encode());
3006 self.db
3007 .insert_account_storage(contract, hashed_balance_slot.into(), value)?;
3008 Ok(())
3009 }
3010
3011 /// Set an ERC20 balance by probing storage mapping slots until `balanceOf(owner)` reflects
3012 /// a probe value, then writing `amount` to the discovered slot.
3013 ///
3014 /// Returns `Ok(true)` if the balance was set and verified, `Ok(false)` if no slot in
3015 /// `0..=max_slot` matched, and `Err` on EVM/cache failures.
3016 pub fn set_erc20_balance_with_slot_scan(
3017 &mut self,
3018 token: Address,
3019 owner: Address,
3020 amount: U256,
3021 max_slot: u16,
3022 ) -> Result<bool> {
3023 if let Some(slot) = self.erc20_balance_slots.get(&token).copied() {
3024 self.insert_mapping_storage_slot(token, slot, owner, amount)?;
3025 if self.erc20_balance_of(token, owner)? == amount {
3026 return Ok(true);
3027 }
3028 self.erc20_balance_slots.remove(&token);
3029 }
3030
3031 let Some(discovered_slot) =
3032 self.discover_erc20_balance_slot_with_scan(token, owner, max_slot)?
3033 else {
3034 return Ok(false);
3035 };
3036
3037 self.insert_mapping_storage_slot(token, discovered_slot, owner, amount)?;
3038 let verified = self.erc20_balance_of(token, owner)? == amount;
3039 if verified {
3040 self.erc20_balance_slots.insert(token, discovered_slot);
3041 } else {
3042 self.erc20_balance_slots.remove(&token);
3043 }
3044 Ok(verified)
3045 }
3046
3047 fn discover_erc20_balance_slot_with_scan(
3048 &mut self,
3049 token: Address,
3050 owner: Address,
3051 max_slot: u16,
3052 ) -> Result<Option<U256>> {
3053 if let Some(slot) = self.erc20_balance_slots.get(&token).copied() {
3054 return Ok(Some(slot));
3055 }
3056
3057 let baseline_snapshot = self.snapshot();
3058 let baseline_balance = self.erc20_balance_of(token, owner)?;
3059
3060 // Choose a probe value distinct from baseline to avoid false positives.
3061 let mut probe = U256::from(0xDEAD_BEEF_u64);
3062 if probe == baseline_balance {
3063 probe = baseline_balance.saturating_add(U256::from(1u64));
3064 }
3065 if probe == baseline_balance {
3066 probe = U256::MAX;
3067 }
3068
3069 for slot_idx in 0..=max_slot {
3070 self.restore(baseline_snapshot.clone());
3071 let slot = U256::from(slot_idx);
3072 self.insert_mapping_storage_slot(token, slot, owner, probe)?;
3073 if self.erc20_balance_of(token, owner)? == probe {
3074 self.restore(baseline_snapshot);
3075 self.erc20_balance_slots.insert(token, slot);
3076 return Ok(Some(slot));
3077 }
3078 }
3079
3080 self.restore(baseline_snapshot);
3081 Ok(None)
3082 }
3083
3084 /// Execute a call with automatic account/storage fetching.
3085 ///
3086 /// Unlike the old implementation, this does NOT prefetch via access lists.
3087 /// The SharedBackend lazily fetches any missing data during execution.
3088 #[instrument(level = "debug", skip(self, calldata), fields(calldata_len = calldata.len()))]
3089 pub fn call(
3090 &mut self,
3091 from: Address,
3092 to: Address,
3093 calldata: Bytes,
3094 commit: bool,
3095 ) -> Result<ExecutionResult> {
3096 self.call_raw(from, to, calldata, commit)
3097 }
3098
3099 /// Execute a call without any prefetching.
3100 ///
3101 /// Data is fetched lazily by the SharedBackend as needed during execution.
3102 #[instrument(level = "debug", skip(self, calldata), fields(calldata_len = calldata.len()))]
3103 pub fn call_raw(
3104 &mut self,
3105 from: Address,
3106 to: Address,
3107 calldata: Bytes,
3108 commit: bool,
3109 ) -> Result<ExecutionResult> {
3110 self.call_raw_with(from, to, calldata, commit, &TxConfig::default())
3111 }
3112
3113 /// Execute a non-committing typed Solidity call from [`Address::ZERO`].
3114 ///
3115 /// This is the typed equivalent of encoding a [`SolCall`], passing it to
3116 /// [`call_raw`](Self::call_raw) with `commit = false`, and decoding the
3117 /// successful return data with [`SolCall::abi_decode_returns`].
3118 ///
3119 /// ```no_run
3120 /// # use alloy_primitives::Address;
3121 /// # use alloy_sol_types::sol;
3122 /// # use evm_fork_cache::cache::EvmCache;
3123 /// # fn example(cache: &mut EvmCache, token: Address, owner: Address) -> anyhow::Result<()> {
3124 /// sol! {
3125 /// function balanceOf(address account) external view returns (uint256);
3126 /// }
3127 ///
3128 /// let balance = cache.call_sol(token, balanceOfCall { account: owner })?;
3129 /// # let _ = balance;
3130 /// # Ok(())
3131 /// # }
3132 /// ```
3133 pub fn call_sol<C>(&mut self, to: Address, call: C) -> Result<C::Return>
3134 where
3135 C: SolCall,
3136 {
3137 self.call_sol_from(Address::ZERO, to, call)
3138 }
3139
3140 /// Execute a non-committing typed Solidity call from an explicit sender.
3141 ///
3142 /// Uses the default [`TxConfig`], so native value, gas limit/price, nonce,
3143 /// and access list are left at the same defaults as [`call_raw`](Self::call_raw).
3144 pub fn call_sol_from<C>(&mut self, from: Address, to: Address, call: C) -> Result<C::Return>
3145 where
3146 C: SolCall,
3147 {
3148 self.call_sol_with_commit(from, to, call, &TxConfig::default(), false)
3149 }
3150
3151 /// Execute a non-committing typed Solidity call with explicit tx overrides.
3152 ///
3153 /// This is the typed equivalent of [`call_raw_with`](Self::call_raw_with)
3154 /// with `commit = false`.
3155 pub fn call_sol_with<C>(
3156 &mut self,
3157 from: Address,
3158 to: Address,
3159 call: C,
3160 tx: &TxConfig,
3161 ) -> Result<C::Return>
3162 where
3163 C: SolCall,
3164 {
3165 self.call_sol_with_commit(from, to, call, tx, false)
3166 }
3167
3168 /// Execute a typed Solidity call and commit its state changes.
3169 ///
3170 /// This is the typed equivalent of [`call_raw_with`](Self::call_raw_with)
3171 /// with `commit = true`; the call's state changes are persisted through the
3172 /// same path as the raw committing API before the return data is decoded.
3173 pub fn transact_sol<C>(
3174 &mut self,
3175 from: Address,
3176 to: Address,
3177 call: C,
3178 tx: &TxConfig,
3179 ) -> Result<C::Return>
3180 where
3181 C: SolCall,
3182 {
3183 self.call_sol_with_commit(from, to, call, tx, true)
3184 }
3185
3186 /// Execute a call with explicit transaction-environment overrides
3187 /// ([`TxConfig`]): native `value`, gas limit/price, nonce, and an input
3188 /// access list. This is the entry point for value-bearing and gas-bounded
3189 /// simulation; [`call_raw`](Self::call_raw) is the zero-value shorthand.
3190 #[instrument(level = "debug", skip(self, calldata, tx), fields(calldata_len = calldata.len()))]
3191 pub fn call_raw_with(
3192 &mut self,
3193 from: Address,
3194 to: Address,
3195 calldata: Bytes,
3196 commit: bool,
3197 tx: &TxConfig,
3198 ) -> Result<ExecutionResult> {
3199 let tx_env = Self::build_tx_env_with(from, to, calldata, tx)?;
3200 let mut evm = self.build_evm();
3201
3202 if commit {
3203 return evm
3204 .transact_commit(tx_env)
3205 .map_err(|e| anyhow!("Failed to transact: {:?}", e));
3206 }
3207
3208 let checkpoint = evm.journaled_state.checkpoint();
3209 let result = evm.transact_one(tx_env);
3210 evm.journaled_state.checkpoint_revert(checkpoint);
3211 result.map_err(|e| anyhow!("Failed to transact: {:?}", e))
3212 }
3213
3214 /// Execute a non-committing call and extract the access list of touched
3215 /// accounts and storage slots before reverting.
3216 ///
3217 /// Used for EIP-2929 marginal gas estimation in batched simulations.
3218 pub fn call_raw_with_access_list(
3219 &mut self,
3220 from: Address,
3221 to: Address,
3222 calldata: Bytes,
3223 ) -> Result<(ExecutionResult, StorageAccessList)> {
3224 let tx = Self::build_tx_env(from, to, calldata)?;
3225 let mut evm = self.build_evm();
3226
3227 let checkpoint = evm.journaled_state.checkpoint();
3228 match evm.transact_one(tx) {
3229 Ok(result) => {
3230 // Extract access list from journaled state before reverting. After
3231 // transact_one, journaled_state.state holds all touched accounts/slots.
3232 let mut access_list = StorageAccessList::default();
3233 for (address, account) in evm.journaled_state.state.iter() {
3234 if account.is_touched() {
3235 access_list.accounts.insert(*address);
3236 for (slot_key, _) in account.storage.iter() {
3237 access_list.slots.insert((*address, *slot_key));
3238 }
3239 }
3240 }
3241 evm.journaled_state.checkpoint_revert(checkpoint);
3242 Ok((result, access_list))
3243 }
3244 Err(e) => {
3245 // Revert the checkpoint even on a host/transact error so the EVM
3246 // journal is not left dirty (mirrors `call_raw`).
3247 evm.journaled_state.checkpoint_revert(checkpoint);
3248 Err(anyhow!("Failed to transact: {:?}", e))
3249 }
3250 }
3251 }
3252
3253 /// Execute a call and return its emitted logs and gas used.
3254 ///
3255 /// A thin wrapper over [`call`](Self::call) that requires success and
3256 /// discards the return data. When `commit` is true the call's state changes
3257 /// are persisted to the CacheDB overlay; otherwise they are reverted.
3258 ///
3259 /// # Errors
3260 /// Returns an error if the underlying transact fails, or if the call did not
3261 /// `Success` (i.e. it reverted or halted).
3262 pub fn call_logs(
3263 &mut self,
3264 from: Address,
3265 to: Address,
3266 calldata: Bytes,
3267 commit: bool,
3268 ) -> Result<(Vec<Log>, u64)> {
3269 let result = self.call(from, to, calldata, commit)?;
3270 if let ExecutionResult::Success { logs, gas_used, .. } = result {
3271 Ok((logs, gas_used))
3272 } else {
3273 Err(anyhow!("Failed to call: {:?}", result))
3274 }
3275 }
3276
3277 /// Read an ERC20 token balance by simulating a `balanceOf(owner)` call.
3278 ///
3279 /// Non-committing: the read is reverted, so it never mutates cache state.
3280 ///
3281 /// # Errors
3282 /// Returns an error if the simulated call fails or does not `Success` (e.g.
3283 /// `token` is not a contract or reverts), or if the returned data cannot be
3284 /// ABI-decoded as a `uint256`.
3285 pub fn erc20_balance_of(&mut self, token: Address, owner: Address) -> Result<U256> {
3286 let call = IERC20::balanceOfCall { target: owner };
3287 let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
3288
3289 match result {
3290 ExecutionResult::Success { output, .. } => {
3291 let out = output.into_data();
3292 let balance = IERC20::balanceOfCall::abi_decode_returns(&out)
3293 .map_err(|e| anyhow!("Failed to decode balanceOf: {:?}", e))?;
3294 Ok(balance)
3295 }
3296 _ => Err(anyhow!("balanceOf call failed: {:?}", result)),
3297 }
3298 }
3299
3300 /// Read an ERC20 allowance by simulating an `allowance(owner, spender)` call.
3301 ///
3302 /// Non-committing: the read is reverted, so it never mutates cache state.
3303 ///
3304 /// # Errors
3305 /// Returns an error if the simulated call fails or does not `Success` (e.g.
3306 /// `token` is not a contract or reverts), or if the returned data cannot be
3307 /// ABI-decoded as a `uint256`.
3308 pub fn erc20_allowance(
3309 &mut self,
3310 token: Address,
3311 owner: Address,
3312 spender: Address,
3313 ) -> Result<U256> {
3314 let call = IERC20::allowanceCall { owner, spender };
3315 let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
3316
3317 match result {
3318 ExecutionResult::Success { output, .. } => {
3319 let out = output.into_data();
3320 let allowance = IERC20::allowanceCall::abi_decode_returns(&out)
3321 .map_err(|e| anyhow!("Failed to decode allowance: {:?}", e))?;
3322 Ok(allowance)
3323 }
3324 _ => Err(anyhow!("allowance call failed: {:?}", result)),
3325 }
3326 }
3327
3328 /// Read an ERC20 token's decimals by simulating a `decimals()` call.
3329 ///
3330 /// Memoized: a hit in the in-memory token-decimals map returns immediately
3331 /// without simulating. On a miss the value is resolved by a non-committing
3332 /// `decimals()` call.
3333 ///
3334 /// # Side effects
3335 /// On a miss the resolved value is cached in **both** the in-memory
3336 /// token-decimals map (process lifetime) **and** the immutable data cache
3337 /// (so it is persisted to disk on the next [`flush`](Self::flush)).
3338 ///
3339 /// # Errors
3340 /// Returns an error if the simulated call fails or does not `Success` (e.g.
3341 /// `token` is not a contract or reverts), or if the returned data cannot be
3342 /// ABI-decoded as a `uint8`.
3343 pub fn erc20_decimals(&mut self, token: Address) -> Result<u8> {
3344 if let Some(decimals) = self.token_decimals.get(&token) {
3345 return Ok(*decimals);
3346 }
3347
3348 let call = IERC20::decimalsCall {};
3349 let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
3350
3351 match result {
3352 ExecutionResult::Success { output, .. } => {
3353 let out = output.into_data();
3354 let decimals = IERC20::decimalsCall::abi_decode_returns(&out)
3355 .map_err(|e| anyhow!("Failed to decode decimals: {:?}", e))?;
3356 self.token_decimals.insert(token, decimals);
3357 // Also update immutable cache for persistence
3358 self.immutable_cache.set_token_decimals(token, decimals);
3359 Ok(decimals)
3360 }
3361 _ => Err(anyhow!("decimals call failed: {:?}", result)),
3362 }
3363 }
3364
3365 /// Get a reference to the immutable data cache (token decimals).
3366 pub fn immutable_cache(&self) -> &ImmutableDataCache {
3367 &self.immutable_cache
3368 }
3369
3370 /// Get a mutable reference to the immutable data cache.
3371 ///
3372 /// Use this to pre-populate token decimals that would otherwise be discovered
3373 /// lazily. Entries are persisted on the next [`flush`](Self::flush) (and on
3374 /// drop) when a [`CacheConfig`] is set.
3375 pub fn immutable_cache_mut(&mut self) -> &mut ImmutableDataCache {
3376 &mut self.immutable_cache
3377 }
3378
3379 /// Check if an address has storage slots pre-loaded in the BlockchainDb.
3380 ///
3381 /// This is useful to determine if we loaded the EVM state from the unified
3382 /// `evm_state.bin` cache and an address already has reusable storage.
3383 ///
3384 /// # Arguments
3385 /// * `address` - The contract address to check
3386 ///
3387 /// # Returns
3388 /// `true` if the address has any storage slots in the underlying BlockchainDb,
3389 /// `false` otherwise
3390 pub fn has_contract_storage(&self, address: Address) -> bool {
3391 let storage = self.blockchain_db.storage().read();
3392 storage
3393 .get(&address)
3394 .map(|slots| !slots.is_empty())
3395 .unwrap_or(false)
3396 }
3397
3398 /// Get the number of storage slots loaded for a contract address.
3399 ///
3400 /// Useful for debugging and logging to understand cache state.
3401 pub fn contract_storage_slot_count(&self, address: Address) -> usize {
3402 let storage = self.blockchain_db.storage().read();
3403 storage.get(&address).map(|slots| slots.len()).unwrap_or(0)
3404 }
3405
3406 /// Get memory statistics for the shared memory buffer used during EVM simulations.
3407 ///
3408 /// Returns a tuple of (current_capacity_bytes, current_length_bytes).
3409 ///
3410 /// The capacity represents the high-water mark of memory usage across all
3411 /// simulations since the buffer grows but doesn't shrink. The length is
3412 /// typically 0 between simulations (cleared after each use).
3413 ///
3414 /// # Use Case
3415 /// Call this after running a batch of simulations to understand memory usage
3416 /// and inform the optimal initial capacity for `SharedMemory`.
3417 ///
3418 /// # Example
3419 /// ```ignore
3420 /// let (capacity, _len) = cache.shared_memory_stats();
3421 /// println!("Peak memory usage: {} KB", capacity / 1024);
3422 /// ```
3423 pub fn shared_memory_stats(&self) -> (usize, usize) {
3424 let buffer = self.shared_memory_buffer.borrow();
3425 (buffer.capacity(), buffer.len())
3426 }
3427
3428 /// Log the current shared memory buffer statistics.
3429 ///
3430 /// Useful for profiling after running a batch of simulations.
3431 pub fn log_shared_memory_stats(&self) {
3432 let (capacity, len) = self.shared_memory_stats();
3433 debug!(
3434 capacity_bytes = capacity,
3435 capacity_kb = capacity / 1024,
3436 current_len = len,
3437 "Shared memory buffer stats (peak capacity across simulations)"
3438 );
3439 }
3440
3441 /// Pre-allocate the shared memory buffer to a specific capacity.
3442 ///
3443 /// Use this after measuring peak usage to avoid reallocation overhead
3444 /// during simulations. The buffer will grow beyond this if needed,
3445 /// but pre-sizing to the expected peak eliminates allocations.
3446 ///
3447 /// # Arguments
3448 /// * `capacity` - The capacity in bytes to reserve
3449 ///
3450 /// # Example
3451 /// ```ignore
3452 /// // After profiling shows peak usage is ~32KB
3453 /// cache.reserve_shared_memory(32 * 1024);
3454 /// ```
3455 pub fn reserve_shared_memory(&mut self, capacity: usize) {
3456 let mut buffer = self.shared_memory_buffer.borrow_mut();
3457 let current_capacity = buffer.capacity();
3458 if current_capacity < capacity {
3459 buffer.reserve(capacity - current_capacity);
3460 debug!(
3461 new_capacity = buffer.capacity(),
3462 requested = capacity,
3463 "Reserved shared memory buffer capacity"
3464 );
3465 }
3466 drop(buffer);
3467 // Record the high-water mark so snapshots taken afterwards propagate it to
3468 // their overlays (snapshots copy the capacity at creation time).
3469 self.shared_memory_capacity = self.shared_memory_capacity.max(capacity);
3470 }
3471
3472 /// The resolved per-context EVM shared-memory pre-allocation, in bytes.
3473 ///
3474 /// This is the [`SharedMemoryCapacity`] configured on the
3475 /// [`EvmCacheBuilder`] resolved to a concrete size (with
3476 /// [`SharedMemoryCapacity::Auto`] resolved against the state loaded at
3477 /// construction), raised by any later [`reserve_shared_memory`](Self::reserve_shared_memory).
3478 /// Each [`create_snapshot`](Self::create_snapshot) copies it onto the snapshot
3479 /// so snapshot-backed [`EvmOverlay`]s pre-allocate the same amount.
3480 pub fn shared_memory_capacity(&self) -> usize {
3481 self.shared_memory_capacity
3482 }
3483
3484 /// Purge all storage slots for a specific contract from both cache layers.
3485 ///
3486 /// This clears:
3487 /// 1. **CacheDB overlay** (`self.db.cache.accounts[addr].storage`) - the in-memory
3488 /// layer that caches storage slots fetched during EVM execution. Without clearing
3489 /// this layer, subsequent EVM calls return stale values even after the backend
3490 /// is purged.
3491 /// 2. **BlockchainDb backend** (`self.blockchain_db.storage()`) - the persistent
3492 /// layer that caches RPC responses and is loaded from `evm_state.bin`.
3493 ///
3494 /// After purging both layers, the next EVM read for this contract's storage will
3495 /// go all the way to the RPC for fresh data.
3496 pub fn purge_contract_storage(&mut self, address: Address) -> usize {
3497 // Thin wrapper over the unified purge primitive; returns the backend slot
3498 // count the `AllStorage` scope removed.
3499 self.apply_update(&StateUpdate::purge(address, PurgeScope::AllStorage))
3500 .purged
3501 .first()
3502 .map(|rec| rec.slots_removed)
3503 .unwrap_or(0)
3504 }
3505
3506 /// `AllStorage`-scope purge layer logic. Clears the overlay storage for
3507 /// `address` and removes its backend storage map. Returns the number of
3508 /// backend slots removed.
3509 fn purge_contract_storage_inner(&mut self, address: Address) -> usize {
3510 // Layer 1: Clear CacheDB overlay
3511 let cache_db_cleared = if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
3512 let count = db_account.storage.len();
3513 db_account.storage.clear();
3514 count
3515 } else {
3516 0
3517 };
3518
3519 // Layer 2: Clear BlockchainDb backend
3520 let backend_cleared = {
3521 let mut storage = self.blockchain_db.storage().write();
3522 if let Some(slots) = storage.remove(&address) {
3523 slots.len()
3524 } else {
3525 0
3526 }
3527 };
3528
3529 if cache_db_cleared > 0 || backend_cleared > 0 {
3530 debug!(
3531 contract = %address,
3532 cache_db_slots = cache_db_cleared,
3533 backend_slots = backend_cleared,
3534 "purged contract storage from both cache layers"
3535 );
3536 }
3537
3538 // Layer-2 storage for this address was removed → invalidate base.
3539 self.mark_base_dirty(address);
3540 backend_cleared
3541 }
3542
3543 /// Purge specific storage slots for a contract from both cache layers.
3544 ///
3545 /// Unlike `purge_contract_storage()` which removes ALL storage, this only removes
3546 /// the specified slots. This is useful when only a narrow subset of hot storage
3547 /// became stale and the rest of the contract's cached storage should be kept.
3548 ///
3549 /// Returns the number of slots removed from the BlockchainDb backend.
3550 pub fn purge_contract_slots(&mut self, address: Address, slots: &[U256]) -> usize {
3551 // Thin wrapper over the unified purge primitive; returns the backend slot
3552 // count the `Slots` scope removed.
3553 self.apply_update(&StateUpdate::purge(
3554 address,
3555 PurgeScope::Slots(slots.to_vec()),
3556 ))
3557 .purged
3558 .first()
3559 .map(|rec| rec.slots_removed)
3560 .unwrap_or(0)
3561 }
3562
3563 /// `Slots`-scope purge layer logic. Removes the listed slots from the overlay
3564 /// and the backend storage map. Returns the number of backend slots removed.
3565 fn purge_contract_slots_inner(&mut self, address: Address, slots: &[U256]) -> usize {
3566 let mut cache_db_removed = 0usize;
3567 let mut backend_removed = 0usize;
3568
3569 // Layer 1: Remove specific slots from CacheDB overlay
3570 if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
3571 for slot in slots {
3572 if db_account.storage.remove(slot).is_some() {
3573 cache_db_removed += 1;
3574 }
3575 }
3576 }
3577
3578 // Layer 2: Remove specific slots from BlockchainDb backend
3579 {
3580 let mut storage = self.blockchain_db.storage().write();
3581 if let Some(address_storage) = storage.get_mut(&address) {
3582 for slot in slots {
3583 if address_storage.remove(slot).is_some() {
3584 backend_removed += 1;
3585 }
3586 }
3587 }
3588 }
3589
3590 if cache_db_removed > 0 || backend_removed > 0 {
3591 trace!(
3592 contract = %address,
3593 requested = slots.len(),
3594 cache_db_removed,
3595 backend_removed,
3596 "selectively purged contract storage slots from both cache layers"
3597 );
3598 }
3599
3600 // Layer-2 storage for this address changed (slots dropped) → invalidate
3601 // base. The growth scan only catches length changes; mark explicitly.
3602 self.mark_base_dirty(address);
3603 backend_removed
3604 }
3605
3606 /// Purge storage slots for multiple contracts from both cache layers.
3607 ///
3608 /// See `purge_contract_storage()` for details on what each layer contains.
3609 pub fn purge_contracts_storage(
3610 &mut self,
3611 addresses: impl IntoIterator<Item = Address>,
3612 ) -> usize {
3613 let mut total_purged = 0usize;
3614
3615 for address in addresses {
3616 // Layer 1: Clear CacheDB overlay
3617 if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
3618 db_account.storage.clear();
3619 }
3620
3621 // Layer 2: Clear BlockchainDb backend
3622 let mut storage = self.blockchain_db.storage().write();
3623 if let Some(slots) = storage.remove(&address) {
3624 let count = slots.len();
3625 if count > 0 {
3626 debug!(
3627 contract = %address,
3628 slots_removed = count,
3629 "purged contract storage from both cache layers"
3630 );
3631 }
3632 total_purged += count;
3633 }
3634 }
3635
3636 if total_purged > 0 {
3637 debug!(
3638 total_slots_purged = total_purged,
3639 "purged contract storage from both cache layers"
3640 );
3641 }
3642 // Multiple layer-2 contracts changed → full base rebuild (coarse but
3643 // correct; cheaper than enumerating each touched address here).
3644 self.invalidate_base();
3645 total_purged
3646 }
3647
3648 /// Purge ALL storage slots from both cache layers while preserving bytecodes.
3649 ///
3650 /// Use this for periodic full cache refresh (e.g., every 48 hours) to ensure
3651 /// any stale data like strategy swap paths, proxy implementations, reward rates,
3652 /// etc. are re-fetched from the actual on-chain state.
3653 ///
3654 /// This preserves:
3655 /// - Account info (nonce, balance, code hash)
3656 /// - Contract bytecodes (immutable)
3657 ///
3658 /// This purges:
3659 /// - All storage slots from CacheDB overlay (layer 1)
3660 /// - All storage slots from BlockchainDb backend (layer 2)
3661 ///
3662 /// # Returns
3663 /// The total number of storage slots that were removed from the BlockchainDb
3664 pub fn purge_all_storage(&mut self) -> usize {
3665 // Layer 1: Clear all storage in CacheDB overlay
3666 let mut cache_db_cleared = 0usize;
3667 for db_account in self.db.cache.accounts.values_mut() {
3668 cache_db_cleared += db_account.storage.len();
3669 db_account.storage.clear();
3670 }
3671
3672 // Layer 2: Clear BlockchainDb backend
3673 let (total_slots, contract_count) = {
3674 let mut storage = self.blockchain_db.storage().write();
3675 let total_slots: usize = storage.values().map(|s| s.len()).sum();
3676 let contract_count = storage.len();
3677 storage.clear();
3678 (total_slots, contract_count)
3679 };
3680
3681 if total_slots > 0 || cache_db_cleared > 0 {
3682 warn!(
3683 contracts_cleared = contract_count,
3684 backend_slots_purged = total_slots,
3685 cache_db_slots_purged = cache_db_cleared,
3686 "purged ALL storage from both cache layers (full refresh)"
3687 );
3688 }
3689 // All layer-2 storage was cleared → full base rebuild.
3690 self.invalidate_base();
3691 total_slots
3692 }
3693
3694 /// Enumerate all cached storage slots for a contract address.
3695 ///
3696 /// Returns the union of slot keys from both CacheDB overlay (layer 1) and
3697 /// BlockchainDb backend (layer 2). Used by the slot observation tracker to
3698 /// selectively purge only slots likely to have changed.
3699 pub fn enumerate_contract_slots(&self, address: Address) -> Vec<U256> {
3700 let mut slots: HashSet<U256> = HashSet::new();
3701
3702 // Layer 1: CacheDB overlay
3703 if let Some(db_account) = self.db.cache.accounts.get(&address) {
3704 slots.extend(db_account.storage.keys().copied());
3705 }
3706
3707 // Layer 2: BlockchainDb backend
3708 let storage = self.blockchain_db.storage().read();
3709 if let Some(backend_slots) = storage.get(&address) {
3710 slots.extend(backend_slots.keys().copied());
3711 }
3712
3713 slots.into_iter().collect()
3714 }
3715
3716 /// Return all contract addresses that have cached storage in either layer.
3717 ///
3718 /// Used by the observation-aware full purge to enumerate what needs checking.
3719 pub fn all_cached_contract_addresses(&self) -> Vec<Address> {
3720 let mut addrs: HashSet<Address> = HashSet::new();
3721
3722 // Layer 1: CacheDB overlay
3723 for (addr, account) in &self.db.cache.accounts {
3724 if !account.storage.is_empty() {
3725 addrs.insert(*addr);
3726 }
3727 }
3728
3729 // Layer 2: BlockchainDb backend
3730 let storage = self.blockchain_db.storage().read();
3731 for addr in storage.keys() {
3732 addrs.insert(*addr);
3733 }
3734
3735 addrs.into_iter().collect()
3736 }
3737
3738 /// Get the number of storage slots in the CacheDB overlay for a contract.
3739 ///
3740 /// This is useful for diagnostics: if a contract has slots in the CacheDB
3741 /// overlay, they will be served on EVM reads without going to the backend.
3742 pub fn cache_db_storage_slot_count(&self, address: Address) -> usize {
3743 self.db
3744 .cache
3745 .accounts
3746 .get(&address)
3747 .map(|a| a.storage.len())
3748 .unwrap_or(0)
3749 }
3750
3751 /// Simulate a call and compute `owner`'s net balance change for each token
3752 /// in `tokens` by reading `balanceOf(owner)` immediately before and after.
3753 ///
3754 /// Each delta is the signed `post - pre` difference (see
3755 /// [`CallSimulationResult::token_deltas`]). When `commit` is true the call's
3756 /// state changes are persisted to the CacheDB overlay; otherwise they are
3757 /// reverted. Unlike
3758 /// [`simulate_with_transfer_tracking`](Self::simulate_with_transfer_tracking),
3759 /// this measures deltas via pre/post balance reads (not transfer-event
3760 /// inspection). The returned [`access_list`](CallSimulationResult::access_list)
3761 /// includes the accounts and slots touched by the pre/post `balanceOf` reads
3762 /// and the simulated call.
3763 ///
3764 /// # Errors
3765 /// Returns an error if building the tx env fails, if a pre/post
3766 /// `balanceOf` read fails, or if the call does not `Success` (i.e. it
3767 /// reverted or halted). On error the simulation is reverted.
3768 pub fn simulate_call_with_balance_deltas(
3769 &mut self,
3770 from: Address,
3771 to: Address,
3772 calldata: Bytes,
3773 owner: Address,
3774 tokens: impl IntoIterator<Item = Address>,
3775 commit: bool,
3776 ) -> Result<CallSimulationResult> {
3777 let token_list: Vec<Address> = tokens.into_iter().collect();
3778
3779 let mut pre_balances = HashMap::with_capacity(token_list.len());
3780 let mut access_lists = Vec::with_capacity(token_list.len().saturating_mul(2) + 1);
3781 for token in &token_list {
3782 let mut evm = self.build_evm();
3783 let synthetic_beneficiary = Self::seed_synthetic_beneficiary(&mut evm);
3784 let (balance, access_list) =
3785 Self::erc20_balance_of_in_evm_isolated(&mut evm, from, *token, owner)?;
3786 Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3787 pre_balances.insert(*token, balance);
3788 access_lists.push(access_list);
3789 }
3790
3791 let tx = Self::build_tx_env(from, to, calldata)?;
3792 let mut evm = self.build_evm();
3793 let synthetic_beneficiary = Self::seed_synthetic_beneficiary(&mut evm);
3794 let target_checkpoint = evm.journaled_state.checkpoint();
3795 let result = evm
3796 .transact_one(tx)
3797 .map_err(|e| anyhow!("Failed to transact: {:?}", e))?;
3798 let (logs, gas_used, output) = match result {
3799 ExecutionResult::Success {
3800 logs,
3801 gas_used,
3802 output,
3803 ..
3804 } => (logs, gas_used, output.into_data()),
3805 _ => {
3806 evm.journaled_state.checkpoint_revert(target_checkpoint);
3807 Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3808 return Err(anyhow!("Failed to call: {:?}", result));
3809 }
3810 };
3811 access_lists.push(extract_access_list(&evm.journaled_state.state));
3812
3813 let mut token_deltas = HashMap::with_capacity(token_list.len());
3814 for token in &token_list {
3815 let (post, access_list) =
3816 match Self::erc20_balance_of_in_evm_isolated(&mut evm, from, *token, owner) {
3817 Ok(result) => result,
3818 Err(err) => {
3819 evm.journaled_state.checkpoint_revert(target_checkpoint);
3820 Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3821 return Err(err);
3822 }
3823 };
3824 let pre = pre_balances.get(token).copied().unwrap_or_default();
3825 token_deltas.insert(*token, I256::from_raw(post) - I256::from_raw(pre));
3826 access_lists.push(access_list);
3827 }
3828
3829 let access_list = merge_access_lists(access_lists);
3830 if commit {
3831 Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3832 evm.commit_inner();
3833 } else {
3834 evm.journaled_state.checkpoint_revert(target_checkpoint);
3835 Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
3836 }
3837
3838 Ok(CallSimulationResult {
3839 status: SimStatus::Success,
3840 gas_used,
3841 token_deltas,
3842 logs,
3843 access_list,
3844 output,
3845 })
3846 }
3847
3848 /// Simulate a call and track token balance changes using a TransferInspector.
3849 ///
3850 /// This method uses EVM inspection to capture ERC20 Transfer events during execution,
3851 /// eliminating the need for manual balance reads before/after the transaction.
3852 ///
3853 /// Returns:
3854 /// - `Ok(CallSimulationResult)` on successful execution
3855 /// - `Err(SimError::Revert(_))` when the transaction reverts (graceful failure)
3856 /// - `Err(SimError::Other(_))` for unexpected errors (should be propagated)
3857 #[instrument(level = "debug", skip(self, calldata, tokens), fields(calldata_len = calldata.len()))]
3858 pub fn simulate_with_transfer_tracking(
3859 &mut self,
3860 from: Address,
3861 to: Address,
3862 calldata: Bytes,
3863 owner: Address,
3864 tokens: Option<impl IntoIterator<Item = Address>>,
3865 commit: bool,
3866 ) -> SimulationResult<CallSimulationResult> {
3867 let tx = Self::build_tx_env(from, to, calldata).map_err(SimError::Other)?;
3868 let inspector = TransferInspector::new();
3869 let mut evm = self.build_evm_with_inspector(inspector);
3870 let checkpoint = evm.journaled_state.checkpoint();
3871
3872 let result = evm
3873 .inspect_one_tx(tx)
3874 .map_err(|e| SimError::Other(anyhow!("Failed to transact: {:?}", e)));
3875
3876 match result {
3877 Ok(ExecutionResult::Success {
3878 logs,
3879 gas_used,
3880 output,
3881 ..
3882 }) => {
3883 // Compute balance deltas from captured transfers
3884 let token_deltas = if let Some(token_list) = tokens {
3885 evm.inspector.balance_deltas_for_tokens(owner, token_list)
3886 } else {
3887 evm.inspector.balance_deltas(owner)
3888 };
3889
3890 // Log shared memory buffer capacity for profiling
3891 let memory_capacity = evm.ctx.local.shared_memory_buffer.borrow().capacity();
3892 trace!(
3893 memory_capacity_bytes = memory_capacity,
3894 memory_capacity_kb = memory_capacity / 1024,
3895 "EVM shared memory buffer capacity after simulation"
3896 );
3897
3898 // Extract EIP-2930 access list from journaled state before commit/revert.
3899 // After inspect_one_tx, state contains all touched accounts and storage slots.
3900 let access_list = extract_access_list(&evm.journaled_state.state);
3901
3902 if commit {
3903 evm.commit_inner();
3904 } else {
3905 evm.journaled_state.checkpoint_revert(checkpoint);
3906 }
3907
3908 Ok(CallSimulationResult {
3909 status: SimStatus::Success,
3910 gas_used,
3911 token_deltas,
3912 logs,
3913 access_list,
3914 output: output.into_data(),
3915 })
3916 }
3917 Ok(ExecutionResult::Revert { gas_used, output }) => {
3918 evm.journaled_state.checkpoint_revert(checkpoint);
3919 Err(SimulationError::from_revert(gas_used, output).into())
3920 }
3921 Ok(ExecutionResult::Halt { reason, gas_used }) => {
3922 evm.journaled_state.checkpoint_revert(checkpoint);
3923 Err(SimError::Halt {
3924 reason: format!("{reason:?}"),
3925 gas_used,
3926 })
3927 }
3928 Err(err) => {
3929 evm.journaled_state.checkpoint_revert(checkpoint);
3930 Err(err)
3931 }
3932 }
3933 }
3934
3935 /// Simulate a call with transfer tracking without any prefetching.
3936 ///
3937 /// This is identical to `simulate_with_transfer_tracking` since we no longer
3938 /// do access list prefetching. Kept for API compatibility.
3939 pub fn simulate_with_transfer_tracking_raw(
3940 &mut self,
3941 from: Address,
3942 to: Address,
3943 calldata: Bytes,
3944 owner: Address,
3945 tokens: Option<impl IntoIterator<Item = Address>>,
3946 commit: bool,
3947 ) -> SimulationResult<CallSimulationResult> {
3948 self.simulate_with_transfer_tracking(from, to, calldata, owner, tokens, commit)
3949 }
3950
3951 /// Simulate an ordered transaction **bundle** over cumulative block state,
3952 /// with a revert policy and coinbase/miner-payment accounting (Phase 6
3953 /// Track A+B).
3954 ///
3955 /// This is a convenience wrapper: it snapshots the cache and runs the bundle
3956 /// on a fresh transient [`EvmOverlay`] via
3957 /// [`EvmOverlay::simulate_bundle`](crate::cache::EvmOverlay::simulate_bundle),
3958 /// which carries the full semantics (ordered cumulative state, the
3959 /// [`RevertPolicy`](crate::bundle::RevertPolicy), and coinbase accounting).
3960 ///
3961 /// The cache itself is **never** mutated — even when `opts.commit` is `true`.
3962 /// `commit` controls only whether the bundle's cumulative state is folded
3963 /// into the transient overlay (and is therefore moot here, since that overlay
3964 /// is dropped when this call returns). Snapshot the cache yourself and drive
3965 /// [`EvmOverlay::simulate_bundle`] directly when you need the committed
3966 /// overlay state to outlive the call (e.g. to chain a follow-up read).
3967 ///
3968 /// # Errors
3969 ///
3970 /// Returns [`SimError`] if a transaction environment cannot be built or revm
3971 /// fails to transact. A transaction reverting is reported through the
3972 /// per-transaction outcome and the revert policy, not as an error.
3973 pub fn simulate_bundle(
3974 &mut self,
3975 txs: &[crate::bundle::BundleTx],
3976 opts: &crate::bundle::BundleOptions,
3977 ) -> SimulationResult<crate::bundle::BundleResult> {
3978 let snapshot = self.create_snapshot();
3979 let mut overlay = EvmOverlay::new(snapshot, None);
3980 overlay.simulate_bundle(txs, opts)
3981 }
3982
3983 /// Deploy a contract via CREATE transaction and return the deployed address.
3984 ///
3985 /// The `creation_code` should include the init code with ABI-encoded constructor
3986 /// arguments appended. Nonce checks are disabled, so any `from` address works.
3987 ///
3988 /// Note: This commits the deployment to the CacheDB. Use a throw-away deployer
3989 /// address (e.g., `Address::ZERO`) to avoid side effects on real accounts.
3990 ///
3991 /// # Errors
3992 /// Returns an error if the CREATE tx env cannot be built, if the deployment
3993 /// reverts or halts, or if it succeeds but the EVM returns no contract
3994 /// address.
3995 pub fn deploy_contract(&mut self, from: Address, creation_code: Bytes) -> Result<Address> {
3996 let tx = TxEnv::builder()
3997 .caller(from)
3998 .kind(TxKind::Create)
3999 .data(creation_code)
4000 .value(U256::ZERO)
4001 .build()
4002 .map_err(|e| anyhow!("Failed to build CREATE tx: {:?}", e))?;
4003
4004 // Use a relaxed contract size limit for deployment. Arbitrum supports
4005 // larger contracts than the EIP-170 24576-byte limit via ArbOS.
4006 let mut evm = self.build_evm();
4007 evm.cfg.limit_contract_code_size = Some(usize::MAX);
4008 let result = evm
4009 .transact_commit(tx)
4010 .map_err(|e| anyhow!("Contract deployment failed: {:?}", e))?;
4011
4012 match result {
4013 ExecutionResult::Success { output, .. } => output
4014 .address()
4015 .copied()
4016 .ok_or_else(|| anyhow!("Contract deployment succeeded but no address returned")),
4017 ExecutionResult::Revert { output, .. } => Err(anyhow!(
4018 "Contract deployment reverted: 0x{}",
4019 alloy_primitives::hex::encode(&output)
4020 )),
4021 ExecutionResult::Halt { reason, .. } => {
4022 Err(anyhow!("Contract deployment halted: {:?}", reason))
4023 }
4024 }
4025 }
4026
4027 /// Override the bytecode at `target` address with bytecode from `source` address.
4028 ///
4029 /// Copies only non-empty runtime code and code_hash; storage, balance, and nonce
4030 /// at `target` remain unchanged. `target` must already have non-empty runtime
4031 /// bytecode. Both the CacheDB overlay and BlockchainDb backend are updated,
4032 /// ensuring the override is visible to parallel EVM tasks sharing the same backend.
4033 ///
4034 /// # Errors
4035 /// Returns an error if `source` has no cached bytecode or its code is empty,
4036 /// if `target` cannot be loaded (it must already exist on the backend), or
4037 /// if `target` has no existing runtime bytecode to override. For synthetic
4038 /// `target` addresses that may not exist, use
4039 /// [`override_or_create_account_code`](Self::override_or_create_account_code).
4040 pub fn override_account_code(&mut self, source: Address, target: Address) -> Result<()> {
4041 self.override_account_code_with_missing_target(source, target, MissingTargetBehavior::Error)
4042 }
4043
4044 /// Override the bytecode at `target`, creating a default target account when absent.
4045 ///
4046 /// Use this for synthetic addresses in local simulations. For live forked
4047 /// accounts where storage/balance/nonce must be preserved, prefer
4048 /// [`Self::override_account_code`].
4049 pub fn override_or_create_account_code(
4050 &mut self,
4051 source: Address,
4052 target: Address,
4053 ) -> Result<()> {
4054 self.override_account_code_with_missing_target(
4055 source,
4056 target,
4057 MissingTargetBehavior::Create,
4058 )
4059 }
4060
4061 /// Override code at `target`, with explicit behavior for missing target accounts.
4062 ///
4063 /// This is intentionally **not** folded onto
4064 /// [`apply_update`](Self::apply_update)'s `Account` code patch: it copies code
4065 /// from a `source` account, preserves the target's existing balance/nonce/
4066 /// storage, and **unconditionally materializes** the target in the CacheDB
4067 /// overlay (the primary read path for EVM execution, required for the
4068 /// `Create` synthetic-target case). The generic primitive writes the overlay
4069 /// only when an account is already present, so the two are not
4070 /// behavior-equivalent. For a plain code overwrite that follows the
4071 /// dual-layer write-through policy, use
4072 /// `apply_update(StateUpdate::Account { patch: AccountPatch::default().code(..) })`.
4073 pub fn override_account_code_with_missing_target(
4074 &mut self,
4075 source: Address,
4076 target: Address,
4077 missing_target: MissingTargetBehavior,
4078 ) -> Result<()> {
4079 // Read deployed bytecode from source (in CacheDB overlay after deploy_contract)
4080 let source_code = self
4081 .db
4082 .cache
4083 .accounts
4084 .get(&source)
4085 .and_then(|a| a.info.code.clone())
4086 .ok_or_else(|| anyhow!("No bytecode found at source address {}", source))?;
4087 Self::ensure_runtime_code(source, Some(&source_code), "source")?;
4088
4089 let code_hash = source_code.hash_slow();
4090 debug!(
4091 source = %source,
4092 target = %target,
4093 code_size = source_code.len(),
4094 "Overriding account bytecode"
4095 );
4096
4097 let mut target_info = self.target_account_info(target, missing_target)?;
4098
4099 if matches!(missing_target, MissingTargetBehavior::Error) {
4100 Self::ensure_runtime_code(target, target_info.code.as_ref(), "target")?;
4101 }
4102
4103 target_info.code = Some(source_code);
4104 target_info.code_hash = code_hash;
4105
4106 // Update CacheDB overlay (primary read path for EVM execution).
4107 self.db.insert_account_info(target, target_info.clone());
4108
4109 // Update BlockchainDb backend (shared with parallel tasks)
4110 {
4111 let mut accounts = self.blockchain_db.accounts().write();
4112 accounts.insert(target, target_info);
4113 }
4114
4115 // Layer 2 changed → invalidate the memoized base for `target`. The layer-1
4116 // `insert_account_info` above currently shadows it on every snapshot read,
4117 // but we dirty unconditionally for uniformity with every other layer-2 write
4118 // site (D2), so base correctness never relies on that shadowing invariant.
4119 self.mark_base_dirty(target);
4120
4121 Ok(())
4122 }
4123
4124 pub(crate) fn require_contract_target(&self, target: Address) -> Result<()> {
4125 let target_info = self.target_account_info(target, MissingTargetBehavior::Error)?;
4126 Self::ensure_runtime_code(target, target_info.code.as_ref(), "target")
4127 }
4128
4129 fn target_account_info(
4130 &self,
4131 target: Address,
4132 missing_target: MissingTargetBehavior,
4133 ) -> Result<AccountInfo> {
4134 if let Some(account) = self.db.cache.accounts.get(&target) {
4135 // A NotExisting overlay account is absent to the EVM (revm
4136 // `DbAccount::info()` returns None); treat it as a missing target
4137 // rather than returning its stale/default info.
4138 if !matches!(account.account_state, AccountState::NotExisting) {
4139 return Ok(account.info.clone());
4140 }
4141 }
4142
4143 match missing_target {
4144 MissingTargetBehavior::Create => Ok(AccountInfo::default()),
4145 MissingTargetBehavior::Error => {
4146 use revm::database_interface::DatabaseRef;
4147 self.backend
4148 .basic_ref(target)
4149 .map_err(|e| anyhow!("Failed to fetch target account {}: {:?}", target, e))?
4150 .ok_or_else(|| {
4151 anyhow!(
4152 "Target account {} not found; use override_or_create_account_code for synthetic targets",
4153 target
4154 )
4155 })
4156 }
4157 }
4158 }
4159
4160 fn ensure_runtime_code(address: Address, code: Option<&Bytecode>, role: &str) -> Result<()> {
4161 if code.is_some_and(|code| !code.is_empty()) {
4162 return Ok(());
4163 }
4164
4165 Err(anyhow!(
4166 "{} account {} has no runtime bytecode",
4167 role,
4168 address
4169 ))
4170 }
4171}
4172
4173/// Read-only state view for the event pipeline (Pillar B.2): a decoder reads the
4174/// current cached value of a slot through [`cached_storage_value`](EvmCache::cached_storage_value),
4175/// which never touches RPC and is `account_state`-aware (a cold slot reads
4176/// `None`).
4177impl crate::events::StateView for EvmCache {
4178 fn storage(&self, address: Address, slot: U256) -> Option<U256> {
4179 self.cached_storage_value(address, slot)
4180 }
4181}
4182
4183impl EvmCache {
4184 /// Create a LocalContext that reuses the shared memory buffer.
4185 ///
4186 /// The buffer is cleared (length set to 0) but capacity is preserved,
4187 /// avoiding repeated allocations across simulations.
4188 fn make_local_context(&self) -> LocalContext {
4189 // Clear the buffer but preserve capacity. `Vec::clear` sets the length
4190 // to 0 without releasing the allocation, so the buffer is reused across
4191 // simulations.
4192 self.shared_memory_buffer.borrow_mut().clear();
4193
4194 LocalContext {
4195 shared_memory_buffer: self.shared_memory_buffer.clone(),
4196 precompile_error_message: None,
4197 }
4198 }
4199
4200 fn build_evm(&mut self) -> CacheEvm<'_> {
4201 let local = self.make_local_context();
4202 let chain_id = self.chain_id;
4203 let mut evm = Context::mainnet()
4204 .with_db(&mut self.db)
4205 .with_local(local)
4206 .modify_cfg_chained(|cfg| {
4207 cfg.disable_nonce_check = true;
4208 cfg.disable_eip3607 = true;
4209 cfg.disable_base_fee = true;
4210 cfg.disable_balance_check = true;
4211 cfg.chain_id = chain_id;
4212 cfg.limit_contract_code_size = None;
4213 cfg.tx_chain_id_check = false;
4214 cfg.spec = self.spec_id;
4215 })
4216 .build_mainnet();
4217
4218 let timestamp = self
4219 .timestamp_override
4220 .unwrap_or_else(|| unix_timestamp_secs_saturating(SystemTime::now()));
4221 evm.block.timestamp = U256::from(timestamp);
4222 if let Some(number) = self.block_number {
4223 evm.block.number = U256::from(number);
4224 }
4225 if let Some(basefee) = self.basefee {
4226 evm.block.basefee = basefee;
4227 }
4228 if let Some(coinbase) = self.coinbase {
4229 evm.block.beneficiary = coinbase;
4230 }
4231 if let Some(prevrandao) = self.prevrandao {
4232 evm.block.prevrandao = Some(prevrandao);
4233 }
4234 if let Some(gas_limit) = self.block_gas_limit {
4235 evm.block.gas_limit = gas_limit;
4236 }
4237 evm
4238 }
4239
4240 fn build_evm_with_inspector<INSP>(&mut self, inspector: INSP) -> InspectorCacheEvm<'_, INSP> {
4241 let local = self.make_local_context();
4242 let chain_id = self.chain_id;
4243 let mut evm = Context::mainnet()
4244 .with_db(&mut self.db)
4245 .with_local(local)
4246 .modify_cfg_chained(|cfg| {
4247 cfg.disable_nonce_check = true;
4248 cfg.disable_eip3607 = true;
4249 cfg.disable_base_fee = true;
4250 cfg.disable_balance_check = true;
4251 cfg.chain_id = chain_id;
4252 cfg.limit_contract_code_size = None;
4253 cfg.tx_chain_id_check = false;
4254 cfg.spec = self.spec_id;
4255 })
4256 .build_mainnet_with_inspector(inspector);
4257
4258 let timestamp = self
4259 .timestamp_override
4260 .unwrap_or_else(|| unix_timestamp_secs_saturating(SystemTime::now()));
4261 evm.block.timestamp = U256::from(timestamp);
4262 if let Some(number) = self.block_number {
4263 evm.block.number = U256::from(number);
4264 }
4265 if let Some(basefee) = self.basefee {
4266 evm.block.basefee = basefee;
4267 }
4268 if let Some(coinbase) = self.coinbase {
4269 evm.block.beneficiary = coinbase;
4270 }
4271 if let Some(prevrandao) = self.prevrandao {
4272 evm.block.prevrandao = Some(prevrandao);
4273 }
4274 if let Some(gas_limit) = self.block_gas_limit {
4275 evm.block.gas_limit = gas_limit;
4276 }
4277 evm
4278 }
4279
4280 fn build_tx_env(from: Address, to: Address, calldata: Bytes) -> Result<TxEnv> {
4281 Self::build_tx_env_with(from, to, calldata, &TxConfig::default())
4282 }
4283
4284 fn build_tx_env_with(
4285 from: Address,
4286 to: Address,
4287 calldata: Bytes,
4288 tx: &TxConfig,
4289 ) -> Result<TxEnv> {
4290 let mut builder = TxEnv::builder()
4291 .caller(from)
4292 .kind(TxKind::Call(to))
4293 .data(calldata)
4294 .value(tx.value);
4295 if let Some(gas_limit) = tx.gas_limit {
4296 builder = builder.gas_limit(gas_limit);
4297 }
4298 if let Some(gas_price) = tx.gas_price {
4299 builder = builder.gas_price(gas_price);
4300 }
4301 if let Some(nonce) = tx.nonce {
4302 builder = builder.nonce(nonce);
4303 }
4304 if let Some(access_list) = &tx.access_list {
4305 builder = builder.access_list(access_list.clone());
4306 }
4307 builder
4308 .build()
4309 .map_err(|e| anyhow!("Failed to build tx env: {:?}", e))
4310 }
4311
4312 fn call_sol_with_commit<C>(
4313 &mut self,
4314 from: Address,
4315 to: Address,
4316 call: C,
4317 tx: &TxConfig,
4318 commit: bool,
4319 ) -> Result<C::Return>
4320 where
4321 C: SolCall,
4322 {
4323 let calldata = Bytes::from(call.abi_encode());
4324 let result = self
4325 .call_raw_with(from, to, calldata, commit, tx)
4326 .with_context(|| {
4327 format!(
4328 "failed to execute Solidity call {} from {from:?} to {to:?}",
4329 C::SIGNATURE
4330 )
4331 })?;
4332 Self::decode_sol_call_result::<C>(from, to, result)
4333 }
4334
4335 fn decode_sol_call_result<C>(
4336 from: Address,
4337 to: Address,
4338 result: ExecutionResult,
4339 ) -> Result<C::Return>
4340 where
4341 C: SolCall,
4342 {
4343 match result {
4344 ExecutionResult::Success { output, .. } => {
4345 let output = output.into_data();
4346 C::abi_decode_returns(&output).map_err(|error| {
4347 anyhow!(
4348 "failed to decode Solidity call {} return data from {from:?} to {to:?}: output_len={}, error: {:?}",
4349 C::SIGNATURE,
4350 output.len(),
4351 error
4352 )
4353 })
4354 }
4355 other => Err(anyhow!(
4356 "Solidity call {} from {from:?} to {to:?} did not succeed: {:?}",
4357 C::SIGNATURE,
4358 other
4359 )),
4360 }
4361 }
4362
4363 fn erc20_balance_of_in_evm(
4364 evm: &mut CacheEvm<'_>,
4365 caller: Address,
4366 token: Address,
4367 owner: Address,
4368 ) -> Result<U256> {
4369 let call = IERC20::balanceOfCall { target: owner };
4370 let tx = Self::build_tx_env(caller, token, Bytes::from(call.abi_encode()))?;
4371 let result = evm
4372 .transact_one(tx)
4373 .map_err(|e| anyhow!("Failed to transact: {:?}", e))?;
4374
4375 match result {
4376 ExecutionResult::Success { output, .. } => {
4377 let out = output.into_data();
4378 let balance = IERC20::balanceOfCall::abi_decode_returns(&out)
4379 .map_err(|e| anyhow!("Failed to decode balanceOf: {:?}", e))?;
4380 Ok(balance)
4381 }
4382 _ => Err(anyhow!("balanceOf call failed: {:?}", result)),
4383 }
4384 }
4385
4386 fn erc20_balance_of_in_evm_isolated(
4387 evm: &mut CacheEvm<'_>,
4388 caller: Address,
4389 token: Address,
4390 owner: Address,
4391 ) -> Result<(U256, AccessList)> {
4392 let state_before = evm.journaled_state.state.clone();
4393 let checkpoint = evm.journaled_state.checkpoint();
4394 let result = Self::erc20_balance_of_in_evm(evm, caller, token, owner);
4395 let access_list = extract_access_list(&evm.journaled_state.state);
4396 evm.journaled_state.checkpoint_revert(checkpoint);
4397 evm.journaled_state.state = state_before;
4398 result.map(|balance| (balance, access_list))
4399 }
4400
4401 fn seed_synthetic_beneficiary(evm: &mut CacheEvm<'_>) -> Option<Address> {
4402 let beneficiary = evm.block.beneficiary;
4403 if evm.journaled_state.state.contains_key(&beneficiary) {
4404 return None;
4405 }
4406 evm.journaled_state
4407 .state
4408 .insert(beneficiary, Account::from(AccountInfo::default()));
4409 Some(beneficiary)
4410 }
4411
4412 fn remove_synthetic_beneficiary(evm: &mut CacheEvm<'_>, beneficiary: Option<Address>) {
4413 if let Some(beneficiary) = beneficiary {
4414 evm.journaled_state.state.remove(&beneficiary);
4415 }
4416 }
4417}
4418
4419/// A session for executing multiple EVM operations without committing to the underlying DB.
4420///
4421/// Changes made within a session are tracked in the EVM's journaled state. Call `commit()` to
4422/// persist changes to the underlying database, or simply drop the session to discard
4423/// all changes.
4424///
4425/// Note: For snapshot/restore functionality across multiple transactions, use `EvmCache::snapshot()`
4426/// and `EvmCache::restore()` instead, as the EVM journal is cleared after each transaction.
4427pub struct EvmSession<'a> {
4428 evm: CacheEvm<'a>,
4429}
4430
4431impl<'a> EvmSession<'a> {
4432 /// Execute a call within the session.
4433 ///
4434 /// If `commit` is true, changes are persisted to the session's journaled state.
4435 /// If `commit` is false, the call is executed but its effects are immediately reverted.
4436 ///
4437 /// Note: Changes are not persisted to the underlying CacheDB until `commit()` is called
4438 /// on the session itself.
4439 pub fn call_raw(
4440 &mut self,
4441 from: Address,
4442 to: Address,
4443 calldata: Bytes,
4444 commit: bool,
4445 ) -> Result<ExecutionResult> {
4446 let tx = EvmCache::build_tx_env(from, to, calldata)?;
4447
4448 if commit {
4449 self.evm
4450 .transact_one(tx)
4451 .map_err(|e| anyhow!("Failed to transact: {:?}", e))
4452 } else {
4453 let checkpoint = self.evm.journaled_state.checkpoint();
4454 let result = self.evm.transact_one(tx);
4455 self.evm.journaled_state.checkpoint_revert(checkpoint);
4456 result.map_err(|e| anyhow!("Failed to transact: {:?}", e))
4457 }
4458 }
4459
4460 /// Commit all session changes to the underlying database.
4461 ///
4462 /// This persists all changes made during the session to the CacheDB.
4463 pub fn commit(mut self) {
4464 self.evm.commit_inner();
4465 }
4466
4467 /// Get access to the underlying EVM for advanced operations.
4468 ///
4469 /// This exposes revm internals and bypasses the cache's two-layer
4470 /// consistency model: state mutated directly through the journaled EVM
4471 /// lands in the session's journal, not the BlockchainDb backend, and is
4472 /// only flushed to the CacheDB overlay on [`commit`](Self::commit). Use
4473 /// with care.
4474 pub fn evm(&mut self) -> &mut CacheEvm<'a> {
4475 &mut self.evm
4476 }
4477}
4478
4479/// Automatically flush the cache to disk when the EvmCache is dropped.
4480impl Drop for EvmCache {
4481 fn drop(&mut self) {
4482 if self.cache_config.is_some() {
4483 debug!("Flushing EVM cache on drop");
4484 if let Err(e) = self.flush() {
4485 warn!(error = %e, "Failed to flush EVM cache on drop");
4486 }
4487 }
4488 }
4489}
4490
4491#[cfg(test)]
4492mod shared_memory_capacity_tests {
4493 use super::SharedMemoryCapacity as Cap;
4494
4495 #[test]
4496 fn default_is_fixed_64k() {
4497 assert_eq!(Cap::default(), Cap::Fixed(64 * 1024));
4498 }
4499
4500 #[test]
4501 fn fixed_ignores_loaded_slots() {
4502 assert_eq!(Cap::Fixed(8_192).resolve(10_000_000), 8_192);
4503 assert_eq!(Cap::Fixed(0).resolve(123), 0);
4504 }
4505
4506 #[test]
4507 fn auto_floors_clamps_and_scales() {
4508 // Nothing / little loaded → floor.
4509 assert_eq!(Cap::Auto.resolve(0), Cap::MIN_AUTO);
4510 assert_eq!(Cap::Auto.resolve(1_000), Cap::MIN_AUTO); // 16 KiB < 64 KiB floor
4511 // Linear region (16 bytes/slot).
4512 assert_eq!(Cap::Auto.resolve(10_000), 160_000);
4513 assert_eq!(Cap::Auto.resolve(100_000), 1_600_000);
4514 // Ceiling.
4515 assert_eq!(Cap::Auto.resolve(usize::MAX), Cap::MAX_AUTO);
4516 assert_eq!(Cap::Auto.resolve(262_144), Cap::MAX_AUTO); // 262_144 * 16 == 4 MiB
4517 }
4518}
4519
4520/// Tests that exercise the generic cache engine.
4521#[cfg(test)]
4522mod core_tests {
4523 use super::*;
4524
4525 #[test]
4526 fn test_address_to_u256_conversion() {
4527 // Test that address conversion preserves the address bytes correctly
4528 let addr = Address::repeat_byte(0xAB);
4529 let value = U256::from_be_slice(addr.as_slice());
4530
4531 // Address is 20 bytes, should be right-aligned in U256 (32 bytes)
4532 let bytes = value.to_be_bytes::<32>();
4533
4534 // First 12 bytes should be zero (padding)
4535 assert_eq!(&bytes[..12], &[0u8; 12]);
4536
4537 // Last 20 bytes should be the address
4538 assert_eq!(&bytes[12..], addr.as_slice());
4539 }
4540
4541 // ==================== block context tests ====================
4542
4543 #[test]
4544 fn new_defaults_to_latest_block_pin() {
4545 use alloy_provider::RootProvider;
4546 use alloy_rpc_client::RpcClient;
4547 use alloy_transport::mock::Asserter;
4548
4549 let asserter = Asserter::new();
4550 let client = RpcClient::mocked(asserter);
4551 let provider = RootProvider::<AnyNetwork>::new(client);
4552
4553 let rt = tokio::runtime::Builder::new_current_thread()
4554 .enable_all()
4555 .build()
4556 .unwrap();
4557
4558 let cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4559
4560 assert_eq!(
4561 cache.block(),
4562 BlockId::latest(),
4563 "a default cache must carry an explicit latest block pin, not None"
4564 );
4565 }
4566
4567 #[test]
4568 fn test_set_block_context_stores_values() {
4569 use alloy_provider::RootProvider;
4570 use alloy_rpc_client::RpcClient;
4571 use alloy_transport::mock::Asserter;
4572
4573 let asserter = Asserter::new();
4574 let client = RpcClient::mocked(asserter);
4575 let provider = RootProvider::<AnyNetwork>::new(client);
4576
4577 let rt = tokio::runtime::Builder::new_current_thread()
4578 .enable_all()
4579 .build()
4580 .unwrap();
4581
4582 let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4583
4584 // Initially None
4585 assert_eq!(cache.block_number(), None);
4586 assert_eq!(cache.basefee(), None);
4587
4588 // Set values
4589 cache.set_block_context(Some(148_252_680), Some(50));
4590 assert_eq!(cache.block_number(), Some(148_252_680));
4591 assert_eq!(cache.basefee(), Some(50));
4592
4593 // Clear values
4594 cache.set_block_context(None, None);
4595 assert_eq!(cache.block_number(), None);
4596 assert_eq!(cache.basefee(), None);
4597 }
4598
4599 #[test]
4600 fn set_block_latest_clears_stale_block_context() {
4601 use alloy_provider::RootProvider;
4602 use alloy_rpc_client::RpcClient;
4603 use alloy_transport::mock::Asserter;
4604
4605 let asserter = Asserter::new();
4606 let client = RpcClient::mocked(asserter);
4607 let provider = RootProvider::<AnyNetwork>::new(client);
4608
4609 let rt = tokio::runtime::Builder::new_current_thread()
4610 .enable_all()
4611 .build()
4612 .unwrap();
4613
4614 let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4615 cache.set_block_context(Some(148_252_680), Some(50));
4616
4617 cache.set_block(BlockId::latest());
4618
4619 assert_eq!(
4620 cache.block_number(),
4621 None,
4622 "tag pins must not retain a stale NUMBER context"
4623 );
4624 assert_eq!(
4625 cache.basefee(),
4626 None,
4627 "set_block cannot refresh BASEFEE synchronously, so it must clear stale values"
4628 );
4629 }
4630
4631 #[test]
4632 fn set_block_latest_clears_stale_context_even_when_pin_unchanged() {
4633 use alloy_provider::RootProvider;
4634 use alloy_rpc_client::RpcClient;
4635 use alloy_transport::mock::Asserter;
4636
4637 let asserter = Asserter::new();
4638 let client = RpcClient::mocked(asserter);
4639 let provider = RootProvider::<AnyNetwork>::new(client);
4640
4641 let rt = tokio::runtime::Builder::new_current_thread()
4642 .enable_all()
4643 .build()
4644 .unwrap();
4645
4646 let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4647 cache.set_block_context(Some(148_252_680), Some(50));
4648
4649 cache.set_block(BlockId::latest());
4650
4651 assert_eq!(
4652 cache.block_number(),
4653 None,
4654 "latest pins must not retain a stale NUMBER context"
4655 );
4656 assert_eq!(
4657 cache.basefee(),
4658 None,
4659 "latest pins can drift like tags, so stale BASEFEE must be cleared"
4660 );
4661 }
4662
4663 #[test]
4664 fn set_block_number_sets_number_and_clears_stale_basefee() {
4665 use alloy_provider::RootProvider;
4666 use alloy_rpc_client::RpcClient;
4667 use alloy_transport::mock::Asserter;
4668
4669 let asserter = Asserter::new();
4670 let client = RpcClient::mocked(asserter);
4671 let provider = RootProvider::<AnyNetwork>::new(client);
4672
4673 let rt = tokio::runtime::Builder::new_current_thread()
4674 .enable_all()
4675 .build()
4676 .unwrap();
4677
4678 let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4679 cache.set_block_context(Some(100), Some(50));
4680
4681 cache.set_block(BlockId::Number(BlockNumberOrTag::Number(200)));
4682
4683 assert_eq!(cache.block_number(), Some(200));
4684 assert_eq!(
4685 cache.basefee(),
4686 None,
4687 "set_block cannot refresh BASEFEE synchronously, so it must clear stale values"
4688 );
4689 }
4690
4691 #[test]
4692 fn repin_to_block_clears_stale_basefee() {
4693 use alloy_provider::RootProvider;
4694 use alloy_rpc_client::RpcClient;
4695 use alloy_transport::mock::Asserter;
4696
4697 let asserter = Asserter::new();
4698 let client = RpcClient::mocked(asserter);
4699 let provider = RootProvider::<AnyNetwork>::new(client);
4700
4701 let rt = tokio::runtime::Builder::new_current_thread()
4702 .enable_all()
4703 .build()
4704 .unwrap();
4705
4706 let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4707 cache.set_block_context(Some(100), Some(50));
4708
4709 cache.repin_to_block(200);
4710
4711 assert_eq!(cache.block_number(), Some(200));
4712 assert_eq!(
4713 cache.basefee(),
4714 None,
4715 "repin_to_block must not carry stale BASEFEE across blocks"
4716 );
4717 }
4718
4719 #[test]
4720 fn test_build_evm_applies_block_context() {
4721 use alloy_provider::RootProvider;
4722 use alloy_rpc_client::RpcClient;
4723 use alloy_transport::mock::Asserter;
4724
4725 let asserter = Asserter::new();
4726 let client = RpcClient::mocked(asserter);
4727 let provider = RootProvider::<AnyNetwork>::new(client);
4728
4729 let rt = tokio::runtime::Builder::new_current_thread()
4730 .enable_all()
4731 .build()
4732 .unwrap();
4733
4734 let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
4735
4736 let block_num = 148_252_680u64;
4737 let basefee_val = 50u64;
4738 let coinbase = Address::repeat_byte(0xC0);
4739 let prevrandao = B256::repeat_byte(0x77);
4740 let gas_limit = 30_000_000u64;
4741 cache.set_block_context(Some(block_num), Some(basefee_val));
4742 cache.set_coinbase(Some(coinbase));
4743 cache.set_prevrandao(Some(prevrandao));
4744 cache.set_block_gas_limit(Some(gas_limit));
4745
4746 let evm = cache.build_evm();
4747 assert_eq!(evm.block.number, U256::from(block_num));
4748 assert_eq!(evm.block.basefee, basefee_val);
4749 assert_eq!(evm.block.beneficiary, coinbase);
4750 assert_eq!(evm.block.prevrandao, Some(prevrandao));
4751 assert_eq!(evm.block.gas_limit, gas_limit);
4752 }
4753
4754 #[test]
4755 fn test_from_backend_propagates_block_context() {
4756 use alloy_provider::RootProvider;
4757 use alloy_rpc_client::RpcClient;
4758 use alloy_transport::mock::Asserter;
4759
4760 let asserter = Asserter::new();
4761 let client = RpcClient::mocked(asserter);
4762 let provider = RootProvider::<AnyNetwork>::new(client);
4763
4764 let rt = tokio::runtime::Builder::new_current_thread()
4765 .enable_all()
4766 .build()
4767 .unwrap();
4768
4769 let parent = rt.block_on(EvmCache::new(Arc::new(provider)));
4770
4771 let block_num = Some(148_252_680u64);
4772 let basefee_val = Some(50u64);
4773 let child = EvmCache::from_backend(
4774 parent.unchecked_backend().clone(),
4775 parent.unchecked_blockchain_db().clone(),
4776 parent.block(),
4777 42161,
4778 block_num,
4779 basefee_val,
4780 SpecId::CANCUN,
4781 );
4782
4783 assert_eq!(child.block_number(), block_num);
4784 assert_eq!(child.basefee(), basefee_val);
4785 }
4786
4787 #[test]
4788 fn unix_timestamp_secs_saturating_handles_pre_epoch() {
4789 let before_epoch = std::time::UNIX_EPOCH - std::time::Duration::from_secs(5);
4790 assert_eq!(
4791 unix_timestamp_secs_saturating(before_epoch),
4792 0,
4793 "pre-epoch system times must saturate instead of panicking"
4794 );
4795 }
4796}