Skip to main content

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