Skip to main content

degenbot_simulation/sim/evm/
storage_memo.rs

1//! `StorageMemo` - a PER-CYCLE read memo in front of the RPC fallback.
2
3//! SIMPIPE2 M2 (2026-09-05): the CacheDb/WarmCodeCache stack caches basic +
4//! code, but `storage_ref` always forwards to RPC BY DESIGN - the engine
5//! deliberately does not serve engine-carried slots (a partial serve
6//! reintroduces the K-invariant / `LOK` reverts; see `bot_state_db.rs`).
7//! The result: every payload sim pays ~20 cold storage RPC round-trips, and
8//! the FFI-era per-fanout warmed handle never made it to the inline arms,
9//! so ~N paths on the same pools refetch the same slots N times per cycle.
10
11//! This memo is NOT the (refuted, gated) engine-serve seam: it caches the
12//! SAME RPC pre-state for the lifetime of ONE sim block. All sims of a block
13//! run at the same height on the same pre-state, and `execute()` writes are
14//! absorbed by the sim-local `CacheDB` overlay - the memo only ever stores
15//! values the fallback itself fetched. Semantically identical, strictly
16//! fewer RPC trips. The owner recreates the memo on every block advance.
17
18use alloy::primitives::{Address, StorageValue, U256};
19use parking_lot::Mutex;
20use std::collections::HashMap;
21use std::sync::atomic::{AtomicU64, Ordering};
22
23#[derive(Default)]
24struct Inner {
25    map: HashMap<(Address, U256), StorageValue>,
26}
27
28#[derive(Default)]
29pub struct StorageMemo {
30    inner: Mutex<Inner>,
31    hits: AtomicU64,
32    misses: AtomicU64,
33}
34
35impl StorageMemo {
36    #[must_use]
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Memo hit for this block (the memo is block-scoped; the owner recreates
42    /// it when the sim block advances).
43    pub fn get(&self, address: Address, index: U256) -> Option<StorageValue> {
44        let inner = self.inner.lock();
45        let hit = inner.map.get(&(address, index)).copied();
46        if hit.is_some() {
47            self.hits.fetch_add(1, Ordering::Relaxed);
48        } else {
49            self.misses.fetch_add(1, Ordering::Relaxed);
50        }
51        hit
52    }
53
54    pub fn put(&self, address: Address, index: U256, value: StorageValue) {
55        self.inner.lock().map.insert((address, index), value);
56    }
57
58    /// `(hits, misses)` - the lab counters for the probe logs.
59    #[must_use]
60    pub fn stats(&self) -> (u64, u64) {
61        (
62            self.hits.load(Ordering::Relaxed),
63            self.misses.load(Ordering::Relaxed),
64        )
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn round_trip_and_stats() {
74        let memo = StorageMemo::new();
75        let addr = Address::ZERO;
76        let key = U256::from(7u64);
77        assert_eq!(memo.get(addr, key), None);
78        memo.put(addr, key, StorageValue::from(42u64));
79        assert_eq!(memo.get(addr, key), Some(StorageValue::from(42u64)));
80        let (h, m) = memo.stats();
81        assert_eq!((h, m), (1, 1));
82    }
83}