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)]
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, &params));
1526        assert!(obs.should_refetch(unknown.0, unknown.1, now, &params));
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}