use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use alloy_eips::BlockId;
use alloy_eips::eip2930::AccessList;
use alloy_primitives::{Address, Bytes, U256};
use revm::context::result::ExecutionResult;
use tokio::task::JoinHandle;
use crate::cache::{
CallSimulationResult, EvmCache, EvmOverlay, EvmSnapshot, SimStatus, SlotObservationTracker,
StorageBatchFetchFn, TxConfig,
};
use crate::errors::{FreshnessError, FreshnessResult as Result, StorageFetchResult};
use crate::state_update::{AccountChange, StateUpdate};
pub const DEFAULT_MIN_OBSERVATIONS: u32 = 10;
pub const DEFAULT_MAX_REUSE: u64 = 300;
pub const DEFAULT_STALENESS_THRESHOLD: f64 = 0.05;
pub const DEFAULT_ALWAYS_REFETCH_RATE: f64 = 0.9;
pub const DEFAULT_CYCLE_INTERVAL: u64 = 1;
#[derive(Clone, Debug, PartialEq)]
pub struct FreshnessParams {
pub min_observations: u32,
pub max_reuse: u64,
pub staleness_threshold: f64,
pub always_refetch_rate: f64,
pub cycle_interval: u64,
}
impl Default for FreshnessParams {
fn default() -> Self {
Self {
min_observations: DEFAULT_MIN_OBSERVATIONS,
max_reuse: DEFAULT_MAX_REUSE,
staleness_threshold: DEFAULT_STALENESS_THRESHOLD,
always_refetch_rate: DEFAULT_ALWAYS_REFETCH_RATE,
cycle_interval: DEFAULT_CYCLE_INTERVAL,
}
}
}
impl FreshnessParams {
pub fn for_block_clock() -> Self {
Self::default()
}
pub fn for_wall_clock() -> Self {
Self {
max_reuse: 7 * 86400,
cycle_interval: 60,
..Self::default()
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Validity {
Pinned,
Volatile,
ValidThrough(u64),
}
#[derive(Clone, Debug)]
pub struct FreshnessRegistry {
default: Validity,
accounts: HashMap<Address, Validity>,
slots: HashMap<(Address, U256), Validity>,
}
impl Default for FreshnessRegistry {
fn default() -> Self {
Self::new()
}
}
impl FreshnessRegistry {
pub fn new() -> Self {
Self {
default: Validity::Volatile,
accounts: HashMap::new(),
slots: HashMap::new(),
}
}
pub fn with_default(default: Validity) -> Self {
Self {
default,
accounts: HashMap::new(),
slots: HashMap::new(),
}
}
pub fn default_validity(&self) -> Validity {
self.default
}
pub fn pin(&mut self, addr: Address) -> &mut Self {
self.set_account(addr, Validity::Pinned)
}
pub fn pin_slot(&mut self, addr: Address, slot: U256) -> &mut Self {
self.set_slot(addr, slot, Validity::Pinned)
}
pub fn mark_volatile(&mut self, addr: Address) -> &mut Self {
self.set_account(addr, Validity::Volatile)
}
pub fn mark_volatile_slot(&mut self, addr: Address, slot: U256) -> &mut Self {
self.set_slot(addr, slot, Validity::Volatile)
}
pub fn valid_through(&mut self, addr: Address, n: u64) -> &mut Self {
self.set_account(addr, Validity::ValidThrough(n))
}
pub fn valid_through_slot(&mut self, addr: Address, slot: U256, n: u64) -> &mut Self {
self.set_slot(addr, slot, Validity::ValidThrough(n))
}
pub fn set_account(&mut self, addr: Address, validity: Validity) -> &mut Self {
self.accounts.insert(addr, validity);
self
}
pub fn set_slot(&mut self, addr: Address, slot: U256, validity: Validity) -> &mut Self {
self.slots.insert((addr, slot), validity);
self
}
pub fn validity(&self, addr: Address, slot: U256) -> Validity {
if let Some(v) = self.slots.get(&(addr, slot)) {
return *v;
}
if let Some(v) = self.accounts.get(&addr) {
return *v;
}
self.default
}
pub fn is_volatile(&self, addr: Address, slot: U256, now: u64) -> bool {
match self.validity(addr, slot) {
Validity::Pinned => false,
Validity::Volatile => true,
Validity::ValidThrough(m) => now > m,
}
}
}
pub trait FreshnessClock: Send + Sync {
fn now(&self) -> u64;
fn advance(&self, _now: u64) {}
}
#[derive(Clone, Debug, Default)]
pub struct BlockClock(Arc<AtomicU64>);
impl BlockClock {
pub fn new() -> Self {
Self(Arc::new(AtomicU64::new(0)))
}
pub fn at(block: u64) -> Self {
Self(Arc::new(AtomicU64::new(block)))
}
pub fn set_block(&self, block: u64) {
self.0.store(block, Ordering::Relaxed);
}
}
impl FreshnessClock for BlockClock {
fn now(&self) -> u64 {
self.0.load(Ordering::Relaxed)
}
fn advance(&self, now: u64) {
self.set_block(now);
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct WallClock;
impl FreshnessClock for WallClock {
fn now(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
}
pub trait FreshnessPolicy: Send {
fn select(
&mut self,
candidates: &[(Address, U256)],
obs: &SlotObservationTracker,
now: u64,
) -> Vec<(Address, U256)>;
fn on_new_block(&mut self, _block: u64) {}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct AlwaysVerify;
impl FreshnessPolicy for AlwaysVerify {
fn select(
&mut self,
candidates: &[(Address, U256)],
_obs: &SlotObservationTracker,
_now: u64,
) -> Vec<(Address, U256)> {
candidates.to_vec()
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NeverVerify;
impl FreshnessPolicy for NeverVerify {
fn select(
&mut self,
_candidates: &[(Address, U256)],
_obs: &SlotObservationTracker,
_now: u64,
) -> Vec<(Address, U256)> {
Vec::new()
}
}
#[derive(Clone, Debug, Default)]
pub struct ObservationDriven {
pub params: FreshnessParams,
}
impl ObservationDriven {
pub fn new(params: FreshnessParams) -> Self {
Self { params }
}
}
impl FreshnessPolicy for ObservationDriven {
fn select(
&mut self,
candidates: &[(Address, U256)],
obs: &SlotObservationTracker,
now: u64,
) -> Vec<(Address, U256)> {
candidates
.iter()
.copied()
.filter(|(addr, slot)| obs.should_refetch(*addr, *slot, now, &self.params))
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SlotChange {
pub address: Address,
pub slot: U256,
pub old: U256,
pub new: U256,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SlotOutcome {
pub address: Address,
pub slot: U256,
pub fetch: SlotFetch,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SlotFetch {
Value(U256),
Zero,
FetchFailed {
reason: String,
},
NotAttempted,
}
pub enum Validation {
ConfirmedStorage,
ConfirmedFull,
Corrected {
results: Vec<CallSimulationResult>,
changed_slots: Vec<SlotChange>,
changed_accounts: Vec<AccountChange>,
},
Unverified {
reason: String,
},
}
impl std::fmt::Debug for Validation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Validation::ConfirmedStorage => write!(f, "ConfirmedStorage"),
Validation::ConfirmedFull => write!(f, "ConfirmedFull"),
Validation::Corrected {
changed_slots,
changed_accounts,
..
} => f
.debug_struct("Corrected")
.field("changed_slots", changed_slots)
.field("changed_accounts", changed_accounts)
.finish_non_exhaustive(),
Validation::Unverified { reason } => f
.debug_struct("Unverified")
.field("reason", reason)
.finish(),
}
}
}
#[derive(Clone, Debug)]
pub struct SimRequest {
pub from: Address,
pub to: Address,
pub calldata: Bytes,
pub tx: TxConfig,
}
impl SimRequest {
pub fn new(from: Address, to: Address, calldata: Bytes) -> Self {
Self {
from,
to,
calldata,
tx: TxConfig::default(),
}
}
pub fn with_access_list(mut self, access_list: AccessList) -> Self {
self.tx.access_list = Some(access_list);
self
}
pub fn with_value(mut self, value: U256) -> Self {
self.tx.value = value;
self
}
pub fn with_gas_limit(mut self, gas_limit: u64) -> Self {
self.tx.gas_limit = Some(gas_limit);
self
}
pub fn with_gas_price(mut self, gas_price: u128) -> Self {
self.tx.gas_price = Some(gas_price);
self
}
}
pub struct SpeculativeSim {
optimistic: Vec<CallSimulationResult>,
validation: Option<JoinHandle<Validation>>,
cancelled: Arc<AtomicBool>,
}
impl SpeculativeSim {
pub fn optimistic(&self) -> &[CallSimulationResult] {
&self.optimistic
}
pub fn into_optimistic(mut self) -> Vec<CallSimulationResult> {
self.cancelled.store(true, Ordering::Relaxed);
if let Some(handle) = self.validation.take() {
handle.abort();
}
std::mem::take(&mut self.optimistic)
}
pub async fn validate(mut self) -> Result<Validation> {
let Some(handle) = self.validation.take() else {
return Err(FreshnessError::ValidationHandleConsumed);
};
handle
.await
.map_err(|source| FreshnessError::ValidationTaskFailed { source })
}
}
impl Drop for SpeculativeSim {
fn drop(&mut self) {
self.cancelled.store(true, Ordering::Relaxed);
if let Some(handle) = self.validation.take() {
handle.abort();
}
}
}
pub struct FreshnessController<P: FreshnessPolicy, C: FreshnessClock> {
registry: FreshnessRegistry,
tracker: Arc<Mutex<SlotObservationTracker>>,
policy: P,
clock: C,
pending: Arc<Mutex<Vec<SlotChange>>>,
rerun_count: Arc<AtomicUsize>,
}
impl<P: FreshnessPolicy> FreshnessController<P, BlockClock> {
pub fn new(registry: FreshnessRegistry, policy: P) -> Self {
Self::with_clock(registry, policy, BlockClock::new())
}
}
impl<P: FreshnessPolicy, C: FreshnessClock> FreshnessController<P, C> {
pub fn with_clock(registry: FreshnessRegistry, policy: P, clock: C) -> Self {
Self {
registry,
tracker: Arc::new(Mutex::new(SlotObservationTracker::new())),
policy,
clock,
pending: Arc::new(Mutex::new(Vec::new())),
rerun_count: Arc::new(AtomicUsize::new(0)),
}
}
pub fn with_tracker(mut self, tracker: Arc<Mutex<SlotObservationTracker>>) -> Self {
self.tracker = tracker;
self
}
pub fn tracker(&self) -> &Arc<Mutex<SlotObservationTracker>> {
&self.tracker
}
pub fn registry(&self) -> &FreshnessRegistry {
&self.registry
}
pub fn registry_mut(&mut self) -> &mut FreshnessRegistry {
&mut self.registry
}
pub fn pending_len(&self) -> usize {
self.pending.lock().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn rerun_count(&self) -> usize {
self.rerun_count.load(Ordering::Relaxed)
}
pub fn on_new_block(&mut self, block: u64) {
self.clock.advance(block);
self.policy.on_new_block(block);
}
pub fn run(
&mut self,
cache: &mut EvmCache,
requests: Vec<SimRequest>,
) -> Result<SpeculativeSim> {
let now = self.clock.now();
{
let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
if !pending.is_empty() {
let injects: Vec<StateUpdate> = pending
.iter()
.map(|c| StateUpdate::slot(c.address, c.slot, c.new))
.collect();
cache.apply_updates(&injects);
pending.clear();
}
}
let snapshot = cache.snapshot();
let fetcher = cache.storage_batch_fetcher().cloned();
let validation_block = cache.block();
let mut optimistic = Vec::with_capacity(requests.len());
let mut read_sets: Vec<Vec<(Address, U256)>> = Vec::with_capacity(requests.len());
let mut blockhash_readers: Vec<usize> = Vec::new();
for (index, req) in requests.iter().enumerate() {
let mut overlay = EvmOverlay::new(Arc::clone(&snapshot), None);
let (result, access) = overlay.call_raw_with_access_list_with(
req.from,
req.to,
req.calldata.clone(),
&req.tx,
)?;
if overlay.blockhash_zero_fallback() {
blockhash_readers.push(index);
}
optimistic.push(result_to_sim(result, &access.to_eip2930()));
let volatile: Vec<(Address, U256)> = access
.slots
.iter()
.copied()
.filter(|(addr, slot)| self.registry.is_volatile(*addr, *slot, now))
.collect();
read_sets.push(volatile);
}
let mut candidate_set: HashSet<(Address, U256)> = HashSet::new();
for req in &requests {
if let Some(al) = &req.tx.access_list {
for item in &al.0 {
for key in &item.storage_keys {
let slot = U256::from_be_bytes(key.0);
if self.registry.is_volatile(item.address, slot, now) {
candidate_set.insert((item.address, slot));
}
}
}
}
}
let candidates: Vec<(Address, U256)> = candidate_set.into_iter().collect();
let verify_set = {
let tracker = self.tracker.lock().unwrap_or_else(|e| e.into_inner());
self.policy.select(&candidates, &tracker, now)
};
let registry = self.registry.clone();
let tracker = Arc::clone(&self.tracker);
let pending = Arc::clone(&self.pending);
let rerun_count = Arc::clone(&self.rerun_count);
let optimistic_for_task = optimistic.clone();
let cancelled = Arc::new(AtomicBool::new(false));
let cancelled_for_task = Arc::clone(&cancelled);
let validation = tokio::spawn(async move {
tokio::task::yield_now().await;
run_validator(ValidatorInput {
snapshot,
fetcher,
requests,
read_sets,
registry,
tracker,
pending,
rerun_count,
now,
verify_set,
optimistic: optimistic_for_task,
cancelled: cancelled_for_task,
validation_block,
blockhash_readers,
})
});
Ok(SpeculativeSim {
optimistic,
validation: Some(validation),
cancelled,
})
}
}
struct ValidatorInput {
snapshot: Arc<EvmSnapshot>,
fetcher: Option<StorageBatchFetchFn>,
requests: Vec<SimRequest>,
read_sets: Vec<Vec<(Address, U256)>>,
registry: FreshnessRegistry,
tracker: Arc<Mutex<SlotObservationTracker>>,
pending: Arc<Mutex<Vec<SlotChange>>>,
rerun_count: Arc<AtomicUsize>,
now: u64,
verify_set: Vec<(Address, U256)>,
optimistic: Vec<CallSimulationResult>,
cancelled: Arc<AtomicBool>,
validation_block: BlockId,
blockhash_readers: Vec<usize>,
}
const MAX_VALIDATION_ROUNDS: u32 = 8;
fn collect_fetch_results(
requested: &[(Address, U256)],
results: Vec<(Address, U256, StorageFetchResult<U256>)>,
) -> std::result::Result<HashMap<(Address, U256), U256>, String> {
let mut map: HashMap<(Address, U256), U256> = HashMap::new();
for (addr, slot, value) in results {
match value {
Ok(v) => {
map.insert((addr, slot), v);
}
Err(e) => return Err(format!("fetch failed for {addr}:{slot}: {e}")),
}
}
for &key in requested {
if !map.contains_key(&key) {
return Err(format!(
"fetcher omitted requested slot {}:{}",
key.0, key.1
));
}
}
Ok(map)
}
fn run_validator(input: ValidatorInput) -> Validation {
let ValidatorInput {
snapshot,
fetcher,
requests,
read_sets,
registry,
tracker,
pending,
rerun_count,
now,
verify_set,
optimistic,
cancelled,
validation_block,
blockhash_readers,
} = input;
if cancelled.load(Ordering::Relaxed) {
return Validation::ConfirmedStorage;
}
if let Some(first) = blockhash_readers.first() {
return Validation::Unverified {
reason: format!(
"request {first} read BLOCKHASH, which resolves to ZERO in \
validator overlays (block hashes are not tracked); the result \
cannot be verified"
),
};
}
let Some(fetcher) = fetcher else {
return Validation::Unverified {
reason: "no storage batch fetcher available".to_string(),
};
};
let mut verify: HashSet<(Address, U256)> = verify_set.into_iter().collect();
for set in &read_sets {
verify.extend(set.iter().copied());
}
verify.retain(|(addr, slot)| registry.is_volatile(*addr, *slot, now));
if verify.is_empty() {
return Validation::ConfirmedStorage;
}
let verify: Vec<(Address, U256)> = verify.into_iter().collect();
if cancelled.load(Ordering::Relaxed) {
return Validation::ConfirmedStorage;
}
let results = (fetcher)(verify.clone(), validation_block);
let fresh = match collect_fetch_results(&verify, results) {
Ok(map) => map,
Err(reason) => return Validation::Unverified { reason },
};
if cancelled.load(Ordering::Relaxed) {
return Validation::ConfirmedStorage;
}
let mut changed_map: HashMap<(Address, U256), SlotChange> = HashMap::new();
let mut verified: HashSet<(Address, U256)> = verify.iter().copied().collect();
{
let mut tracker = tracker.lock().unwrap_or_else(|e| e.into_inner());
for &(addr, slot) in &verify {
let new = fresh[&(addr, slot)];
let old = snapshot.storage_value(addr, slot).unwrap_or(U256::ZERO);
tracker.observe(addr, slot, new, now);
if new != old {
changed_map.insert(
(addr, slot),
SlotChange {
address: addr,
slot,
old,
new,
},
);
}
}
}
if changed_map.is_empty() {
return Validation::ConfirmedStorage;
}
let mut results = optimistic;
let mut sim_reads = read_sets;
let mut rerun_indices: HashSet<usize> = HashSet::new();
let mut round: u32 = 0;
loop {
let changed_keys: HashSet<(Address, U256)> = changed_map.keys().copied().collect();
let overrides: Vec<(Address, U256, U256)> = changed_map
.values()
.map(|c| (c.address, c.slot, c.new))
.collect();
let mut any_rerun = false;
let mut new_candidates: HashSet<(Address, U256)> = HashSet::new();
for (i, req) in requests.iter().enumerate() {
if !sim_reads[i].iter().any(|k| changed_keys.contains(k)) {
continue;
}
any_rerun = true;
rerun_indices.insert(i);
let mut overlay = EvmOverlay::new(Arc::clone(&snapshot), None);
for &(addr, slot, value) in &overrides {
overlay.override_slot(addr, slot, value);
}
let (result, access) = match overlay.call_raw_with_access_list_with(
req.from,
req.to,
req.calldata.clone(),
&req.tx,
) {
Ok(v) => v,
Err(e) => {
return Validation::Unverified {
reason: format!("corrected re-run failed for request {i}: {e}"),
};
}
};
if overlay.blockhash_zero_fallback() {
return Validation::Unverified {
reason: format!(
"corrected re-run for request {i} read BLOCKHASH, which \
resolves to ZERO in validator overlays; the corrected \
result cannot be verified"
),
};
}
results[i] = result_to_sim(result, &access.to_eip2930());
let new_volatile: Vec<(Address, U256)> = access
.slots
.iter()
.copied()
.filter(|(a, s)| registry.is_volatile(*a, *s, now))
.collect();
for &key in &new_volatile {
if !verified.contains(&key) {
new_candidates.insert(key);
}
}
sim_reads[i] = new_volatile;
}
if !any_rerun || new_candidates.is_empty() {
break;
}
if round >= MAX_VALIDATION_ROUNDS {
tracing::warn!(
rounds = round,
"freshness validator exceeded fixed-point round cap; returning Unverified"
);
return Validation::Unverified {
reason: format!(
"freshness validation exceeded fixed-point round cap ({MAX_VALIDATION_ROUNDS})"
),
};
}
if cancelled.load(Ordering::Relaxed) {
return Validation::ConfirmedStorage;
}
let new_vec: Vec<(Address, U256)> = new_candidates.into_iter().collect();
let fetched = (fetcher)(new_vec.clone(), validation_block);
let new_fresh = match collect_fetch_results(&new_vec, fetched) {
Ok(map) => map,
Err(reason) => return Validation::Unverified { reason },
};
let mut grew = false;
{
let mut tracker = tracker.lock().unwrap_or_else(|e| e.into_inner());
for &(addr, slot) in &new_vec {
verified.insert((addr, slot));
let new = new_fresh[&(addr, slot)];
let old = snapshot.storage_value(addr, slot).unwrap_or(U256::ZERO);
tracker.observe(addr, slot, new, now);
if new != old {
changed_map.insert(
(addr, slot),
SlotChange {
address: addr,
slot,
old,
new,
},
);
grew = true;
}
}
}
if !grew {
break;
}
round += 1;
}
rerun_count.fetch_add(rerun_indices.len(), Ordering::Relaxed);
let changed_slots: Vec<SlotChange> = changed_map.into_values().collect();
{
let mut pending = pending.lock().unwrap_or_else(|e| e.into_inner());
pending.extend(changed_slots.iter().cloned());
}
Validation::Corrected {
results,
changed_slots,
changed_accounts: Vec::new(),
}
}
fn result_to_sim(result: ExecutionResult, access_list: &AccessList) -> CallSimulationResult {
let (status, gas_used, logs, output) = match result {
ExecutionResult::Success {
gas_used,
logs,
output,
..
} => (SimStatus::Success, gas_used, logs, output.into_data()),
ExecutionResult::Revert { gas_used, output } => {
(SimStatus::Revert, gas_used, Vec::new(), output)
}
ExecutionResult::Halt { gas_used, reason } => (
SimStatus::Halt {
reason: format!("{reason:?}"),
},
gas_used,
Vec::new(),
Bytes::new(),
),
};
CallSimulationResult {
status,
gas_used,
token_deltas: HashMap::new(),
logs,
access_list: access_list.clone(),
output,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn addr(n: u8) -> Address {
Address::repeat_byte(n)
}
#[test]
fn registry_default_is_volatile() {
let reg = FreshnessRegistry::new();
assert_eq!(reg.default_validity(), Validity::Volatile);
assert_eq!(reg.validity(addr(1), U256::from(0)), Validity::Volatile);
}
#[test]
fn registry_with_default_overrides_unclassified() {
let reg = FreshnessRegistry::with_default(Validity::Pinned);
assert_eq!(reg.validity(addr(1), U256::from(0)), Validity::Pinned);
assert!(!reg.is_volatile(addr(1), U256::from(0), 100));
}
#[test]
fn registry_resolution_order_slot_account_default() {
let a = addr(1);
let mut reg = FreshnessRegistry::new(); reg.pin(a); reg.mark_volatile_slot(a, U256::from(7));
assert_eq!(reg.validity(a, U256::from(7)), Validity::Volatile);
assert_eq!(reg.validity(a, U256::from(8)), Validity::Pinned);
assert_eq!(reg.validity(addr(2), U256::from(7)), Validity::Volatile);
}
#[test]
fn is_volatile_per_variant() {
let a = addr(1);
let mut reg = FreshnessRegistry::new();
reg.pin_slot(a, U256::from(1));
reg.mark_volatile_slot(a, U256::from(2));
reg.valid_through_slot(a, U256::from(3), 50);
assert!(!reg.is_volatile(a, U256::from(1), 100)); assert!(reg.is_volatile(a, U256::from(2), 100)); }
#[test]
fn valid_through_boundary() {
let a = addr(1);
let slot = U256::from(3);
let mut reg = FreshnessRegistry::new();
reg.valid_through_slot(a, slot, 50);
assert!(!reg.is_volatile(a, slot, 49)); assert!(!reg.is_volatile(a, slot, 50)); assert!(reg.is_volatile(a, slot, 51)); }
#[test]
fn registry_is_clone() {
let mut reg = FreshnessRegistry::new();
reg.pin(addr(1));
let clone = reg.clone();
assert_eq!(clone.validity(addr(1), U256::from(0)), Validity::Pinned);
}
#[test]
fn block_clock_default_and_set() {
let clock = BlockClock::new();
assert_eq!(clock.now(), 0);
clock.set_block(123);
assert_eq!(clock.now(), 123);
}
#[test]
fn block_clock_clone_shares_counter() {
let clock = BlockClock::at(10);
let clone = clock.clone();
clock.set_block(42);
assert_eq!(clone.now(), 42);
}
#[test]
fn wall_clock_is_unix_seconds() {
let now = WallClock.now();
assert!(now > 1_600_000_000);
}
#[test]
fn always_verify_selects_all() {
let obs = SlotObservationTracker::new();
let candidates = [(addr(1), U256::from(0)), (addr(2), U256::from(1))];
let mut policy = AlwaysVerify;
assert_eq!(policy.select(&candidates, &obs, 0), candidates.to_vec());
}
#[test]
fn never_verify_selects_none() {
let obs = SlotObservationTracker::new();
let candidates = [(addr(1), U256::from(0))];
let mut policy = NeverVerify;
assert!(policy.select(&candidates, &obs, 0).is_empty());
}
#[test]
fn observation_driven_selects_only_should_refetch() {
let mut obs = SlotObservationTracker::new();
let params = FreshnessParams::default();
let stable = (addr(1), U256::from(0));
let unknown = (addr(2), U256::from(0));
for now in 0..params.min_observations {
obs.observe(stable.0, stable.1, U256::from(42), now as u64);
}
let now = params.min_observations as u64 - 1;
assert!(!obs.should_refetch(stable.0, stable.1, now, ¶ms));
assert!(obs.should_refetch(unknown.0, unknown.1, now, ¶ms));
let mut policy = ObservationDriven::new(params);
let selected = policy.select(&[stable, unknown], &obs, now);
assert_eq!(selected, vec![unknown]);
}
}