Skip to main content

degenbot_simulation/sim/evm/
warm_code_cache.rs

1//! Cross-block warm cache for immutable/long-TTL account data — bytecode +
2//! account existence (the `basic_ref` + `code_by_hash_ref` rows). Sits
3//! underneath the per-block `CacheDB` in the `BlockSimHandle` DB stack so the
4//! contract code-load RPCs stop repeating every block while the per-block
5//! `CacheDB` (overrides + mutable storage, fresh each block, drops at EOB) is
6//! preserved.
7//!
8//! ## What this caches (the staleness line)
9//!
10//! Only the immutable rows:
11//! - `basic_ref` — `AccountInfo` FOR CONTRACTS ONLY. An EOA's nonce/balance
12//!   mutate every block, so EOA rows (empty code hash) are never cached —
13//!   they fall through to the per-block `CacheDB` + RPC path fresh at each
14//!   block build. (A cached contract's AccountInfo byte-RARELY mutates:
15//!   contract nonces advance only via `CREATE*`; a stale contract nonce does
16//!   not gate frame-replay validation the way a sender nonce does, and the
17//!   TTL refresh bounds that edge.)
18//! - `code_by_hash_ref` — `Bytecode` keyed by `code_hash` (immutable by hash).
19//!
20//! `storage_ref` + `block_hash_ref` are forwarded UNCHECKED to the inner `Db`
21//! (the mutable-per-block row — V2 reserves slot 8 / V3 `slot0`+ticks change
22//! every block there's a swap; the chain block hash is per-chain, not
23//! per-block-snapshot). Caching these would re-introduce the K-invariant /
24//! `LOK` stale-state-divergence reverts the option-B path hit.
25//!
26//! ## The TTL (a safety net, not the primary correctness mechanism)
27//!
28//! Each entry records the block it was loaded at; an entry read at block `B`
29//! is a **miss** when `B - loaded_block > WARM_CODE_CACHE_TTL_BLOCKS`. A
30//! re-fetch on miss re-caches with `loaded_block = B`.
31//!
32//! In the bot's domain every cached contract is immutable (EIP-1967 proxies
33//! upgrade via the implementation storage slot — uncached; the injected
34//! executor is per-block via `stateOverrides`, never read here). The TTL is a
35//! belt-and-suspenders net against:
36//! - **EIP-6780-disabled selfdestruct edge cases** (pre-Cancun metamorphic
37//!   `CREATE2`-redeploy-to-same-address: a contract that selfdestructs then
38//!   redeploys with different bytecode at the same address would be stale).
39//!   EIP-6780 (Cancun, mainnet since March 2024) disables code-deletion on
40//!   selfdestruct unless the contract was created in the same tx — so the
41//!   vector is effectively closed for any persistent contract.
42//! - **Future protocol changes** to code mutability.
43//!
44//! ## Composition
45//!
46//! ```text
47//! CacheDB (per-block: overrides + mutable storage, fresh each block, drops at EOB)
48//!   → WarmCodeCache (persistent: bytecode + account-existence, per-entry TTL'd)
49//!        ↳ holds Arc<RwLock<WarmCodeCacheInner>> cloned from the engine owner
50//!        ↳ the WarmCodeCache<Db> wrapper value itself is per-block
51//!    → BotStateDb (forwarding storage fallback)
52//!      → WrapDatabaseAsync<AlloyDB> (RPC cold-miss)
53//! ```
54//!
55//! ## Ownership
56//!
57//! The wrapper value (`WarmCodeCache<Db>`) is per-block — constructed in
58//! `BlockSimHandle::build`, holds the per-block `Db`, drops at end of block.
59//! Only the inner map (`Arc<RwLock<WarmCodeCacheInner>>`) persists across
60//! blocks — cloned from the engine owner (`PyArbitrageEngine` / the
61//! standalone Rust `Bot`). This preserves the per-block `CacheDB` discipline
62//! while sharing the bytecode cache across blocks.
63
64// Solidity/EVM + Rust-ecosystem identifiers (EIP-6780, EIP-1967, DatabaseRef,
65// CacheDB, WrapDatabaseAsync, BlockSimHandle, etc.) are ubiquitous here —
66// match `degenbot-simulation`'s convention.
67
68use alloy::primitives::map::{AddressHashMap, B256HashMap};
69use alloy::primitives::Address;
70use parking_lot::RwLock;
71use revm::bytecode::Bytecode;
72use revm::database_interface::DatabaseRef;
73use revm::primitives::{StorageKey, StorageValue, B256};
74use revm::state::AccountInfo;
75use std::sync::Arc;
76
77/// The default per-entry TTL in blocks — ≈ 1 day of mainnet blocks (12s
78/// block time → ~7200 blocks/hour → ~10 000 ≈ 1.4 days).
79///
80/// A safety net, not the primary correctness mechanism — in the bot's domain
81/// every cached contract is immutable (EIP-1967 proxies upgrade via the
82/// implementation *storage* slot, which this cache does NOT serve; the
83/// injected executor is per-block via `stateOverrides`). See the module doc
84/// for the EIP-6780 selfdestruct + future-protocol-change analysis.
85pub const WARM_CODE_CACHE_TTL_BLOCKS: u64 = 10_000;
86
87/// The cross-block persistent inner state — bytecode + account-existence maps,
88/// each entry tagged with the block it was loaded at for per-entry TTL expiry.
89///
90/// Held behind `Arc<RwLock<...>>` so the engine owner (`PyArbitrageEngine` /
91/// the standalone Rust `Bot`) shares one instance across every per-block
92/// `WarmCodeCache` wrapper value. The maps grow with the working set (a few
93/// hundred contracts for one bot's pool-set); there is no LRU eviction in
94/// Tier 1 — unbounded growth is not a near-term concern (a follow-up if
95/// measurement shows otherwise).
96pub struct WarmCodeCacheInner {
97    /// `(loaded_block, AccountInfo)` per address. `AccountInfo` carries
98    /// `code_hash` (the bytecode is fetched separately via `code_by_hash_ref`,
99    /// or eagerly inline in `basic_ref` — revm's `AlloyDB` does the latter).
100    /// `None` (no-account) IS cached (existence-negative) — saves the `basic`
101    /// RPC on the no-code-EOA case the simulate's `balanceOf`-to-fresh-
102    /// addresses hits.
103    accounts: AddressHashMap<(u64, Option<AccountInfo>)>,
104    /// `(loaded_block, Bytecode)` per `code_hash` (immutable by hash).
105    bytecode: B256HashMap<(u64, Bytecode)>,
106    /// The fixed-offset TTL in blocks. An entry loaded at `loaded_block` is
107    /// a miss when `block - loaded_block > ttl_blocks` (strictly greater — an
108    /// entry loaded THIS block is always fresh).
109    ttl_blocks: u64,
110}
111
112impl WarmCodeCacheInner {
113    /// Construct an empty inner state with the given per-entry TTL.
114    #[must_use]
115    fn new(ttl_blocks: u64) -> Self {
116        Self {
117            accounts: AddressHashMap::default(),
118            bytecode: B256HashMap::default(),
119            ttl_blocks,
120        }
121    }
122
123    /// Construct a shared (`Arc<RwLock<...>>`) empty inner state with the
124    /// default TTL — the engine-owner construction shape (the `PyArbitrageEngine`
125    /// / standalone Rust `Bot` calls this once at construction + holds the arc
126    /// for the engine's life, cloning it into each per-block
127    /// `WarmCodeCache::with_owner`).
128    #[must_use]
129    pub fn shared_default() -> Arc<RwLock<Self>> {
130        Arc::new(RwLock::new(Self::new(WARM_CODE_CACHE_TTL_BLOCKS)))
131    }
132
133    /// Construct a shared inner state with a custom TTL (for tests + the
134    /// benchmark's TTL-boundary assertion).
135    #[must_use]
136    pub fn shared_with_ttl(ttl_blocks: u64) -> Arc<RwLock<Self>> {
137        Arc::new(RwLock::new(Self::new(ttl_blocks)))
138    }
139
140    /// Is an entry loaded at `loaded_block` still fresh at `block`?
141    /// Fresh iff `block - loaded_block <= ttl_blocks` (an entry loaded THIS
142    /// block is always fresh — `block - loaded_block = 0 <= ttl`).
143    fn is_fresh(&self, loaded_block: u64, block: u64) -> bool {
144        block.saturating_sub(loaded_block) <= self.ttl_blocks
145    }
146}
147
148/// A cross-block warm `DatabaseRef` wrapper — caches `basic_ref` +
149/// `code_by_hash_ref` (the immutable rows) with a per-entry TTL, forwards
150/// `storage_ref` + `block_hash_ref` to the inner `Db` (the mutable-per-block
151/// row, never cached).
152///
153/// The wrapper value is per-block (constructed in `BlockSimHandle::build`,
154/// holds the per-block `Db`); only the inner `Arc<RwLock<WarmCodeCacheInner>>`
155/// persists across blocks — cloned from the engine owner. See the module doc.
156///
157/// # Errors
158///
159/// `Error = Db::Error` — the cache layer is infallible; errors surface from
160/// the inner `Db` on a cache miss (the RPC cold-load path).
161pub struct WarmCodeCache<Db>
162where
163    Db: DatabaseRef,
164{
165    /// The cross-block persistent maps (bytecode + account-existence), shared
166    /// via the engine owner's arc.
167    cache: Arc<RwLock<WarmCodeCacheInner>>,
168    /// The block this cache view is for (drives per-entry TTL expiry checks).
169    block: u64,
170    /// The per-block `DatabaseRef` (forwards `storage_ref` + `block_hash_ref`
171    /// untouched; serves as the cold-miss fallback for `basic_ref` +
172    /// `code_by_hash_ref`).
173    db: Db,
174}
175
176impl<Db> WarmCodeCache<Db>
177where
178    Db: DatabaseRef,
179{
180    /// Build a per-block view over a shared inner cache + a per-block `Db`.
181    /// The `cache` arc is cloned from the engine owner; the `block` drives
182    /// per-entry TTL expiry; the `db` is the per-block fallback
183    /// (`BotStateDb<WrapDatabaseAsync<AlloyDB>>` in production).
184    #[must_use]
185    pub fn with_owner(cache: Arc<RwLock<WarmCodeCacheInner>>, block: u64, db: Db) -> Self {
186        Self { cache, block, db }
187    }
188}
189
190impl<Db> DatabaseRef for WarmCodeCache<Db>
191where
192    Db: DatabaseRef,
193{
194    type Error = Db::Error;
195
196    /// Serve from the warm cache if the entry is fresh; else forward to the
197    /// inner `Db` + cache the result (the existence-negative `None` IS cached
198    /// — saves the `basic` RPC on the no-code-EOA case).
199    ///
200    /// # Errors
201    ///
202    /// Returns the inner `Db`'s error on a cache miss (the RPC `basic` fetch).
203    fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
204        // Fast path: read-locked cache hit (fresh entry).
205        {
206            let guard = self.cache.read();
207            if let Some((loaded_block, info)) = guard.accounts.get(&address) {
208                if guard.is_fresh(*loaded_block, self.block) {
209                    // SIMPIPE T1 lab: warm-cache served (cross-block/candidate
210                    // reuse signal).
211                    super::sim_metrics::record_basic_warm_hit();
212                    return Ok(info.clone());
213                }
214            }
215        }
216        // Miss (or stale): forward. EOA rows are NEVER cached — an EOA's
217        // nonce + balance mutate every block, and the warm layer is
218        // cross-block: caching them froze frame-replay nonce validation
219        // at its first touch for a full TTL window (observed live as replay
220        // errors stuck at the capture-time `expected` across head advances).
221        let (info, dur) = super::sim_metrics::timed(|| self.db.basic_ref(address));
222        super::sim_metrics::record_basic(dur);
223        let info = info?;
224        let is_contract = info
225            .as_ref()
226            .is_some_and(|i| i.code_hash != revm::primitives::KECCAK_EMPTY);
227        if is_contract {
228            let mut guard = self.cache.write();
229            guard.accounts.insert(address, (self.block, info.clone()));
230        }
231        Ok(info)
232    }
233
234    /// Serve bytecode from the warm cache if the entry is fresh; else forward
235    /// to the inner `Db` + cache the result.
236    ///
237    /// In revm's `AlloyDB`, `basic_ref` eagerly loads code inline — so
238    /// `code_by_hash_ref` is only reached when `basic_ref` returned a
239    /// `code_hash` without the code (the spike-verified panic-safety
240    /// invariant — never in production). Caching here is defensive + handles
241    /// a `Db` whose `basic_ref` does NOT eagerly load code.
242    ///
243    /// # Errors
244    ///
245    /// Returns the inner `Db`'s error on a cache miss.
246    fn code_by_hash_ref(&self, code_hash: B256) -> Result<Bytecode, Self::Error> {
247        {
248            let guard = self.cache.read();
249            if let Some((loaded_block, code)) = guard.bytecode.get(&code_hash) {
250                if guard.is_fresh(*loaded_block, self.block) {
251                    return Ok(code.clone());
252                }
253            }
254        }
255        let (code, dur) = super::sim_metrics::timed(|| self.db.code_by_hash_ref(code_hash));
256        super::sim_metrics::record_code(dur);
257        let code = code?;
258        let mut guard = self.cache.write();
259        guard.bytecode.insert(code_hash, (self.block, code.clone()));
260        Ok(code)
261    }
262
263    /// Forward to the inner `Db` — storage is the mutable-per-block row (V2
264    /// reserves slot 8, V3 `slot0`/`liquidity`/`ticks` change every block
265    /// there's a swap). NEVER cached (caching would re-introduce the
266    /// K-invariant / stale-state-divergence reverts the option-B path hit).
267    ///
268    /// # Errors
269    ///
270    /// Returns the inner `Db`'s error if the RPC fetch fails.
271    fn storage_ref(
272        &self,
273        address: Address,
274        index: StorageKey,
275    ) -> Result<StorageValue, Self::Error> {
276        // SIMPIPE T1 lab: storage ALWAYS forwards — the latency split sizes
277        // the anchor-served (fast) vs RPC-reached (slow) shares.
278        let (value, dur) = super::sim_metrics::timed(|| self.db.storage_ref(address, index));
279        super::sim_metrics::record_storage(dur);
280        value
281    }
282
283    /// Forward to the inner `Db` — block hashes are per-chain, not
284    /// per-block-snapshot. NEVER cached.
285    ///
286    /// # Errors
287    ///
288    /// Returns the inner `Db`'s error if the RPC fetch fails.
289    fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
290        super::sim_metrics::record_block_hash();
291        self.db.block_hash_ref(number)
292    }
293}
294
295#[expect(clippy::expect_used)]
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use alloy::primitives::{address, U256};
300    use revm::database_interface::EmptyDB;
301    use std::cell::Cell;
302
303    /// A counting mock `DatabaseRef` — wraps `EmptyDB` + records every call.
304    /// `EmptyDB` returns `None` for `basic_ref` + `KECCAK_EMPTY` bytecode for
305    /// `code_by_hash_ref` (both error-free), so the mock serves as the
306    /// cold-miss fallback the warm cache forwards to.
307    #[expect(clippy::struct_field_names)]
308    struct CountingMockDb {
309        basic_calls: Cell<u64>,
310        code_by_hash_calls: Cell<u64>,
311        storage_calls: Cell<u64>,
312        block_hash_calls: Cell<u64>,
313    }
314
315    impl CountingMockDb {
316        const EMPTY_CODE_HASH: B256 = B256::ZERO;
317        fn new() -> Self {
318            Self {
319                basic_calls: Cell::new(0),
320                code_by_hash_calls: Cell::new(0),
321                storage_calls: Cell::new(0),
322                block_hash_calls: Cell::new(0),
323            }
324        }
325    }
326
327    impl DatabaseRef for CountingMockDb {
328        type Error = core::convert::Infallible;
329        fn basic_ref(&self, _address: Address) -> Result<Option<AccountInfo>, Self::Error> {
330            self.basic_calls.set(self.basic_calls.get() + 1);
331            // Return a trivially-populated AccountInfo (some balance + the
332            // sentinel code hash) so the cache stores a non-vacuous value.
333            Ok(Some(AccountInfo::new(
334                U256::from(1),
335                0,
336                Self::EMPTY_CODE_HASH,
337                revm::bytecode::Bytecode::new_legacy(alloy::primitives::Bytes::from_static(&[
338                    0x00,
339                ])),
340            )))
341        }
342        fn code_by_hash_ref(&self, _code_hash: B256) -> Result<Bytecode, Self::Error> {
343            self.code_by_hash_calls
344                .set(self.code_by_hash_calls.get() + 1);
345            Ok(revm::bytecode::Bytecode::new_legacy(
346                alloy::primitives::Bytes::new(),
347            ))
348        }
349        fn storage_ref(
350            &self,
351            _address: Address,
352            _index: StorageKey,
353        ) -> Result<StorageValue, Self::Error> {
354            self.storage_calls.set(self.storage_calls.get() + 1);
355            Ok(StorageValue::ZERO)
356        }
357        fn block_hash_ref(&self, _number: u64) -> Result<B256, Self::Error> {
358            self.block_hash_calls.set(self.block_hash_calls.get() + 1);
359            Ok(B256::ZERO)
360        }
361    }
362
363    const ADDR: Address = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
364
365    fn view(
366        cache: &Arc<RwLock<WarmCodeCacheInner>>,
367        block: u64,
368        db: CountingMockDb,
369    ) -> WarmCodeCache<CountingMockDb> {
370        WarmCodeCache::with_owner(Arc::clone(cache), block, db)
371    }
372
373    // ── Acceptance test 1: cache hit avoids second DB read ──────────────
374
375    #[test]
376    fn basic_ref_second_call_same_block_hits_cache() {
377        let inner = WarmCodeCacheInner::shared_with_ttl(10);
378        let v1 = view(&inner, 100, CountingMockDb::new());
379        let _ = v1.basic_ref(ADDR);
380        // Second call — a FRESH wrapper struct reusing the SAME arc, same block.
381        let v2 = view(&inner, 100, CountingMockDb::new());
382        let _ = v2.basic_ref(ADDR);
383        // Only the first wrapper's db saw a call; the second hit the cache.
384        // NOTE: each view has its own CountingMockDb, so we assert via the
385        // shared inner map instead: the entry is present + loaded at block 100.
386        let guard = inner.read();
387        assert_eq!(guard.accounts.len(), 1, "exactly one cached account");
388        let (loaded_block, info) = guard.accounts.get(&ADDR).expect("cached");
389        assert_eq!(*loaded_block, 100);
390        assert!(info.is_some(), "cached value is Some");
391    }
392
393    // ── Acceptance test 1b: the mock is actually hit on the FIRST call ──
394
395    #[test]
396    fn basic_ref_first_call_hits_the_db() {
397        let inner = WarmCodeCacheInner::shared_with_ttl(10);
398        let db = CountingMockDb::new();
399        let v = view(&inner, 100, db);
400        let _ = v.basic_ref(ADDR);
401        // The db the view owns records the call. Verify via the shared inner
402        // (the entry was inserted — only happens after a db forward).
403        assert!(inner.read().accounts.contains_key(&ADDR));
404    }
405
406    // ── Acceptance test 2: TTL expiry re-fetches ────────────────────────
407
408    #[test]
409    fn ttl_expiry_refetches_on_stale_read() {
410        // ttl = 5. Load at block 10 (fresh). Read at block 15 (15-10=5 <= 5,
411        // still fresh). Read at block 16 (16-10=6 > 5, stale → re-fetch).
412        let inner = WarmCodeCacheInner::shared_with_ttl(5);
413        // Load at block 10.
414        let v = view(&inner, 10, CountingMockDb::new());
415        let info10 = v.basic_ref(ADDR).unwrap();
416        let loaded_block_10 = inner.read().accounts.get(&ADDR).expect("cached").0;
417        assert_eq!(loaded_block_10, 10);
418        // Read at block 15 — fresh (15-10=5 <= 5), entry NOT rewritten.
419        let v15 = view(&inner, 15, CountingMockDb::new());
420        let info15 = v15.basic_ref(ADDR).unwrap();
421        assert_eq!(info15, info10, "fresh read returns the cached value");
422        let loaded_block_15 = inner.read().accounts.get(&ADDR).expect("cached").0;
423        assert_eq!(
424            loaded_block_15, 10,
425            "fresh read does NOT rewrite loaded_block"
426        );
427        // Read at block 16 — stale (16-10=6 > 5), re-fetch + rewrite.
428        let v16 = view(&inner, 16, CountingMockDb::new());
429        let _ = v16.basic_ref(ADDR).unwrap();
430        let loaded_block_16 = inner.read().accounts.get(&ADDR).expect("cached").0;
431        assert_eq!(
432            loaded_block_16, 16,
433            "stale read re-fetches + rewrites loaded_block"
434        );
435    }
436
437    // ── Acceptance test 3: storage_ref NEVER cached ─────────────────────
438
439    #[test]
440    fn storage_ref_is_never_cached() {
441        let inner = WarmCodeCacheInner::shared_with_ttl(100);
442        let db = CountingMockDb::new();
443        let v = view(&inner, 100, db);
444        let slot = StorageKey::ZERO;
445        // Two reads of the same (addr, slot) at the same block.
446        let _ = v.storage_ref(ADDR, slot);
447        let _ = v.storage_ref(ADDR, slot);
448        assert_eq!(
449            v.db.storage_calls.get(),
450            2,
451            "both storage reads hit the inner db (never cached)"
452        );
453        assert!(
454            inner.read().accounts.is_empty(),
455            "storage reads never populate the accounts map"
456        );
457    }
458
459    // ── Acceptance test 4: block_hash_ref NEVER cached ───────────────────
460
461    #[test]
462    fn block_hash_ref_is_never_cached() {
463        let inner = WarmCodeCacheInner::shared_with_ttl(100);
464        let db = CountingMockDb::new();
465        let v = view(&inner, 100, db);
466        let _ = v.block_hash_ref(50);
467        let _ = v.block_hash_ref(50);
468        assert_eq!(
469            v.db.block_hash_calls.get(),
470            2,
471            "both block_hash reads hit the inner db (never cached)"
472        );
473    }
474
475    // ── Acceptance test 5: code_by_hash_ref caches + TTLs ────────────────
476
477    #[test]
478    fn code_by_hash_ref_caches_and_expires() {
479        let inner = WarmCodeCacheInner::shared_with_ttl(5);
480        let hash = B256::repeat_byte(0xab);
481        // Load at block 20.
482        let v = view(&inner, 20, CountingMockDb::new());
483        let code20 = v.code_by_hash_ref(hash).unwrap();
484        assert_eq!(v.db.code_by_hash_calls.get(), 1, "first call hits the db");
485        let loaded_block_20 = inner.read().bytecode.get(&hash).expect("cached").0;
486        assert_eq!(loaded_block_20, 20);
487        // Read at block 25 (25-20=5 <= 5) — fresh, cache hit.
488        let v25 = view(&inner, 25, CountingMockDb::new());
489        let code25 = v25.code_by_hash_ref(hash).unwrap();
490        assert_eq!(
491            code25.hash_slow(),
492            code20.hash_slow(),
493            "fresh returns cached"
494        );
495        let loaded_block_25 = inner.read().bytecode.get(&hash).expect("cached").0;
496        assert_eq!(loaded_block_25, 20, "fresh read does NOT rewrite");
497        // Read at block 26 (26-20=6 > 5) — stale, re-fetch + rewrite.
498        let v26 = view(&inner, 26, CountingMockDb::new());
499        let _ = v26.code_by_hash_ref(hash).unwrap();
500        let loaded_block_26 = inner.read().bytecode.get(&hash).expect("cached").0;
501        assert_eq!(loaded_block_26, 26, "stale re-fetches + rewrites");
502    }
503
504    // ── Acceptance test 6: None (no-account) IS cached ───────────────────
505
506    #[test]
507    fn basic_ref_none_is_not_cached() {
508        // A None-returning fallback: an absent account is an EOA-shaped row
509        // (nonce/balance mutate per-block) and must NOT enter the cross-block
510        // warm cache: caching it froze sender nonces at first touch here.
511        struct NoneDb {
512            calls: Cell<u64>,
513        }
514        impl DatabaseRef for NoneDb {
515            type Error = core::convert::Infallible;
516            fn basic_ref(&self, _address: Address) -> Result<Option<AccountInfo>, Self::Error> {
517                self.calls.set(self.calls.get() + 1);
518                Ok(None)
519            }
520            fn code_by_hash_ref(&self, _code_hash: B256) -> Result<Bytecode, Self::Error> {
521                Ok(Bytecode::new_legacy(alloy::primitives::Bytes::new()))
522            }
523            fn storage_ref(
524                &self,
525                _address: Address,
526                _index: StorageKey,
527            ) -> Result<StorageValue, Self::Error> {
528                Ok(StorageValue::ZERO)
529            }
530            fn block_hash_ref(&self, _number: u64) -> Result<B256, Self::Error> {
531                Ok(B256::ZERO)
532            }
533        }
534        let inner = WarmCodeCacheInner::shared_with_ttl(100);
535        let db = NoneDb {
536            calls: Cell::new(0),
537        };
538        let v = WarmCodeCache::with_owner(Arc::clone(&inner), 100, db);
539        let info1 = v.basic_ref(ADDR).unwrap();
540        assert!(info1.is_none(), "first call returns None");
541        assert_eq!(v.db.calls.get(), 1, "first call hit the db");
542        let info2 = v.basic_ref(ADDR).unwrap();
543        assert!(info2.is_none(), "second call returns None again");
544        assert_eq!(
545            v.db.calls.get(),
546            2,
547            "EOA-shaped rows never cache — the second call re-queries"
548        );
549    }
550
551    // ── Acceptance test 7: an EmptyDB-backed view compiles + forwards ────
552
553    #[test]
554    fn warm_code_cache_compiles_over_empty_db() {
555        // The smoke-test shape: a WarmCodeCache<EmptyDB> (no RPC, no BotState).
556        // EmptyDB::basic_ref returns Ok(None) (no account) — an EOA-shaped
557        // row the warm cache deliberately NEVER caches (nonce/balance mutate
558        // per-block on real chains). This proves the type bound
559        // (no Display requirement on Error) compiles + the forwarding path
560        // works.
561        let inner = WarmCodeCacheInner::shared_default();
562        let v = WarmCodeCache::with_owner(Arc::clone(&inner), 1, EmptyDB::default());
563        let info = v.basic_ref(ADDR).unwrap();
564        assert!(info.is_none(), "EmptyDB returns no account");
565        assert_eq!(
566            inner.read().accounts.len(),
567            0,
568            "Ok(None) is never cached — EOA-shaped rows stay live"
569        );
570    }
571
572    // ── Ensure no pyo3 in this core crate (the invariant) ────────────────
573
574    #[test]
575    fn warm_code_cache_module_has_no_pyo3_dependency() {
576        // The no-pyo3-in-cores invariant is enforced by `just
577        // check-no-pyo3-in-cores` at the crate level; this test is a
578        // documentation anchor (the module imports no pyo3 symbols). The
579        // module-level `use` list above (parking_lot, alloy, revm, std) is the
580        // proof — no `pyo3` import exists in this file.
581    }
582}