Skip to main content

evm_fork_cache/cache/
snapshot.rs

1//! Immutable, shareable EVM state snapshots.
2//!
3//! # Two-tier copy-on-write model (Pillar A)
4//!
5//! A snapshot is split into two tiers:
6//!
7//! - a **memoized immutable base** (`BaseState`) flattening the *cold* layer-2
8//!   `BlockchainDb` index, shared across successive snapshots by `Arc` — both the
9//!   base as a whole and each account's storage map (`Arc<HashMap<U256, U256>>`) —
10//!   so taking a snapshot when the cold index is unchanged is an `Arc` handle
11//!   copy, never a per-slot deep copy;
12//! - a small per-snapshot **overlay** folding the *hot* layer-1 CacheDB delta
13//!   (committed sim changes, write-throughs, freshness corrections), which always
14//!   shadows the base on a read.
15//!
16//! [`super::EvmCache::snapshot`] memoizes the base (via the internal
17//! `refresh_base`) and folds only layer 1 fresh, so its cost tracks *changed*
18//! state, not *total* state. The retained
19//! [`super::EvmCache::snapshot_deep_clone`] produces the same two-tier
20//! shape with everything flattened into the base and empty overlay maps; it is the
21//! A/B benchmark baseline and the read-equivalence reference.
22//!
23//! Reads stay O(1) `HashMap` lookups with no locks (Decision D1: `Arc` sharing,
24//! not a persistent/HAMT map), so the snapshot is `Send + Sync` and an
25//! [`EvmOverlay`] built from it is `Send`.
26//!
27//! # Per-simulation dirty layer
28//!
29//! Each simulation does not mutate the shared snapshot. Instead it wraps the
30//! `Arc<EvmSnapshot>` in an [`EvmOverlay`], which adds a per-simulation
31//! *dirty layer* on top: writes (committed account/storage changes, RPC
32//! fallbacks, freshness overrides) land in the overlay's own maps and take
33//! precedence over the snapshot on subsequent reads. Two overlays built from
34//! the same `Arc<EvmSnapshot>` are fully isolated from one another, so
35//! simulations can run in parallel without contending for or corrupting the
36//! shared base state.
37//!
38//! [`EvmOverlay`]: super::EvmOverlay
39
40use std::collections::{HashMap, HashSet};
41use std::sync::Arc;
42
43use alloy_primitives::{Address, B256, U256};
44use revm::primitives::hardfork::SpecId;
45use revm::state::{AccountInfo, Bytecode};
46
47use crate::access_set::StorageAccessList;
48
49/// Memoized, immutable flatten of the **cold layer-2** index (Pillar A).
50///
51/// Holds layer-2 (`BlockchainDb`) account info and storage only; the layer-1
52/// `StorageCleared` / `NotExisting` classification is purely a layer-1 property
53/// and lives on [`EvmSnapshot`], not here (see the read rules on
54/// [`EvmSnapshot::storage_value`]). Each account's storage is wrapped in an `Arc`
55/// so that rebuilding the base on a partial change (copy-on-write) shares the
56/// `Arc` handles of unchanged accounts instead of deep-copying their slots.
57///
58/// Built and memoized by [`EvmCache::refresh_base`](super::EvmCache::refresh_base);
59/// shared across snapshots and across threads via `Arc<BaseState>`.
60pub(crate) struct BaseState {
61    /// Layer-2 account info, by address. (Layer 2 has no `NotExisting` concept;
62    /// that classification is purely a layer-1 property — see [`EvmSnapshot`].)
63    pub(crate) accounts: HashMap<Address, AccountInfo>,
64    /// Layer-2 storage, per account, **shared by `Arc`** so cloning a base — or
65    /// rebuilding it for an unchanged account — is a handle copy, never a per-slot
66    /// copy.
67    pub(crate) storage: HashMap<Address, Arc<HashMap<U256, U256>>>,
68    /// Bytecode by `code_hash`, derived from `accounts` at build time.
69    pub(crate) code_by_hash: HashMap<B256, Bytecode>,
70}
71
72/// Immutable EVM state snapshot — `Send + Sync`, shared via `Arc` across threads.
73///
74/// A two-tier copy-on-write view (see the [module docs](self)): an `Arc`-shared,
75/// memoized cold base (layer 2) plus a small per-snapshot overlay folding the hot
76/// layer-1 CacheDB delta, which shadows the base on reads. Lookups (including the
77/// public [`storage_value`](Self::storage_value)) are O(1) and lock-free, and
78/// reproduce the live cache's layered semantics bit-for-bit.
79///
80/// Created via [`super::EvmCache::snapshot()`]. Each parallel simulation
81/// task gets its own [`super::EvmOverlay`] backed by a cheap `Arc::clone` of
82/// the snapshot.
83pub struct EvmSnapshot {
84    /// Memoized, `Arc`-shared cold layer-2 base.
85    pub(crate) base: Arc<BaseState>,
86    /// Layer-1 accounts that are present to the EVM (`NotExisting` excluded).
87    /// Shadows [`BaseState::accounts`] on a read.
88    pub(crate) overlay_accounts: HashMap<Address, AccountInfo>,
89    /// Layer-1 storage delta, per account. A cleared account (revm
90    /// `StorageCleared` / `NotExisting`) ALWAYS has an entry here (possibly empty)
91    /// so the cleared rule is decided without consulting the base.
92    pub(crate) overlay_storage: HashMap<Address, HashMap<U256, U256>>,
93    /// Bytecode introduced by layer 1 (checked before [`BaseState::code_by_hash`]).
94    pub(crate) overlay_code_by_hash: HashMap<B256, Bytecode>,
95    /// Accounts whose storage is locally complete (revm `StorageCleared` /
96    /// `NotExisting`): a slot absent from `overlay_storage` for such an account
97    /// reads as ZERO and must NOT fall through to the base or an `ext_db`,
98    /// mirroring the live EVM SLOAD and
99    /// [`EvmCache::cached_storage_value`](super::EvmCache::cached_storage_value).
100    pub(crate) storage_cleared: HashSet<Address>,
101    /// Accounts that are absent to the EVM (revm `NotExisting`):
102    /// [`account_info`](Self::account_info) returns `None` for them and must NOT
103    /// fall through to the base or an `ext_db`, mirroring revm `DbAccount::info()`
104    /// and [`EvmCache`](super::EvmCache)'s live account read. These addresses are
105    /// excluded from `overlay_accounts` / `overlay_code_by_hash`.
106    pub(crate) accounts_not_existing: HashSet<Address>,
107    pub(crate) block_hashes: HashMap<u64, B256>,
108    /// Hash-pinned block identity captured from the cache's `BlockId`.
109    ///
110    /// This is deliberately separate from `block_hashes`: EVM `BLOCKHASH`
111    /// cannot return the current block's hash, while callers that attest an
112    /// immutable snapshot lineage still need to bind the snapshot to the
113    /// current canonical block.
114    pub(crate) block_context_hash: Option<B256>,
115    // Block context
116    pub(crate) block_number: Option<u64>,
117    pub(crate) basefee: Option<u64>,
118    pub(crate) coinbase: Option<Address>,
119    pub(crate) prevrandao: Option<B256>,
120    pub(crate) gas_limit: Option<u64>,
121    pub(crate) chain_id: u64,
122    pub(crate) timestamp: Option<u64>,
123    pub(crate) spec_id: SpecId,
124    /// Per-context EVM shared-memory pre-allocation (bytes) copied from the
125    /// [`EvmCache`](super::EvmCache) at snapshot time, so an [`EvmOverlay`] built
126    /// from this snapshot pre-allocates the same working-memory size the live cache
127    /// was configured with (see
128    /// [`SharedMemoryCapacity`](super::SharedMemoryCapacity)).
129    pub(crate) shared_memory_capacity: usize,
130}
131
132impl EvmSnapshot {
133    /// Chain ID captured by this immutable simulation snapshot.
134    pub const fn chain_id(&self) -> u64 {
135        self.chain_id
136    }
137
138    /// Block number installed in the snapshot's EVM context.
139    pub const fn block_number(&self) -> Option<u64> {
140        self.block_number
141    }
142
143    /// Return the exact `BLOCKHASH` value resident for `number` when one was
144    /// captured by this immutable snapshot.
145    ///
146    /// This lookup is provider-free and never infers a hash from the snapshot's
147    /// EVM block context. In particular, [`block_number`](Self::block_number)
148    /// being `Some(number)` does not make that number's hash resident; callers
149    /// receive `None` unless the cache held an explicit block-hash entry when the
150    /// snapshot was created.
151    pub fn block_hash(&self, number: u64) -> Option<B256> {
152        self.block_hashes.get(&number).copied()
153    }
154
155    /// Return the hash-pinned identity of the snapshot's current block context.
156    ///
157    /// This is `Some` only when the source cache was pinned with
158    /// `BlockId::Hash`; number/tag-pinned snapshots return `None`. It is not an
159    /// EVM `BLOCKHASH` value and is therefore kept separate from
160    /// [`block_hash`](Self::block_hash).
161    pub const fn block_context_hash(&self) -> Option<B256> {
162        self.block_context_hash
163    }
164
165    /// Base fee installed in the snapshot's EVM context.
166    pub const fn basefee(&self) -> Option<u64> {
167        self.basefee
168    }
169
170    /// Block beneficiary installed in the snapshot's EVM context.
171    pub const fn coinbase(&self) -> Option<Address> {
172        self.coinbase
173    }
174
175    /// PREVRANDAO value installed in the snapshot's EVM context.
176    pub const fn prevrandao(&self) -> Option<B256> {
177        self.prevrandao
178    }
179
180    /// Block gas limit installed in the snapshot's EVM context.
181    pub const fn gas_limit(&self) -> Option<u64> {
182        self.gas_limit
183    }
184
185    /// Timestamp installed in the snapshot's EVM context.
186    pub const fn timestamp(&self) -> Option<u64> {
187        self.timestamp
188    }
189
190    /// Enumerate account, code, explicit storage, and block-hash entries held by
191    /// this immutable snapshot.
192    ///
193    /// Accounts already proven absent are included because their `None` result
194    /// is locally authoritative. Storage entries implied to be zero by a
195    /// `StorageCleared` account are not enumerable; use
196    /// [`missing_read_set`](Self::missing_read_set) when checking a concrete
197    /// required set.
198    pub fn resident_read_set(&self) -> StorageAccessList {
199        let mut resident = StorageAccessList::default();
200        resident.accounts.extend(self.base.accounts.keys().copied());
201        resident
202            .accounts
203            .extend(self.overlay_accounts.keys().copied());
204        resident
205            .accounts
206            .extend(self.accounts_not_existing.iter().copied());
207        resident
208            .code_hashes
209            .extend(self.base.code_by_hash.keys().copied());
210        resident
211            .code_hashes
212            .extend(self.overlay_code_by_hash.keys().copied());
213        for (address, slots) in &self.base.storage {
214            resident
215                .slots
216                .extend(slots.keys().copied().map(|slot| (*address, slot)));
217        }
218        for (address, slots) in &self.overlay_storage {
219            resident
220                .slots
221                .extend(slots.keys().copied().map(|slot| (*address, slot)));
222        }
223        resident
224            .block_numbers
225            .extend(self.block_hashes.keys().copied());
226        resident
227    }
228
229    /// Return the concrete subset of `required` this snapshot cannot resolve
230    /// without an external database.
231    pub fn missing_read_set(&self, required: &StorageAccessList) -> StorageAccessList {
232        StorageAccessList {
233            accounts: required
234                .accounts
235                .iter()
236                .copied()
237                .filter(|address| {
238                    !self.accounts_not_existing.contains(address)
239                        && self.account_info(*address).is_none()
240                })
241                .collect(),
242            code_hashes: required
243                .code_hashes
244                .iter()
245                .copied()
246                .filter(|hash| self.code(*hash).is_none())
247                .collect(),
248            slots: required
249                .slots
250                .iter()
251                .copied()
252                .filter(|(address, slot)| self.storage_value(*address, *slot).is_none())
253                .collect(),
254            block_numbers: required
255                .block_numbers
256                .iter()
257                .copied()
258                .filter(|number| !self.block_hashes.contains_key(number))
259                .collect(),
260        }
261    }
262
263    /// Account info as the EVM sees it: overlay (layer 1) wins, else the base
264    /// (layer 2), else `None`.
265    ///
266    /// Returns `None` for a `NotExisting` account without consulting the base,
267    /// mirroring revm `DbAccount::info()` and the live `EvmCache` account read.
268    pub(crate) fn account_info(&self, address: Address) -> Option<&AccountInfo> {
269        if self.accounts_not_existing.contains(&address) {
270            return None;
271        }
272        self.overlay_accounts
273            .get(&address)
274            .or_else(|| self.base.accounts.get(&address))
275    }
276
277    /// Return the snapshot's value for a storage slot, mirroring the live read.
278    ///
279    /// Used by the freshness validator to compare a freshly-fetched value against
280    /// the value the snapshot was built from. Resolution matches
281    /// [`EvmCache::cached_storage_value`](super::EvmCache::cached_storage_value)
282    /// over the two tiers: an overlay (layer-1) slot wins; for a cleared account
283    /// an absent overlay slot returns `Some(ZERO)` (its storage is locally
284    /// complete — the base is never consulted); otherwise the base (layer-2) slot
285    /// is returned, or `None` if neither tier has seen the slot.
286    pub fn storage_value(&self, address: Address, slot: U256) -> Option<U256> {
287        if let Some(account_storage) = self.overlay_storage.get(&address) {
288            if let Some(value) = account_storage.get(&slot) {
289                return Some(*value);
290            }
291            // A StorageCleared / NotExisting account's storage is locally complete:
292            // an absent slot reads ZERO and never falls through to the base.
293            if self.storage_cleared.contains(&address) {
294                return Some(U256::ZERO);
295            }
296            // Non-cleared overlay account: fall through to the base below.
297        }
298        self.base
299            .storage
300            .get(&address)
301            .and_then(|s| s.get(&slot).copied())
302    }
303
304    /// Return the runtime-code hash resident for `address` in this snapshot.
305    ///
306    /// The lookup follows the same account-shadowing and known-absent rules as
307    /// EVM account reads. It is provider-free and is intended for callers that
308    /// bind offline evaluation to a reviewed deployed runtime identity.
309    pub fn account_code_hash(&self, address: Address) -> Option<B256> {
310        self.account_info(address).map(|info| info.code_hash)
311    }
312
313    /// Bytecode by `code_hash`: overlay (layer 1) wins, else the base (layer 2).
314    pub(crate) fn code(&self, code_hash: B256) -> Option<&Bytecode> {
315        self.overlay_code_by_hash
316            .get(&code_hash)
317            .or_else(|| self.base.code_by_hash.get(&code_hash))
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    /// Build an empty `Arc<BaseState>` for snapshot literals in tests.
326    fn empty_base() -> Arc<BaseState> {
327        Arc::new(BaseState {
328            accounts: HashMap::new(),
329            storage: HashMap::new(),
330            code_by_hash: HashMap::new(),
331        })
332    }
333
334    #[test]
335    fn test_snapshot_is_send_sync() {
336        fn assert_send_sync<T: Send + Sync>() {}
337        assert_send_sync::<EvmSnapshot>();
338        assert_send_sync::<Arc<EvmSnapshot>>();
339    }
340
341    #[test]
342    fn test_empty_snapshot() {
343        let snap = EvmSnapshot {
344            base: empty_base(),
345            overlay_accounts: HashMap::new(),
346            overlay_storage: HashMap::new(),
347            overlay_code_by_hash: HashMap::new(),
348            storage_cleared: HashSet::new(),
349            accounts_not_existing: HashSet::new(),
350            block_hashes: HashMap::new(),
351            block_context_hash: None,
352            block_number: Some(100),
353            basefee: Some(1000),
354            coinbase: None,
355            prevrandao: None,
356            gas_limit: None,
357            chain_id: 42161,
358            timestamp: None,
359            spec_id: SpecId::CANCUN,
360            shared_memory_capacity: 64_000,
361        };
362        assert_eq!(snap.chain_id, 42161);
363        assert_eq!(snap.block_number, Some(100));
364    }
365}