evm_fork_cache/freshness.rs
1//! Freshness control plane and the optimistic verify-and-rerun execution loop.
2//!
3//! This module is the generic core of the engine's "honest freshness" model: it
4//! knows which cached state it can trust, for how long, and how to keep the rest
5//! correct without blocking simulations on RPC. It is built from four layers:
6//!
7//! 1. **Classification** — [`Validity`] (`Pinned` / `Volatile` / `ValidThrough`)
8//! and the [`FreshnessRegistry`] that resolves a validity per `(address, slot)`
9//! with the precedence **slot ▸ account ▸ default**.
10//! 2. **Observation** — [`SlotObservationTracker`] records per-slot change
11//! frequency (clock-agnostic) to drive adaptive re-verification, tuned by
12//! [`FreshnessParams`].
13//! 3. **Policy** — the [`FreshnessPolicy`] trait decides *which* volatile slots to
14//! verify this cycle; built-ins are [`AlwaysVerify`], [`NeverVerify`] and
15//! [`ObservationDriven`].
16//! 4. **Mechanism** — `EvmCache::verify_slots` / `EvmCache::purge_account`, and
17//! the freshness controller that runs the optimistic loop.
18//!
19//! The clock is configurable via [`FreshnessClock`]: [`BlockClock`] (the default,
20//! block-number based) or [`WallClock`] (unix seconds). The controller threads
21//! `clock.now()` as `now: u64` through the tracker, the policy, and
22//! [`FreshnessRegistry::is_volatile`].
23//!
24//! # Reconciliation scope
25//!
26//! The optimistic loop verifies only **volatile storage slots** in each sim's
27//! read set. Account-level state — native balance, nonce, and bytecode — is
28//! **not** re-fetched or diffed today, so [`Validation::ConfirmedStorage`] means
29//! *"no volatile storage slot the sims read had changed"*, not *"no account state
30//! changed"*. A sim whose result depends on a `BALANCE`/`SELFBALANCE` (or
31//! nonce/code) that moved on-chain without a co-changing storage slot in its read
32//! set can still be reported `ConfirmedStorage`. If account-level state matters to
33//! a sim, mark the account [`Validity::Pinned`] and keep it fresh via event-driven
34//! writes, or reconcile it out of band.
35//!
36//! The verdict taxonomy is deliberately split so this over-promise is visible in
37//! the type: [`ConfirmedStorage`](Validation::ConfirmedStorage) (storage only,
38//! account fields unverified) is distinct from
39//! [`ConfirmedFull`](Validation::ConfirmedFull) (storage *and* verified
40//! account-level fields both unchanged). `ConfirmedFull` is defined but not yet
41//! emitted — a follow-up wave wires validator-side account verification that will
42//! populate it and the [`Corrected`](Validation::Corrected) verdict's
43//! `changed_accounts`. See [`Validation`] for the per-verdict note.
44//!
45//! # Example
46//!
47//! Classification + policy selection, no network required:
48//!
49//! ```
50//! use alloy_primitives::{Address, U256};
51//! use evm_fork_cache::freshness::{
52//! AlwaysVerify, FreshnessPolicy, FreshnessRegistry, NeverVerify,
53//! };
54//! use evm_fork_cache::cache::SlotObservationTracker;
55//!
56//! let contract = Address::repeat_byte(0x01);
57//! let volatile_slot = U256::from(0);
58//! let immutable_slot = U256::from(6); // e.g. a constructor-set config value
59//!
60//! let mut registry = FreshnessRegistry::new(); // default: Volatile
61//! registry.pin_slot(contract, immutable_slot); // never re-verified
62//!
63//! // `now` is in clock units (block number for the default BlockClock).
64//! let now = 100;
65//! assert!(registry.is_volatile(contract, volatile_slot, now));
66//! assert!(!registry.is_volatile(contract, immutable_slot, now));
67//!
68//! // Policies pick which volatile candidates to verify this cycle.
69//! let obs = SlotObservationTracker::new();
70//! let candidates = [(contract, volatile_slot)];
71//! assert_eq!(
72//! AlwaysVerify.select(&candidates, &obs, now),
73//! vec![(contract, volatile_slot)]
74//! );
75//! assert!(NeverVerify.select(&candidates, &obs, now).is_empty());
76//! ```
77
78use std::collections::{HashMap, HashSet};
79use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
80use std::sync::{Arc, Mutex};
81use std::time::{SystemTime, UNIX_EPOCH};
82
83use alloy_eips::BlockId;
84use alloy_eips::eip2930::AccessList;
85use alloy_primitives::{Address, Bytes, U256};
86use revm::context::result::ExecutionResult;
87use tokio::task::JoinHandle;
88
89use crate::cache::{
90 CallSimulationResult, EvmCache, EvmOverlay, EvmSnapshot, SimStatus, SlotObservationTracker,
91 StorageBatchFetchFn, TxConfig,
92};
93use crate::errors::{FreshnessError, FreshnessResult as Result, StorageFetchResult};
94use crate::state_update::{AccountChange, StateUpdate};
95
96/// Default minimum observations before the change-frequency data is trusted.
97pub const DEFAULT_MIN_OBSERVATIONS: u32 = 10;
98
99/// Default maximum reuse window, in clock units, before a slot is rechecked.
100///
101/// Block-based default (≈300 blocks). Wall-clock users typically set this to
102/// `7 * 86400` (one week) to reproduce the original behavior.
103pub const DEFAULT_MAX_REUSE: u64 = 300;
104
105/// Default refetch threshold on expected probability of change.
106pub const DEFAULT_STALENESS_THRESHOLD: f64 = 0.05;
107
108/// Default change-rate above which a slot is always refetched.
109pub const DEFAULT_ALWAYS_REFETCH_RATE: f64 = 0.9;
110
111/// Default clock units per "cycle" used by the probabilistic model.
112pub const DEFAULT_CYCLE_INTERVAL: u64 = 1;
113
114/// Tunable thresholds for the adaptive freshness model.
115///
116/// All time-like fields are expressed in **clock units** (`FreshnessClock`):
117/// block numbers for a block clock, unix seconds for a wall clock. The defaults
118/// are block-oriented; wall-clock users should raise [`max_reuse`](Self::max_reuse)
119/// and [`cycle_interval`](Self::cycle_interval) accordingly.
120#[derive(Clone, Debug, PartialEq)]
121pub struct FreshnessParams {
122 /// Minimum observations before the change frequency is trusted (else refetch).
123 pub min_observations: u32,
124 /// Maximum reuse window (clock units) before a slot is force-rechecked.
125 pub max_reuse: u64,
126 /// Refetch when the expected probability of change exceeds this threshold.
127 pub staleness_threshold: f64,
128 /// Slots changing more often than this rate are always refetched.
129 pub always_refetch_rate: f64,
130 /// Clock units per "cycle" for the probabilistic expected-change estimate.
131 /// Must be non-zero; a zero is treated as one to avoid division by zero.
132 pub cycle_interval: u64,
133}
134
135impl Default for FreshnessParams {
136 fn default() -> Self {
137 Self {
138 min_observations: DEFAULT_MIN_OBSERVATIONS,
139 max_reuse: DEFAULT_MAX_REUSE,
140 staleness_threshold: DEFAULT_STALENESS_THRESHOLD,
141 always_refetch_rate: DEFAULT_ALWAYS_REFETCH_RATE,
142 cycle_interval: DEFAULT_CYCLE_INTERVAL,
143 }
144 }
145}
146
147impl FreshnessParams {
148 /// Block-oriented defaults (`max_reuse ≈ 300` blocks, one cycle per block).
149 pub fn for_block_clock() -> Self {
150 Self::default()
151 }
152
153 /// Wall-clock defaults: reuse up to one week, ~60s cycles, matching the
154 /// original (pre-Phase-2) hardcoded behavior of the observation tracker.
155 pub fn for_wall_clock() -> Self {
156 Self {
157 max_reuse: 7 * 86400,
158 cycle_interval: 60,
159 ..Self::default()
160 }
161 }
162}
163
164// ---------------------------------------------------------------------------
165// 1. Classification
166// ---------------------------------------------------------------------------
167
168/// How long a cached account or storage slot can be trusted.
169///
170/// Resolution precedence is **slot ▸ account ▸ default** (see
171/// [`FreshnessRegistry::validity`]).
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub enum Validity {
174 /// Caller-owned: immutable, or kept fresh out-of-band (e.g. via event
175 /// writes). The freshness system never re-verifies or purges it.
176 Pinned,
177 /// Governed by the active [`FreshnessPolicy`]; may be re-verified each cycle.
178 Volatile,
179 /// Pinned until clock value `N` (inclusive), then treated as [`Volatile`].
180 ///
181 /// [`Volatile`]: Validity::Volatile
182 ValidThrough(u64),
183}
184
185/// Per-address / per-slot validity classification.
186///
187/// A slot's validity is resolved with the precedence **slot ▸ account ▸
188/// default**: an explicit `(address, slot)` entry wins, else the account-level
189/// entry for `address`, else the registry default ([`Validity::Volatile`] unless
190/// changed via [`with_default`](Self::with_default)).
191///
192/// The setters are builder-style (`&mut Self`) so they can be chained.
193#[derive(Clone, Debug)]
194pub struct FreshnessRegistry {
195 default: Validity,
196 accounts: HashMap<Address, Validity>,
197 slots: HashMap<(Address, U256), Validity>,
198}
199
200impl Default for FreshnessRegistry {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206impl FreshnessRegistry {
207 /// A registry whose default validity is [`Validity::Volatile`].
208 pub fn new() -> Self {
209 Self {
210 default: Validity::Volatile,
211 accounts: HashMap::new(),
212 slots: HashMap::new(),
213 }
214 }
215
216 /// A registry with a custom default validity for unclassified state.
217 pub fn with_default(default: Validity) -> Self {
218 Self {
219 default,
220 accounts: HashMap::new(),
221 slots: HashMap::new(),
222 }
223 }
224
225 /// The default validity applied when neither the slot nor the account is set.
226 pub fn default_validity(&self) -> Validity {
227 self.default
228 }
229
230 /// Pin an account ([`Validity::Pinned`]).
231 pub fn pin(&mut self, addr: Address) -> &mut Self {
232 self.set_account(addr, Validity::Pinned)
233 }
234
235 /// Pin a single slot ([`Validity::Pinned`]).
236 pub fn pin_slot(&mut self, addr: Address, slot: U256) -> &mut Self {
237 self.set_slot(addr, slot, Validity::Pinned)
238 }
239
240 /// Mark an account [`Validity::Volatile`].
241 pub fn mark_volatile(&mut self, addr: Address) -> &mut Self {
242 self.set_account(addr, Validity::Volatile)
243 }
244
245 /// Mark a single slot [`Validity::Volatile`].
246 pub fn mark_volatile_slot(&mut self, addr: Address, slot: U256) -> &mut Self {
247 self.set_slot(addr, slot, Validity::Volatile)
248 }
249
250 /// Mark an account [`Validity::ValidThrough`] block/clock `n`.
251 pub fn valid_through(&mut self, addr: Address, n: u64) -> &mut Self {
252 self.set_account(addr, Validity::ValidThrough(n))
253 }
254
255 /// Mark a single slot [`Validity::ValidThrough`] block/clock `n`.
256 pub fn valid_through_slot(&mut self, addr: Address, slot: U256, n: u64) -> &mut Self {
257 self.set_slot(addr, slot, Validity::ValidThrough(n))
258 }
259
260 /// Set the account-level validity for `addr`.
261 pub fn set_account(&mut self, addr: Address, validity: Validity) -> &mut Self {
262 self.accounts.insert(addr, validity);
263 self
264 }
265
266 /// Set the slot-level validity for `(addr, slot)`.
267 pub fn set_slot(&mut self, addr: Address, slot: U256, validity: Validity) -> &mut Self {
268 self.slots.insert((addr, slot), validity);
269 self
270 }
271
272 /// Resolve the validity of `(addr, slot)` with **slot ▸ account ▸ default**.
273 pub fn validity(&self, addr: Address, slot: U256) -> Validity {
274 if let Some(v) = self.slots.get(&(addr, slot)) {
275 return *v;
276 }
277 if let Some(v) = self.accounts.get(&addr) {
278 return *v;
279 }
280 self.default
281 }
282
283 /// Whether `(addr, slot)` is currently volatile (subject to verification).
284 ///
285 /// `true` for [`Validity::Volatile`], and for [`Validity::ValidThrough`]`(m)`
286 /// once `now > m`. `false` for [`Validity::Pinned`] and a still-valid
287 /// `ValidThrough` (`now <= m`).
288 pub fn is_volatile(&self, addr: Address, slot: U256, now: u64) -> bool {
289 match self.validity(addr, slot) {
290 Validity::Pinned => false,
291 Validity::Volatile => true,
292 Validity::ValidThrough(m) => now > m,
293 }
294 }
295}
296
297// ---------------------------------------------------------------------------
298// 2. Clock
299// ---------------------------------------------------------------------------
300
301/// Source of the current clock value used throughout the freshness model.
302///
303/// Implementations return a monotone-ish `u64` in their own units. The two
304/// built-ins are [`BlockClock`] (block number, the default) and [`WallClock`]
305/// (unix seconds).
306pub trait FreshnessClock: Send + Sync {
307 /// The current clock value (block number or unix seconds).
308 fn now(&self) -> u64;
309
310 /// Advance the clock to `now`.
311 ///
312 /// Called by [`FreshnessController::on_new_block`] so the natural API drives
313 /// the clock forward. The default is a no-op (for clocks like [`WallClock`]
314 /// that advance on their own); [`BlockClock`] overrides it to set the block.
315 fn advance(&self, _now: u64) {}
316}
317
318/// Block-number clock (the default). Cloning shares the underlying counter, so a
319/// clone observed by a background task sees [`set_block`](Self::set_block)
320/// updates made on the main thread.
321#[derive(Clone, Debug, Default)]
322pub struct BlockClock(Arc<AtomicU64>);
323
324impl BlockClock {
325 /// A block clock starting at block 0.
326 pub fn new() -> Self {
327 Self(Arc::new(AtomicU64::new(0)))
328 }
329
330 /// A block clock starting at `block`.
331 pub fn at(block: u64) -> Self {
332 Self(Arc::new(AtomicU64::new(block)))
333 }
334
335 /// Set the current block number. Shared across clones.
336 pub fn set_block(&self, block: u64) {
337 self.0.store(block, Ordering::Relaxed);
338 }
339}
340
341impl FreshnessClock for BlockClock {
342 fn now(&self) -> u64 {
343 self.0.load(Ordering::Relaxed)
344 }
345
346 /// Set the current block to `now` (shared across clones).
347 fn advance(&self, now: u64) {
348 self.set_block(now);
349 }
350}
351
352/// Wall-clock clock: [`now`](FreshnessClock::now) returns unix seconds.
353///
354/// A zero-sized unit struct: unlike [`BlockClock`] it holds no `Arc`/`AtomicU64`,
355/// since the value is read straight from the system clock on each call. It
356/// advances on its own, so [`advance`](FreshnessClock::advance) is the trait
357/// default no-op and has no effect.
358#[derive(Clone, Copy, Debug, Default)]
359pub struct WallClock;
360
361impl FreshnessClock for WallClock {
362 fn now(&self) -> u64 {
363 SystemTime::now()
364 .duration_since(UNIX_EPOCH)
365 .map(|d| d.as_secs())
366 .unwrap_or(0)
367 }
368}
369
370// ---------------------------------------------------------------------------
371// 3. Policy
372// ---------------------------------------------------------------------------
373
374/// Decides which volatile candidate slots must be verified this cycle.
375///
376/// The controller passes the volatile candidates (predicted read set) plus the
377/// current observation stats and `now`; the policy returns the subset to
378/// re-fetch. Correctness does not depend on the policy being complete — the
379/// background validator always re-checks each sim's *actual* volatile read set
380/// before trusting results — so a policy only trades RPC cost against how often a
381/// `Corrected` verdict is needed.
382pub trait FreshnessPolicy: Send {
383 /// Of these volatile candidate slots, which must be verified this cycle?
384 fn select(
385 &mut self,
386 candidates: &[(Address, U256)],
387 obs: &SlotObservationTracker,
388 now: u64,
389 ) -> Vec<(Address, U256)>;
390
391 /// Hook called when the controller advances to a new block.
392 fn on_new_block(&mut self, _block: u64) {}
393}
394
395/// Verifies every volatile candidate (safe / eager). Always correct, most RPC.
396#[derive(Clone, Copy, Debug, Default)]
397pub struct AlwaysVerify;
398
399impl FreshnessPolicy for AlwaysVerify {
400 fn select(
401 &mut self,
402 candidates: &[(Address, U256)],
403 _obs: &SlotObservationTracker,
404 _now: u64,
405 ) -> Vec<(Address, U256)> {
406 candidates.to_vec()
407 }
408}
409
410/// Verifies nothing (trust-all). Selects no slots from the predicted set, though
411/// the actual-read-set reconcile in the background validator can still surface
412/// changes.
413#[derive(Clone, Copy, Debug, Default)]
414pub struct NeverVerify;
415
416impl FreshnessPolicy for NeverVerify {
417 fn select(
418 &mut self,
419 _candidates: &[(Address, U256)],
420 _obs: &SlotObservationTracker,
421 _now: u64,
422 ) -> Vec<(Address, U256)> {
423 Vec::new()
424 }
425}
426
427/// Adaptive policy: verifies candidates the observation tracker flags via
428/// [`SlotObservationTracker::should_refetch`](crate::cache::SlotObservationTracker::should_refetch),
429/// driven by the thresholds in [`FreshnessParams`].
430#[derive(Clone, Debug, Default)]
431pub struct ObservationDriven {
432 /// Thresholds for the underlying [`SlotObservationTracker::should_refetch`](crate::cache::SlotObservationTracker::should_refetch)
433 /// heuristic.
434 pub params: FreshnessParams,
435}
436
437impl ObservationDriven {
438 /// An observation-driven policy with the given parameters.
439 pub fn new(params: FreshnessParams) -> Self {
440 Self { params }
441 }
442}
443
444impl FreshnessPolicy for ObservationDriven {
445 fn select(
446 &mut self,
447 candidates: &[(Address, U256)],
448 obs: &SlotObservationTracker,
449 now: u64,
450 ) -> Vec<(Address, U256)> {
451 candidates
452 .iter()
453 .copied()
454 .filter(|(addr, slot)| obs.should_refetch(*addr, *slot, now, &self.params))
455 .collect()
456 }
457}
458
459// ---------------------------------------------------------------------------
460// 4. Results
461// ---------------------------------------------------------------------------
462
463/// A storage slot whose value changed: `old` is the prior cached/snapshot value
464/// (`ZERO` if previously uncached), `new` is the resulting value.
465///
466/// Produced by two paths: the freshness verifier
467/// ([`EvmCache::verify_slots`](crate::cache::EvmCache::verify_slots) and the
468/// background validator), where `new` is a freshly-fetched value that differed
469/// from the cache; and the state-update writer
470/// ([`EvmCache::apply_update`](crate::cache::EvmCache::apply_update) /
471/// [`apply_updates`](crate::cache::EvmCache::apply_updates)), where `new` is the
472/// value just written.
473#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
474pub struct SlotChange {
475 /// Contract whose storage changed.
476 pub address: Address,
477 /// Storage slot key.
478 pub slot: U256,
479 /// Value previously held in the cache/snapshot.
480 pub old: U256,
481 /// Freshly-fetched value.
482 pub new: U256,
483}
484
485/// The classified outcome of fetching a single storage slot.
486///
487/// Where a [`SlotChange`] records only slots whose value *differed* from the
488/// cache, a [`SlotOutcome`] is produced for **every** requested slot — including
489/// ones that did not change and ones the fetcher could not return. This closes
490/// the "archive-miss" gap: a transient fetch failure is surfaced explicitly as
491/// [`SlotFetch::FetchFailed`] rather than collapsing into the same "no value"
492/// signal as a genuine on-chain zero ([`SlotFetch::Zero`]).
493///
494/// The fetch classification ([`SlotFetch`]) and change detection ([`SlotChange`])
495/// are independent reads of the same fetched value: a genuine `Ok(0)` on a slot
496/// whose cached value was also `0` yields [`SlotFetch::Zero`] **and** no
497/// `SlotChange`.
498#[derive(Clone, Debug, PartialEq, Eq)]
499pub struct SlotOutcome {
500 /// Contract whose storage slot was fetched.
501 pub address: Address,
502 /// Storage slot key.
503 pub slot: U256,
504 /// The classified result of fetching this slot.
505 pub fetch: SlotFetch,
506}
507
508/// The classified result of an individual slot fetch.
509///
510/// A fetcher `Ok(value)` is classified into [`Value`](SlotFetch::Value) (non-zero)
511/// or [`Zero`](SlotFetch::Zero) (a genuine on-chain zero); a fetcher `Err`
512/// becomes [`FetchFailed`](SlotFetch::FetchFailed), carrying the error string.
513/// [`NotAttempted`](SlotFetch::NotAttempted) marks a declared slot that a
514/// short-circuited round never reached (produced only by the accounts/discover
515/// phases of a cold-start round, never by verify).
516#[derive(Clone, Debug, PartialEq, Eq)]
517pub enum SlotFetch {
518 /// The slot was fetched and holds a non-zero value.
519 Value(U256),
520 /// The slot was fetched and holds a genuine on-chain zero.
521 Zero,
522 /// The fetcher returned an error for this slot; `reason` is its description.
523 FetchFailed {
524 /// Human-readable description of why the fetch failed.
525 reason: String,
526 },
527 /// The slot was declared but never reached because the round
528 /// short-circuited on an earlier-phase hard error.
529 NotAttempted,
530}
531
532/// The deferred verdict on a [`SpeculativeSim`]'s optimistic results.
533///
534/// # Verdict taxonomy
535///
536/// The verdict distinguishes *what* was reconciled:
537///
538/// - [`ConfirmedStorage`](Validation::ConfirmedStorage): no volatile storage slot
539/// the sims read changed. Account-level fields (balance/nonce/code) were **not**
540/// verified — this is what today's validator emits on a storage-only success.
541/// - [`ConfirmedFull`](Validation::ConfirmedFull): no volatile storage slot **and**
542/// no verified account-level field changed. Defined but not yet emitted (a
543/// follow-up wave wires validator-side account verification).
544/// - [`Corrected`](Validation::Corrected): at least one read slot (and, once
545/// account verification lands, account field) changed; carries `changed_slots`
546/// and `changed_accounts`.
547/// - [`Unverified`](Validation::Unverified): the fetcher failed; results are not
548/// trusted.
549///
550/// # Reconciliation scope
551///
552/// Today the verdict reflects only **volatile storage slots** in each sim's read
553/// set. Account-level state — native balance, nonce, and bytecode — is **not**
554/// re-verified, so a sim whose result depends on a `BALANCE`/`SELFBALANCE` (or
555/// nonce/code) that changed on-chain *without* a co-changing storage slot in its
556/// read set can still be reported
557/// [`ConfirmedStorage`](Validation::ConfirmedStorage). Classify such accounts as
558/// [`Validity::Pinned`] and keep them fresh via event-driven writes if their
559/// account-level state matters to your sims. A follow-up wave wires validator-side
560/// account verification that will populate
561/// [`ConfirmedFull`](Validation::ConfirmedFull) and the `changed_accounts` field
562/// of [`Corrected`](Validation::Corrected). See the module-level docs for the full
563/// freshness contract.
564pub enum Validation {
565 /// No **volatile storage slot** the sims read changed; account-level
566 /// balance/nonce/code was **NOT** verified. This is the storage-only success
567 /// verdict today's validator emits — it does *not* cover account-level state
568 /// (see the [type-level scope](Validation)).
569 ConfirmedStorage,
570 /// No volatile storage slot **AND** no verified account-level field
571 /// (balance/nonce/code) changed. Not emitted yet: a follow-up wave wires the
572 /// validator-side account verification that populates it (see the
573 /// [type-level scope](Validation)).
574 ConfirmedFull,
575 /// At least one read storage slot changed. `results` is the optimistic set
576 /// with the affected sims re-run against the fresh values; `changed_slots`
577 /// lists the slots that differed (also queued for flow-back into the cache) and
578 /// `changed_accounts` lists any account-level fields that differed. Only
579 /// storage slots are reconciled today — account-level verification is wired by
580 /// a follow-up wave, so `changed_accounts` is currently always empty (see the
581 /// [type-level scope](Validation)).
582 Corrected {
583 /// Optimistic results with the affected sims replaced by re-runs.
584 results: Vec<CallSimulationResult>,
585 /// Slots whose fresh value differed from the snapshot.
586 changed_slots: Vec<SlotChange>,
587 /// Accounts whose native fields differed from the snapshot. Empty until a
588 /// follow-up wave wires validator-side account verification.
589 changed_accounts: Vec<AccountChange>,
590 },
591 /// Validation could not complete — the fetcher failed or was missing, a
592 /// corrected re-run could not execute, the fixed-point round cap was hit,
593 /// or a sim read `BLOCKHASH` (which validator overlays resolve to ZERO and
594 /// therefore cannot vouch for). The optimistic results are *not* trusted.
595 Unverified {
596 /// Human-readable description of why validation could not complete.
597 reason: String,
598 },
599}
600
601impl std::fmt::Debug for Validation {
602 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603 match self {
604 Validation::ConfirmedStorage => write!(f, "ConfirmedStorage"),
605 Validation::ConfirmedFull => write!(f, "ConfirmedFull"),
606 Validation::Corrected {
607 changed_slots,
608 changed_accounts,
609 ..
610 } => f
611 .debug_struct("Corrected")
612 .field("changed_slots", changed_slots)
613 .field("changed_accounts", changed_accounts)
614 .finish_non_exhaustive(),
615 Validation::Unverified { reason } => f
616 .debug_struct("Unverified")
617 .field("reason", reason)
618 .finish(),
619 }
620 }
621}
622
623/// A single non-committing simulation request for the optimistic loop.
624///
625/// `tx.access_list` is the *predicted* read set (a performance hint that seeds
626/// the verify candidates); correctness does not depend on it because the
627/// background validator re-checks each sim's actual volatile read set.
628#[derive(Clone, Debug)]
629pub struct SimRequest {
630 /// Transaction sender.
631 pub from: Address,
632 /// Call target.
633 pub to: Address,
634 /// Calldata.
635 pub calldata: Bytes,
636 /// Per-call tx environment; `tx.access_list` is the predicted read set.
637 pub tx: TxConfig,
638}
639
640impl SimRequest {
641 /// A zero-value request with default tx environment.
642 pub fn new(from: Address, to: Address, calldata: Bytes) -> Self {
643 Self {
644 from,
645 to,
646 calldata,
647 tx: TxConfig::default(),
648 }
649 }
650
651 /// Set the predicted read set (EIP-2930 access list hint).
652 pub fn with_access_list(mut self, access_list: AccessList) -> Self {
653 self.tx.access_list = Some(access_list);
654 self
655 }
656
657 /// Set the native value (wei) sent with the call (e.g. for a payable call).
658 pub fn with_value(mut self, value: U256) -> Self {
659 self.tx.value = value;
660 self
661 }
662
663 /// Set the gas limit for the call (e.g. to model out-of-gas behavior).
664 pub fn with_gas_limit(mut self, gas_limit: u64) -> Self {
665 self.tx.gas_limit = Some(gas_limit);
666 self
667 }
668
669 /// Set the gas price (wei) for the call.
670 pub fn with_gas_price(mut self, gas_price: u128) -> Self {
671 self.tx.gas_price = Some(gas_price);
672 self
673 }
674}
675
676/// Optimistic simulation results plus a handle to their deferred validation.
677///
678/// Returned by [`FreshnessController::run`] as soon as the optimistic sims
679/// finish (without awaiting RPC). Read [`optimistic`](Self::optimistic)
680/// immediately, then `await` [`validate`](Self::validate) for the verdict.
681///
682/// # Cancellation (best-effort)
683/// Dropping this — or calling [`into_optimistic`](Self::into_optimistic) — sets a
684/// cancel flag and aborts the background task. Cancellation is **cooperative and
685/// best-effort, not instantaneous**: `run_validator` is synchronous, so an abort
686/// cannot preempt it once it is running. Instead the validator checks the flag at
687/// several checkpoints — before each fetch, and (on the first pass) after a fetch
688/// returns but before it records observations or queues corrections — so a cancel
689/// observed at a checkpoint skips the side effects downstream of it. A validator
690/// already executing a synchronous step (e.g. mid-fetch) completes that step
691/// before reaching the next checkpoint, and corrections accumulated up to a fetch
692/// that completed in the *final* loop iteration may still be queued for flow-back
693/// (the post-loop queue is not guarded by an immediately-preceding checkpoint).
694/// The intent is that a dropped speculation stops doing further work and, in the
695/// common case, does not flow corrections back into the cache; it does not
696/// guarantee that an in-flight or just-completed fetch's correction is withheld.
697pub struct SpeculativeSim {
698 optimistic: Vec<CallSimulationResult>,
699 /// `Option` so `validate`/`into_optimistic` can take the handle and skip the
700 /// abort-on-drop; `Drop` only aborts a handle still left in place.
701 validation: Option<JoinHandle<Validation>>,
702 /// Set when the caller drops or [`into_optimistic`](Self::into_optimistic)s
703 /// this handle; the validator polls it at its checkpoints to bail out before
704 /// causing side effects (fetching, observing, queuing corrections).
705 cancelled: Arc<AtomicBool>,
706}
707
708impl SpeculativeSim {
709 /// The optimistic results, readable before validation completes.
710 ///
711 /// These (and the re-run results in [`Validation::Corrected`]) carry an
712 /// **empty `token_deltas`** map: the optimistic loop does not run transfer
713 /// tracking, so the signed per-token balance deltas populated by the
714 /// committing `simulate_call_with_balance_deltas` path are not available here.
715 pub fn optimistic(&self) -> &[CallSimulationResult] {
716 &self.optimistic
717 }
718
719 /// Consume the handle and return the optimistic results, aborting the
720 /// background validation task.
721 ///
722 /// Because this takes `self` by value, it and [`validate`](Self::validate)
723 /// are mutually exclusive: only one of them can ever run for a given
724 /// `SpeculativeSim`, and each takes the handle.
725 pub fn into_optimistic(mut self) -> Vec<CallSimulationResult> {
726 self.cancelled.store(true, Ordering::Relaxed);
727 if let Some(handle) = self.validation.take() {
728 handle.abort();
729 }
730 std::mem::take(&mut self.optimistic)
731 }
732
733 /// Await the deferred validation verdict.
734 ///
735 /// If the background task failed to complete (e.g. it panicked), returns an
736 /// error. This consumes `self`, so it is mutually
737 /// exclusive with the cancel paths ([`into_optimistic`](Self::into_optimistic)
738 /// / drop) — a handle that is awaited here is never cancelled.
739 pub async fn validate(mut self) -> Result<Validation> {
740 let Some(handle) = self.validation.take() else {
741 return Err(FreshnessError::ValidationHandleConsumed);
742 };
743 handle
744 .await
745 .map_err(|source| FreshnessError::ValidationTaskFailed { source })
746 }
747}
748
749impl Drop for SpeculativeSim {
750 fn drop(&mut self) {
751 self.cancelled.store(true, Ordering::Relaxed);
752 if let Some(handle) = self.validation.take() {
753 handle.abort();
754 }
755 }
756}
757
758// ---------------------------------------------------------------------------
759// Controller
760// ---------------------------------------------------------------------------
761
762/// Drives the optimistic verify-and-rerun loop over an [`EvmCache`].
763///
764/// Holds the freshness [`FreshnessRegistry`], the shared
765/// [`SlotObservationTracker`], a [`FreshnessPolicy`], a [`FreshnessClock`], and
766/// the pending-corrections queue. The tracker and the pending queue are
767/// `Arc<Mutex<…>>` so the background validator can update them without touching
768/// the `!Send` cache. Adaptive thresholds ([`FreshnessParams`]) live on the
769/// policy that uses them ([`ObservationDriven`]), not on the controller.
770///
771/// # Runtime requirement
772/// [`run`](Self::run) spawns a background task and the (synchronous) fetcher uses
773/// `block_in_place` internally, so a **multi-thread** tokio runtime is required
774/// (`#[tokio::main(flavor = "multi_thread")]` or
775/// `Builder::new_multi_thread()`), mirroring the [`EvmCache`] constructor note.
776pub struct FreshnessController<P: FreshnessPolicy, C: FreshnessClock> {
777 registry: FreshnessRegistry,
778 tracker: Arc<Mutex<SlotObservationTracker>>,
779 policy: P,
780 clock: C,
781 pending: Arc<Mutex<Vec<SlotChange>>>,
782 /// Cumulative count of background re-runs performed by the validator across
783 /// all `run` calls. Shared with the spawned task; incremented once per
784 /// re-executed sim. Lets callers observe that selective re-run actually
785 /// skipped the unaffected sims rather than re-running every one.
786 rerun_count: Arc<AtomicUsize>,
787}
788
789impl<P: FreshnessPolicy> FreshnessController<P, BlockClock> {
790 /// Build a controller with the default [`BlockClock`] (starting at block 0).
791 ///
792 /// Starts with a fresh, empty [`SlotObservationTracker`] and an empty
793 /// pending-corrections queue. Use [`with_tracker`](Self::with_tracker) to share
794 /// a persisted tracker, or [`with_clock`](Self::with_clock) for a non-default
795 /// clock such as [`WallClock`].
796 pub fn new(registry: FreshnessRegistry, policy: P) -> Self {
797 Self::with_clock(registry, policy, BlockClock::new())
798 }
799}
800
801impl<P: FreshnessPolicy, C: FreshnessClock> FreshnessController<P, C> {
802 /// Build a controller with an explicit clock.
803 ///
804 /// Starts with a fresh, empty [`SlotObservationTracker`] and an empty
805 /// pending-corrections queue. The clock's units must match those the
806 /// `policy`'s [`FreshnessParams`] were tuned for (block numbers for
807 /// [`BlockClock`], unix seconds for [`WallClock`]).
808 pub fn with_clock(registry: FreshnessRegistry, policy: P, clock: C) -> Self {
809 Self {
810 registry,
811 tracker: Arc::new(Mutex::new(SlotObservationTracker::new())),
812 policy,
813 clock,
814 pending: Arc::new(Mutex::new(Vec::new())),
815 rerun_count: Arc::new(AtomicUsize::new(0)),
816 }
817 }
818
819 /// Use an existing shared observation tracker (e.g. a persisted one).
820 ///
821 /// Builder-style override that replaces the fresh tracker installed by
822 /// [`new`](Self::new) / [`with_clock`](Self::with_clock) with the given shared
823 /// handle, so change-frequency history survives across runs or is shared with
824 /// other components. The background validator updates this same tracker under
825 /// its `Mutex`.
826 pub fn with_tracker(mut self, tracker: Arc<Mutex<SlotObservationTracker>>) -> Self {
827 self.tracker = tracker;
828 self
829 }
830
831 /// The shared observation tracker.
832 pub fn tracker(&self) -> &Arc<Mutex<SlotObservationTracker>> {
833 &self.tracker
834 }
835
836 /// The freshness registry.
837 pub fn registry(&self) -> &FreshnessRegistry {
838 &self.registry
839 }
840
841 /// Mutable access to the freshness registry.
842 pub fn registry_mut(&mut self) -> &mut FreshnessRegistry {
843 &mut self.registry
844 }
845
846 /// Number of corrections waiting to be drained into the cache on the next
847 /// [`run`](Self::run).
848 pub fn pending_len(&self) -> usize {
849 self.pending.lock().unwrap_or_else(|e| e.into_inner()).len()
850 }
851
852 /// Cumulative number of background re-runs performed by the validator across
853 /// all [`run`](Self::run) calls so far.
854 ///
855 /// Incremented once per sim that the reconcile step actually re-executes
856 /// (i.e. whose read set intersected a changed slot). A `Corrected` verdict
857 /// over `n` requests where only one slot changed advances this by the number
858 /// of *affected* sims, not by `n` — making the selective-re-run behavior
859 /// directly observable.
860 pub fn rerun_count(&self) -> usize {
861 self.rerun_count.load(Ordering::Relaxed)
862 }
863
864 /// Advance to a new block.
865 ///
866 /// Advances the clock to `block` (a no-op for [`WallClock`], a `set_block`
867 /// for [`BlockClock`]) and then notifies the policy. Advancing the clock is
868 /// what ages [`Validity::ValidThrough`] slots into [`Validity::Volatile`] and
869 /// progresses the observation-tracker reuse window through the natural API.
870 pub fn on_new_block(&mut self, block: u64) {
871 self.clock.advance(block);
872 self.policy.on_new_block(block);
873 }
874
875 /// Run the optimistic loop for a batch of requests.
876 ///
877 /// 1. Drain queued corrections from prior cycles into the cache.
878 /// 2. Snapshot the cache and grab the batch fetcher.
879 /// 3. Run each request optimistically against the snapshot, capturing its
880 /// actual volatile read set.
881 /// 4. Compute the predicted volatile candidates and ask the policy which to
882 /// verify.
883 /// 5. Spawn the background validator (Send data only) and return a
884 /// [`SpeculativeSim`] immediately.
885 ///
886 /// # Panics
887 /// Must be called from within a tokio runtime: it calls `tokio::spawn` to
888 /// launch the background validator, which panics (`there is no reactor
889 /// running`) if no runtime is active. The spawned (synchronous) fetcher
890 /// additionally uses `tokio::task::block_in_place` internally, so the runtime
891 /// must be **multi-thread** (`#[tokio::main(flavor = "multi_thread")]` or
892 /// `Builder::new_multi_thread()`); on a current-thread runtime `block_in_place`
893 /// panics, mirroring the [`EvmCache`] constructor note.
894 ///
895 /// # Errors
896 /// Returns an error if any optimistic simulation fails to execute against the
897 /// freshly-created snapshot (propagated from
898 /// `EvmOverlay::call_raw_with_access_list`).
899 pub fn run(
900 &mut self,
901 cache: &mut EvmCache,
902 requests: Vec<SimRequest>,
903 ) -> Result<SpeculativeSim> {
904 let now = self.clock.now();
905
906 // 1. Drain pending corrections into the cache before snapshotting.
907 // Routed through the unified write primitive (`apply_updates` of
908 // write-through `Slot`s); behavior-identical to the old
909 // `inject_storage_batch_fresh`, demonstrating the one write path.
910 {
911 let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
912 if !pending.is_empty() {
913 let injects: Vec<StateUpdate> = pending
914 .iter()
915 .map(|c| StateUpdate::slot(c.address, c.slot, c.new))
916 .collect();
917 cache.apply_updates(&injects);
918 pending.clear();
919 }
920 }
921
922 // 2. Snapshot + fetcher (Arc clones, both Send). Capture the cache's
923 // pinned block now, so the deferred validator fetches at the block the
924 // snapshot was built from even if the cache is re-pinned meanwhile.
925 let snapshot = cache.snapshot();
926 let fetcher = cache.storage_batch_fetcher().cloned();
927 let validation_block = cache.block();
928
929 // 3. Optimistic sims + per-sim actual volatile read sets. Sims whose
930 // execution read `BLOCKHASH` through the ext-db-less overlay (which
931 // resolves it to ZERO) are recorded so the validator can fail
932 // closed instead of confirming a result the replay cannot verify.
933 let mut optimistic = Vec::with_capacity(requests.len());
934 let mut read_sets: Vec<Vec<(Address, U256)>> = Vec::with_capacity(requests.len());
935 let mut blockhash_readers: Vec<usize> = Vec::new();
936 for (index, req) in requests.iter().enumerate() {
937 let mut overlay = EvmOverlay::new(Arc::clone(&snapshot), None);
938 let (result, access) = overlay.call_raw_with_access_list_with(
939 req.from,
940 req.to,
941 req.calldata.clone(),
942 &req.tx,
943 )?;
944 if overlay.blockhash_zero_fallback() {
945 blockhash_readers.push(index);
946 }
947 optimistic.push(result_to_sim(result, &access.to_eip2930()));
948
949 let volatile: Vec<(Address, U256)> = access
950 .slots
951 .iter()
952 .copied()
953 .filter(|(addr, slot)| self.registry.is_volatile(*addr, *slot, now))
954 .collect();
955 read_sets.push(volatile);
956 }
957
958 // 4. Predicted candidates (union of request access lists, volatile only).
959 let mut candidate_set: HashSet<(Address, U256)> = HashSet::new();
960 for req in &requests {
961 if let Some(al) = &req.tx.access_list {
962 for item in &al.0 {
963 for key in &item.storage_keys {
964 let slot = U256::from_be_bytes(key.0);
965 if self.registry.is_volatile(item.address, slot, now) {
966 candidate_set.insert((item.address, slot));
967 }
968 }
969 }
970 }
971 }
972 let candidates: Vec<(Address, U256)> = candidate_set.into_iter().collect();
973 let verify_set = {
974 let tracker = self.tracker.lock().unwrap_or_else(|e| e.into_inner());
975 self.policy.select(&candidates, &tracker, now)
976 };
977
978 // 5. Spawn the validator with Send-only data.
979 let registry = self.registry.clone();
980 let tracker = Arc::clone(&self.tracker);
981 let pending = Arc::clone(&self.pending);
982 let rerun_count = Arc::clone(&self.rerun_count);
983 let optimistic_for_task = optimistic.clone();
984 let cancelled = Arc::new(AtomicBool::new(false));
985 let cancelled_for_task = Arc::clone(&cancelled);
986 let validation = tokio::spawn(async move {
987 // Yield once before doing any work, so a prompt drop/into_optimistic
988 // can cancel before the validator is first polled. `run_validator` is
989 // otherwise synchronous, so cancellation past this point is
990 // cooperative: it observes the cancel flag at checkpoints.
991 tokio::task::yield_now().await;
992 run_validator(ValidatorInput {
993 snapshot,
994 fetcher,
995 requests,
996 read_sets,
997 registry,
998 tracker,
999 pending,
1000 rerun_count,
1001 now,
1002 verify_set,
1003 optimistic: optimistic_for_task,
1004 cancelled: cancelled_for_task,
1005 validation_block,
1006 blockhash_readers,
1007 })
1008 });
1009
1010 Ok(SpeculativeSim {
1011 optimistic,
1012 validation: Some(validation),
1013 cancelled,
1014 })
1015 }
1016}
1017
1018/// Owned inputs handed to the background validator (all `Send`).
1019struct ValidatorInput {
1020 snapshot: Arc<EvmSnapshot>,
1021 fetcher: Option<StorageBatchFetchFn>,
1022 requests: Vec<SimRequest>,
1023 read_sets: Vec<Vec<(Address, U256)>>,
1024 registry: FreshnessRegistry,
1025 tracker: Arc<Mutex<SlotObservationTracker>>,
1026 pending: Arc<Mutex<Vec<SlotChange>>>,
1027 rerun_count: Arc<AtomicUsize>,
1028 now: u64,
1029 verify_set: Vec<(Address, U256)>,
1030 optimistic: Vec<CallSimulationResult>,
1031 cancelled: Arc<AtomicBool>,
1032 /// Block the snapshot was built from; passed to the fetcher so the deferred
1033 /// fetch reads the same block the snapshot represents.
1034 validation_block: BlockId,
1035 /// Indices of requests whose optimistic run read `BLOCKHASH` through the
1036 /// ZERO fallback. Non-empty ⇒ the validator fails closed (`Unverified`):
1037 /// storage verification cannot vouch for a result whose control flow may
1038 /// depend on a hash these overlays cannot resolve.
1039 blockhash_readers: Vec<usize>,
1040}
1041
1042/// Maximum fixed-point iterations the background validator performs while a
1043/// correction keeps expanding a sim's volatile read set. A backstop against
1044/// pathological contracts that read an unbounded chain of new volatile slots;
1045/// reaching it yields [`Validation::Unverified`] (the results have not reached a
1046/// verified fixed point, so they must not be trusted), logged via `tracing::warn!`.
1047const MAX_VALIDATION_ROUNDS: u32 = 8;
1048
1049/// Collect batch-fetcher results into a lookup map, requiring **every** requested
1050/// `(address, slot)` to be present and `Ok`.
1051///
1052/// The validator must never silently trust a gap: a fetch error *or* a slot the
1053/// fetcher omitted from its response yields `Err(reason)` (mapped to
1054/// [`Validation::Unverified`] by the caller) rather than defaulting the missing
1055/// value to zero — a custom fetcher that drops a slot would otherwise produce a
1056/// false confirmation or correction.
1057fn collect_fetch_results(
1058 requested: &[(Address, U256)],
1059 results: Vec<(Address, U256, StorageFetchResult<U256>)>,
1060) -> std::result::Result<HashMap<(Address, U256), U256>, String> {
1061 let mut map: HashMap<(Address, U256), U256> = HashMap::new();
1062 for (addr, slot, value) in results {
1063 match value {
1064 Ok(v) => {
1065 map.insert((addr, slot), v);
1066 }
1067 Err(e) => return Err(format!("fetch failed for {addr}:{slot}: {e}")),
1068 }
1069 }
1070 for &key in requested {
1071 if !map.contains_key(&key) {
1072 return Err(format!(
1073 "fetcher omitted requested slot {}:{}",
1074 key.0, key.1
1075 ));
1076 }
1077 }
1078 Ok(map)
1079}
1080
1081/// The background validation routine. Touches only `Send` data — never the cache.
1082fn run_validator(input: ValidatorInput) -> Validation {
1083 let ValidatorInput {
1084 snapshot,
1085 fetcher,
1086 requests,
1087 read_sets,
1088 registry,
1089 tracker,
1090 pending,
1091 rerun_count,
1092 now,
1093 verify_set,
1094 optimistic,
1095 cancelled,
1096 validation_block,
1097 blockhash_readers,
1098 } = input;
1099
1100 // Checkpoint: cancelled before we even begin (the caller dropped or
1101 // `into_optimistic`d the handle while we were parked at the initial yield).
1102 if cancelled.load(Ordering::Relaxed) {
1103 return Validation::ConfirmedStorage;
1104 }
1105
1106 // Fail closed on unverifiable `BLOCKHASH` reads (G5). The overlays these
1107 // sims ran on carry no block hashes, so the opcode resolved to ZERO;
1108 // re-checking storage slots cannot vouch for a result whose control flow
1109 // may depend on the real hash. This must precede the empty-verify-set
1110 // early confirm below — a hash-reading sim that touches no volatile slots
1111 // would otherwise silently confirm.
1112 if let Some(first) = blockhash_readers.first() {
1113 return Validation::Unverified {
1114 reason: format!(
1115 "request {first} read BLOCKHASH, which resolves to ZERO in \
1116 validator overlays (block hashes are not tracked); the result \
1117 cannot be verified"
1118 ),
1119 };
1120 }
1121
1122 let Some(fetcher) = fetcher else {
1123 return Validation::Unverified {
1124 reason: "no storage batch fetcher available".to_string(),
1125 };
1126 };
1127
1128 // verify = policy-selected set ∪ each sim's actual volatile read set,
1129 // re-filtered through the registry clone so only currently-volatile slots
1130 // are checked (defensive: read sets and the policy selection are already
1131 // volatile-filtered on the main thread).
1132 let mut verify: HashSet<(Address, U256)> = verify_set.into_iter().collect();
1133 for set in &read_sets {
1134 verify.extend(set.iter().copied());
1135 }
1136 verify.retain(|(addr, slot)| registry.is_volatile(*addr, *slot, now));
1137 if verify.is_empty() {
1138 return Validation::ConfirmedStorage;
1139 }
1140 let verify: Vec<(Address, U256)> = verify.into_iter().collect();
1141
1142 // Checkpoint: cancelled before issuing the (costly, side-effecting) fetch.
1143 // This is what makes the "dropped before fetching" guarantee hold.
1144 if cancelled.load(Ordering::Relaxed) {
1145 return Validation::ConfirmedStorage;
1146 }
1147
1148 // Fetch fresh values. Any error OR any omitted slot → Unverified (never trust
1149 // silently: a missing result must not default to zero).
1150 let results = (fetcher)(verify.clone(), validation_block);
1151 let fresh = match collect_fetch_results(&verify, results) {
1152 Ok(map) => map,
1153 Err(reason) => return Validation::Unverified { reason },
1154 };
1155
1156 // Checkpoint: cancelled after the fetch returned but before we record any
1157 // observations or queue a correction. A cancel seen here discards the
1158 // verdict's side effects entirely.
1159 if cancelled.load(Ordering::Relaxed) {
1160 return Validation::ConfirmedStorage;
1161 }
1162
1163 // Compare the initial verify set against the snapshot, observe each checked
1164 // slot, and seed the changed set (deduped by `(address, slot)`).
1165 let mut changed_map: HashMap<(Address, U256), SlotChange> = HashMap::new();
1166 let mut verified: HashSet<(Address, U256)> = verify.iter().copied().collect();
1167 {
1168 let mut tracker = tracker.lock().unwrap_or_else(|e| e.into_inner());
1169 for &(addr, slot) in &verify {
1170 // `collect_fetch_results` guarantees every requested slot is present.
1171 let new = fresh[&(addr, slot)];
1172 let old = snapshot.storage_value(addr, slot).unwrap_or(U256::ZERO);
1173 tracker.observe(addr, slot, new, now);
1174 if new != old {
1175 changed_map.insert(
1176 (addr, slot),
1177 SlotChange {
1178 address: addr,
1179 slot,
1180 old,
1181 new,
1182 },
1183 );
1184 }
1185 }
1186 }
1187
1188 if changed_map.is_empty() {
1189 return Validation::ConfirmedStorage;
1190 }
1191
1192 // Re-run affected sims to a fixed point. A correction can flip control flow
1193 // so a re-run reads a *new* volatile slot the optimistic read set never
1194 // touched; that slot must itself be verified, or the "corrected" result
1195 // would still rest on stale snapshot state. Each round re-runs every sim
1196 // whose (possibly expanded) read set intersects a changed slot — applying
1197 // the full accumulated override set — collects newly-read volatile slots,
1198 // fetches and diffs them, and repeats until no new volatile slot appears,
1199 // none of the newly fetched slots differ, or the iteration cap is reached.
1200 let mut results = optimistic;
1201 // Per-sim current volatile read set; starts at the optimistic read set and
1202 // expands as corrections open new branches.
1203 let mut sim_reads = read_sets;
1204 let mut rerun_indices: HashSet<usize> = HashSet::new();
1205 let mut round: u32 = 0;
1206 loop {
1207 let changed_keys: HashSet<(Address, U256)> = changed_map.keys().copied().collect();
1208 let overrides: Vec<(Address, U256, U256)> = changed_map
1209 .values()
1210 .map(|c| (c.address, c.slot, c.new))
1211 .collect();
1212
1213 // Re-run sims whose current read set intersects a changed slot, applying
1214 // every accumulated override, and gather newly-read volatile candidates.
1215 let mut any_rerun = false;
1216 let mut new_candidates: HashSet<(Address, U256)> = HashSet::new();
1217 for (i, req) in requests.iter().enumerate() {
1218 if !sim_reads[i].iter().any(|k| changed_keys.contains(k)) {
1219 continue;
1220 }
1221 any_rerun = true;
1222 rerun_indices.insert(i);
1223 let mut overlay = EvmOverlay::new(Arc::clone(&snapshot), None);
1224 for &(addr, slot, value) in &overrides {
1225 overlay.override_slot(addr, slot, value);
1226 }
1227 // A host/transact error means the corrected re-run could not execute;
1228 // we must not keep the stale optimistic result and call it "Corrected".
1229 // (A revert/halt is `Ok(..)`, not an `Err`.) → Unverified.
1230 let (result, access) = match overlay.call_raw_with_access_list_with(
1231 req.from,
1232 req.to,
1233 req.calldata.clone(),
1234 &req.tx,
1235 ) {
1236 Ok(v) => v,
1237 Err(e) => {
1238 return Validation::Unverified {
1239 reason: format!("corrected re-run failed for request {i}: {e}"),
1240 };
1241 }
1242 };
1243 // A correction can flip control flow onto a `BLOCKHASH` read the
1244 // optimistic run never made; the re-run saw ZERO for it, so the
1245 // "corrected" result cannot be trusted either (G5, fail closed).
1246 if overlay.blockhash_zero_fallback() {
1247 return Validation::Unverified {
1248 reason: format!(
1249 "corrected re-run for request {i} read BLOCKHASH, which \
1250 resolves to ZERO in validator overlays; the corrected \
1251 result cannot be verified"
1252 ),
1253 };
1254 }
1255 results[i] = result_to_sim(result, &access.to_eip2930());
1256 let new_volatile: Vec<(Address, U256)> = access
1257 .slots
1258 .iter()
1259 .copied()
1260 .filter(|(a, s)| registry.is_volatile(*a, *s, now))
1261 .collect();
1262 for &key in &new_volatile {
1263 if !verified.contains(&key) {
1264 new_candidates.insert(key);
1265 }
1266 }
1267 sim_reads[i] = new_volatile;
1268 }
1269
1270 // No sim read a changed slot (the change came from the predicted
1271 // candidate set, not an actual read), or no new volatile slot surfaced:
1272 // the current results already reflect every override, so we are done.
1273 if !any_rerun || new_candidates.is_empty() {
1274 break;
1275 }
1276 // The fixed point was not reached within the cap: corrections kept opening
1277 // new volatile slots. The results still rest on un-verified state, so we
1278 // must NOT return a trusted `Corrected`. Return `Unverified` without
1279 // queuing any pending corrections (matching the fetch-error paths); the
1280 // still-volatile slots are re-discovered and re-fetched on the next run.
1281 if round >= MAX_VALIDATION_ROUNDS {
1282 tracing::warn!(
1283 rounds = round,
1284 "freshness validator exceeded fixed-point round cap; returning Unverified"
1285 );
1286 return Validation::Unverified {
1287 reason: format!(
1288 "freshness validation exceeded fixed-point round cap ({MAX_VALIDATION_ROUNDS})"
1289 ),
1290 };
1291 }
1292
1293 // Checkpoint: cancelled mid-loop. Results so far reflect the applied
1294 // overrides; do not fetch further or queue corrections.
1295 if cancelled.load(Ordering::Relaxed) {
1296 return Validation::ConfirmedStorage;
1297 }
1298
1299 // Fetch the newly-discovered candidates; any error OR omitted slot →
1300 // Unverified (a missing result must not default to zero).
1301 let new_vec: Vec<(Address, U256)> = new_candidates.into_iter().collect();
1302 let fetched = (fetcher)(new_vec.clone(), validation_block);
1303 let new_fresh = match collect_fetch_results(&new_vec, fetched) {
1304 Ok(map) => map,
1305 Err(reason) => return Validation::Unverified { reason },
1306 };
1307
1308 // Diff + observe the newly fetched slots, growing the changed set.
1309 let mut grew = false;
1310 {
1311 let mut tracker = tracker.lock().unwrap_or_else(|e| e.into_inner());
1312 for &(addr, slot) in &new_vec {
1313 verified.insert((addr, slot));
1314 // `collect_fetch_results` guarantees every requested slot is present.
1315 let new = new_fresh[&(addr, slot)];
1316 let old = snapshot.storage_value(addr, slot).unwrap_or(U256::ZERO);
1317 tracker.observe(addr, slot, new, now);
1318 if new != old {
1319 changed_map.insert(
1320 (addr, slot),
1321 SlotChange {
1322 address: addr,
1323 slot,
1324 old,
1325 new,
1326 },
1327 );
1328 grew = true;
1329 }
1330 }
1331 }
1332
1333 // The newly fetched slots were all unchanged → another round would not
1334 // alter any result; current results are final.
1335 if !grew {
1336 break;
1337 }
1338 round += 1;
1339 }
1340
1341 // Count distinct affected sims once: a sim re-run across multiple rounds is
1342 // still one affected sim, preserving the "once per re-executed sim" contract.
1343 rerun_count.fetch_add(rerun_indices.len(), Ordering::Relaxed);
1344
1345 // Queue every accumulated correction for flow-back into the cache next run.
1346 let changed_slots: Vec<SlotChange> = changed_map.into_values().collect();
1347 {
1348 let mut pending = pending.lock().unwrap_or_else(|e| e.into_inner());
1349 pending.extend(changed_slots.iter().cloned());
1350 }
1351
1352 // Account-level changes are populated by a follow-up wave that wires
1353 // validator-side account verification; empty for now.
1354 Validation::Corrected {
1355 results,
1356 changed_slots,
1357 changed_accounts: Vec::new(),
1358 }
1359}
1360
1361/// Build a [`CallSimulationResult`] from a non-committing execution result and
1362/// its captured access list. `token_deltas` is empty (the optimistic path does
1363/// not run transfer tracking); gas, logs, and return data come from the
1364/// execution result. `status` records whether the call succeeded, reverted, or
1365/// halted; `output` carries the `Success`/`Revert` payload (empty on `Halt`),
1366/// so a corrected view-call's new return value is observable here.
1367fn result_to_sim(result: ExecutionResult, access_list: &AccessList) -> CallSimulationResult {
1368 let (status, gas_used, logs, output) = match result {
1369 ExecutionResult::Success {
1370 gas_used,
1371 logs,
1372 output,
1373 ..
1374 } => (SimStatus::Success, gas_used, logs, output.into_data()),
1375 ExecutionResult::Revert { gas_used, output } => {
1376 (SimStatus::Revert, gas_used, Vec::new(), output)
1377 }
1378 ExecutionResult::Halt { gas_used, reason } => (
1379 SimStatus::Halt {
1380 reason: format!("{reason:?}"),
1381 },
1382 gas_used,
1383 Vec::new(),
1384 Bytes::new(),
1385 ),
1386 };
1387 CallSimulationResult {
1388 status,
1389 gas_used,
1390 token_deltas: HashMap::new(),
1391 logs,
1392 access_list: access_list.clone(),
1393 output,
1394 }
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399 use super::*;
1400
1401 fn addr(n: u8) -> Address {
1402 Address::repeat_byte(n)
1403 }
1404
1405 // --- Classification ----------------------------------------------------
1406
1407 #[test]
1408 fn registry_default_is_volatile() {
1409 let reg = FreshnessRegistry::new();
1410 assert_eq!(reg.default_validity(), Validity::Volatile);
1411 assert_eq!(reg.validity(addr(1), U256::from(0)), Validity::Volatile);
1412 }
1413
1414 #[test]
1415 fn registry_with_default_overrides_unclassified() {
1416 let reg = FreshnessRegistry::with_default(Validity::Pinned);
1417 assert_eq!(reg.validity(addr(1), U256::from(0)), Validity::Pinned);
1418 assert!(!reg.is_volatile(addr(1), U256::from(0), 100));
1419 }
1420
1421 #[test]
1422 fn registry_resolution_order_slot_account_default() {
1423 let a = addr(1);
1424 let mut reg = FreshnessRegistry::new(); // default Volatile
1425 reg.pin(a); // account-level Pinned
1426 reg.mark_volatile_slot(a, U256::from(7)); // slot-level Volatile
1427
1428 // slot-level wins over account-level
1429 assert_eq!(reg.validity(a, U256::from(7)), Validity::Volatile);
1430 // account-level wins over default for a non-overridden slot
1431 assert_eq!(reg.validity(a, U256::from(8)), Validity::Pinned);
1432 // default for an unrelated account
1433 assert_eq!(reg.validity(addr(2), U256::from(7)), Validity::Volatile);
1434 }
1435
1436 #[test]
1437 fn is_volatile_per_variant() {
1438 let a = addr(1);
1439 let mut reg = FreshnessRegistry::new();
1440 reg.pin_slot(a, U256::from(1));
1441 reg.mark_volatile_slot(a, U256::from(2));
1442 reg.valid_through_slot(a, U256::from(3), 50);
1443
1444 assert!(!reg.is_volatile(a, U256::from(1), 100)); // Pinned
1445 assert!(reg.is_volatile(a, U256::from(2), 100)); // Volatile
1446 }
1447
1448 #[test]
1449 fn valid_through_boundary() {
1450 let a = addr(1);
1451 let slot = U256::from(3);
1452 let mut reg = FreshnessRegistry::new();
1453 reg.valid_through_slot(a, slot, 50);
1454
1455 assert!(!reg.is_volatile(a, slot, 49)); // before
1456 assert!(!reg.is_volatile(a, slot, 50)); // at boundary: still valid (now == m)
1457 assert!(reg.is_volatile(a, slot, 51)); // after: now > m
1458 }
1459
1460 #[test]
1461 fn registry_is_clone() {
1462 let mut reg = FreshnessRegistry::new();
1463 reg.pin(addr(1));
1464 let clone = reg.clone();
1465 assert_eq!(clone.validity(addr(1), U256::from(0)), Validity::Pinned);
1466 }
1467
1468 // --- Clock -------------------------------------------------------------
1469
1470 #[test]
1471 fn block_clock_default_and_set() {
1472 let clock = BlockClock::new();
1473 assert_eq!(clock.now(), 0);
1474 clock.set_block(123);
1475 assert_eq!(clock.now(), 123);
1476 }
1477
1478 #[test]
1479 fn block_clock_clone_shares_counter() {
1480 let clock = BlockClock::at(10);
1481 let clone = clock.clone();
1482 clock.set_block(42);
1483 // The clone observes the update through the shared Arc.
1484 assert_eq!(clone.now(), 42);
1485 }
1486
1487 #[test]
1488 fn wall_clock_is_unix_seconds() {
1489 let now = WallClock.now();
1490 // Sanity: after 2021-01-01.
1491 assert!(now > 1_600_000_000);
1492 }
1493
1494 // --- Policy ------------------------------------------------------------
1495
1496 #[test]
1497 fn always_verify_selects_all() {
1498 let obs = SlotObservationTracker::new();
1499 let candidates = [(addr(1), U256::from(0)), (addr(2), U256::from(1))];
1500 let mut policy = AlwaysVerify;
1501 assert_eq!(policy.select(&candidates, &obs, 0), candidates.to_vec());
1502 }
1503
1504 #[test]
1505 fn never_verify_selects_none() {
1506 let obs = SlotObservationTracker::new();
1507 let candidates = [(addr(1), U256::from(0))];
1508 let mut policy = NeverVerify;
1509 assert!(policy.select(&candidates, &obs, 0).is_empty());
1510 }
1511
1512 #[test]
1513 fn observation_driven_selects_only_should_refetch() {
1514 let mut obs = SlotObservationTracker::new();
1515 let params = FreshnessParams::default();
1516 let stable = (addr(1), U256::from(0));
1517 let unknown = (addr(2), U256::from(0));
1518
1519 // Build a stable (never-changed) slot with enough observations so
1520 // `should_refetch` returns false for it.
1521 for now in 0..params.min_observations {
1522 obs.observe(stable.0, stable.1, U256::from(42), now as u64);
1523 }
1524 let now = params.min_observations as u64 - 1;
1525 assert!(!obs.should_refetch(stable.0, stable.1, now, ¶ms));
1526 assert!(obs.should_refetch(unknown.0, unknown.1, now, ¶ms));
1527
1528 let mut policy = ObservationDriven::new(params);
1529 let selected = policy.select(&[stable, unknown], &obs, now);
1530 assert_eq!(selected, vec![unknown]);
1531 }
1532}