Skip to main content

ethrex_levm/db/
mod.rs

1use crate::{errors::DatabaseError, precompiles::PrecompileCache};
2use ethrex_common::seeded_hash::{SeededHashMap, SeededHashSet};
3use ethrex_common::{
4    Address, H256, U256,
5    types::{AccountState, ChainConfig, Code, CodeMetadata},
6};
7use std::sync::{
8    Arc, OnceLock, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard,
9    atomic::{AtomicU64, Ordering},
10};
11
12pub mod gen_db;
13
14/// Distinct-cold-key count above which a prefetch routes to the sorted, sharded
15/// batch read instead of per-key parallel point-gets. Shared by the account and
16/// storage prefetch gates here and by the merkle trie-node prefetch gate in
17/// `ethrex-blockchain`, so tuning it moves all three together. ~16384 cold
18/// accesses is ~34M gas of cold reads: above ordinary cold blocks, below the
19/// large-state blocks these paths target. Tunable.
20pub const BLOATED_BATCH_THRESHOLD: usize = 16_384;
21
22// Type aliases for cache storage maps, all seeded like `TransientStorage`: transactions
23// choose which addresses and slots get cached, and which deployed code, whose hash its
24// deployer can grind (see `ethrex_common::seeded_hash`).
25type AccountCache = SeededHashMap<Address, AccountState>;
26type StorageCache = SeededHashMap<(Address, H256), U256>;
27type CodeCache = SeededHashMap<H256, Code>;
28/// Touched-key snapshot returned by [`CachingDatabase::touched_keys_where`].
29pub struct TouchedKeys {
30    /// Touched accounts with their storage roots.
31    pub accounts: Vec<(Address, H256)>,
32    /// Touched storage slots as `(account address, slot key)`.
33    pub slots: Vec<(Address, H256)>,
34}
35
36pub trait Database: Send + Sync {
37    fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError>;
38    fn get_storage_value(&self, address: Address, key: H256) -> Result<U256, DatabaseError>;
39    fn get_block_hash(&self, block_number: u64) -> Result<H256, DatabaseError>;
40    fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError>;
41    fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError>;
42    fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError>;
43    /// Access the precompile cache, if available at this database layer.
44    fn precompile_cache(&self) -> Option<&PrecompileCache> {
45        None
46    }
47    /// Batch lookup. Default: loop. Backends with a batched read path (e.g. rocksdb
48    /// `multi_get_cf` on the flat key-value table) should override this and the
49    /// caching layer above will dispatch to it.
50    fn get_account_states_batch(
51        &self,
52        addresses: &[Address],
53    ) -> Result<Vec<AccountState>, DatabaseError> {
54        addresses
55            .iter()
56            .map(|a| self.get_account_state(*a))
57            .collect()
58    }
59    /// Batch bytecode lookup, aligned to `code_hashes`. `None` means the hash is absent
60    /// from the database. Default: loop. Backends with a batched read path (e.g. rocksdb
61    /// `multi_get_cf` on the account-codes table) should override this; the caching layer
62    /// above dispatches to it from [`Self::prefetch_codes`].
63    fn get_account_codes_batch(
64        &self,
65        code_hashes: &[H256],
66    ) -> Result<Vec<Option<Code>>, DatabaseError> {
67        code_hashes
68            .iter()
69            .map(|h| self.get_account_code(*h).map(Some))
70            .collect()
71    }
72    /// Batch storage-slot lookup. Default: loop. Backends with a batched read
73    /// path (e.g. rocksdb `multi_get_cf` on the storage flat key-value table)
74    /// should override this and the caching layer above will dispatch to it.
75    fn get_storage_values_batch(
76        &self,
77        keys: &[(Address, H256)],
78    ) -> Result<Vec<U256>, DatabaseError> {
79        keys.iter()
80            .map(|&(addr, key)| self.get_storage_value(addr, key))
81            .collect()
82    }
83
84    /// Byte budget for warming bytecode ahead of execution, as measured by [`Code::size`].
85    ///
86    /// Bounds the bytecode a block warm reads speculatively, since a block access list
87    /// does not say which of its addresses were accessed for their code. Backends holding
88    /// a bytecode cache report its capacity, which is how much the node was configured to
89    /// keep resident; the default is the floor for a backend without one.
90    fn code_cache_budget_bytes(&self) -> u64 {
91        64 * 1024 * 1024
92    }
93
94    /// Prefetch a batch of bytecodes into the cache. Default: sequential fallback.
95    ///
96    /// Returns the total [`Code::size`] warmed, so a caller warming a whole block can
97    /// hold itself to [`Self::code_cache_budget_bytes`]. An absent hash is not an error:
98    /// warming has nothing to do for it and the executor reports it if reached.
99    fn prefetch_codes(&self, code_hashes: &[H256]) -> Result<u64, DatabaseError> {
100        let mut warmed = 0u64;
101        for &hash in code_hashes {
102            if let Ok(code) = self.get_account_code(hash) {
103                warmed = warmed.saturating_add(u64::try_from(code.size()).unwrap_or(u64::MAX));
104            }
105        }
106        Ok(warmed)
107    }
108    /// Prefetch a batch of accounts into the cache. Default: sequential fallback.
109    fn prefetch_accounts(&self, addresses: &[Address]) -> Result<(), DatabaseError> {
110        for &addr in addresses {
111            self.get_account_state(addr)?;
112        }
113        Ok(())
114    }
115    /// Prefetch a batch of storage slots into the cache. Default: sequential fallback.
116    fn prefetch_storage(&self, keys: &[(Address, H256)]) -> Result<(), DatabaseError> {
117        for &(addr, key) in keys {
118            self.get_storage_value(addr, key)?;
119        }
120        Ok(())
121    }
122}
123
124/// A database wrapper that caches state lookups for parallel pre-warming.
125///
126/// This enables parallel warming workers to share cached data, and allows
127/// the sequential execution phase to reuse warmed state. Reduces redundant
128/// database/trie lookups when multiple transactions touch the same accounts.
129///
130/// Thread-safe via RwLock - optimized for read-heavy concurrent access.
131///
132/// This caching database is inspired by reth's overlay/proof worker cache.
133///
134/// Besides the per-block warmer/executor sharing above, the mempool
135/// prewarmer builds one instance per slot and publishes it across the block
136/// boundary: `execute_block_pipeline` seeds the *next* block's execution
137/// with it when the parent state and fork match (see
138/// `ethrex-blockchain::prewarm`).
139///
140/// # Invariant
141///
142/// Because one instance is shared across the block boundary (and the
143/// prewarmer may still be filling it while the next block executes), every
144/// cached entry must be a pure function of the parent state root. A cache
145/// layer whose entries also depend on the executing block (fork, number,
146/// timestamp, ...) needs a matching handoff guard in
147/// `execute_block_pipeline` — see `precompile_cache`, whose fork-dependent
148/// entries are covered by the fork-equality check there.
149pub struct CachingDatabase {
150    inner: Arc<dyn Database>,
151    /// Cached account states (balance, nonce, code_hash, storage_root)
152    accounts: RwLock<AccountCache>,
153    /// Cached storage values
154    storage: RwLock<StorageCache>,
155    /// Cached contract code
156    code: RwLock<CodeCache>,
157    /// Sum of [`Code::size`] over `code`. Nothing is evicted from the map, so a long-lived
158    /// instance (the mempool prewarmer keeps one for a whole slot) uses this to bound
159    /// how much bytecode it keeps.
160    code_bytes: AtomicU64,
161    /// Shared precompile result cache (warmer populates, executor reuses).
162    /// `None` when the cache is disabled via `BlockchainOptions::precompile_cache_enabled = false`.
163    precompile_cache: Option<PrecompileCache>,
164    /// Cached chain config (constant for the lifetime of this database)
165    chain_config: OnceLock<ChainConfig>,
166}
167
168impl CachingDatabase {
169    pub fn new(inner: Arc<dyn Database>, precompile_cache_enabled: bool) -> Self {
170        Self {
171            inner,
172            accounts: RwLock::new(AccountCache::default()),
173            storage: RwLock::new(StorageCache::default()),
174            code: RwLock::new(CodeCache::default()),
175            code_bytes: AtomicU64::new(0),
176            precompile_cache: precompile_cache_enabled.then(PrecompileCache::new),
177            chain_config: OnceLock::new(),
178        }
179    }
180
181    fn read_accounts(&self) -> Result<RwLockReadGuard<'_, AccountCache>, DatabaseError> {
182        self.accounts.read().map_err(poison_error_to_db_error)
183    }
184
185    fn write_accounts(&self) -> Result<RwLockWriteGuard<'_, AccountCache>, DatabaseError> {
186        self.accounts.write().map_err(poison_error_to_db_error)
187    }
188
189    fn read_storage(&self) -> Result<RwLockReadGuard<'_, StorageCache>, DatabaseError> {
190        self.storage.read().map_err(poison_error_to_db_error)
191    }
192
193    fn write_storage(&self) -> Result<RwLockWriteGuard<'_, StorageCache>, DatabaseError> {
194        self.storage.write().map_err(poison_error_to_db_error)
195    }
196
197    /// Total [`Code::size`] of the bytecode this cache holds.
198    pub fn code_bytes(&self) -> u64 {
199        self.code_bytes.load(Ordering::Relaxed)
200    }
201
202    /// Inserts `code` if absent and counts its size once.
203    fn keep_code(&self, cache: &mut CodeCache, code_hash: H256, code: Code) {
204        if let std::collections::hash_map::Entry::Vacant(entry) = cache.entry(code_hash) {
205            let size = u64::try_from(code.size()).unwrap_or(u64::MAX);
206            entry.insert(code);
207            self.code_bytes.fetch_add(size, Ordering::Relaxed);
208        }
209    }
210
211    fn read_code(&self) -> Result<RwLockReadGuard<'_, CodeCache>, DatabaseError> {
212        self.code.read().map_err(poison_error_to_db_error)
213    }
214
215    fn write_code(&self) -> Result<RwLockWriteGuard<'_, CodeCache>, DatabaseError> {
216        self.code.write().map_err(poison_error_to_db_error)
217    }
218
219    /// Per-slot parallel point-gets, in `missing` order. Warm-optimal fan-out
220    /// for normal-sized prefetch batches; bloated batches use the sorted batch
221    /// multi_get instead (see `prefetch_storage`).
222    #[cfg(feature = "rayon")]
223    fn point_get_storage_many(
224        &self,
225        missing: &[(Address, H256)],
226    ) -> Result<Vec<U256>, DatabaseError> {
227        use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
228        missing
229            .par_iter()
230            .map(|&(addr, key)| self.inner.get_storage_value(addr, key))
231            .collect()
232    }
233
234    #[cfg(not(feature = "rayon"))]
235    fn point_get_storage_many(
236        &self,
237        missing: &[(Address, H256)],
238    ) -> Result<Vec<U256>, DatabaseError> {
239        missing
240            .iter()
241            .map(|&(addr, key)| self.inner.get_storage_value(addr, key))
242            .collect()
243    }
244
245    /// Snapshot of the touched key sets matching the given filters: cached
246    /// accounts (with their storage roots) and cached storage slot keys. The
247    /// filters let a caller that tracks already-processed keys collect only
248    /// the delta, keeping the per-call allocation O(new) while the scan
249    /// stays O(cache).
250    pub fn touched_keys_where(
251        &self,
252        account_filter: &dyn Fn(&Address) -> bool,
253        slot_filter: &dyn Fn(&(Address, H256)) -> bool,
254    ) -> TouchedKeys {
255        let accounts = self
256            .accounts
257            .read()
258            .map(|a| {
259                a.iter()
260                    .filter(|(addr, _)| account_filter(addr))
261                    .map(|(addr, st)| (*addr, st.storage_root))
262                    .collect()
263            })
264            .unwrap_or_default();
265        let storage = self
266            .storage
267            .read()
268            .map(|s| s.keys().filter(|k| slot_filter(k)).copied().collect())
269            .unwrap_or_default();
270        TouchedKeys {
271            accounts,
272            slots: storage,
273        }
274    }
275
276    /// Per-account parallel point-gets, in `missing` order. Warm-optimal fan-out
277    /// for normal-sized prefetch batches; large batches use the sorted sharded
278    /// multi_get instead (see `prefetch_accounts`).
279    #[cfg(feature = "rayon")]
280    fn point_get_accounts_many(
281        &self,
282        missing: &[Address],
283    ) -> Result<Vec<AccountState>, DatabaseError> {
284        use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
285        missing
286            .par_iter()
287            .map(|&addr| self.inner.get_account_state(addr))
288            .collect()
289    }
290
291    #[cfg(not(feature = "rayon"))]
292    fn point_get_accounts_many(
293        &self,
294        missing: &[Address],
295    ) -> Result<Vec<AccountState>, DatabaseError> {
296        missing
297            .iter()
298            .map(|&addr| self.inner.get_account_state(addr))
299            .collect()
300    }
301}
302
303fn poison_error_to_db_error<T>(err: PoisonError<T>) -> DatabaseError {
304    DatabaseError::Custom(format!("Cache lock poisoned: {err}"))
305}
306
307impl Database for CachingDatabase {
308    fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
309        // Check cache first
310        if let Some(state) = self.read_accounts()?.get(&address).copied() {
311            return Ok(state);
312        }
313
314        // Cache miss: query underlying database
315        let state = self.inner.get_account_state(address)?;
316
317        // Populate cache (AccountState is Copy, no clone needed)
318        self.write_accounts()?.insert(address, state);
319
320        Ok(state)
321    }
322
323    fn get_storage_value(&self, address: Address, key: H256) -> Result<U256, DatabaseError> {
324        // Check cache first
325        if let Some(value) = self.read_storage()?.get(&(address, key)).copied() {
326            return Ok(value);
327        }
328
329        // Cache miss: query underlying database
330        let value = self.inner.get_storage_value(address, key)?;
331
332        // Populate cache (U256 is Copy, no clone needed)
333        self.write_storage()?.insert((address, key), value);
334
335        Ok(value)
336    }
337
338    fn get_block_hash(&self, block_number: u64) -> Result<H256, DatabaseError> {
339        // Block hashes don't benefit much from caching here
340        // (they're already cached in StoreVmDatabase)
341        self.inner.get_block_hash(block_number)
342    }
343
344    fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
345        if let Some(cfg) = self.chain_config.get() {
346            return Ok(*cfg);
347        }
348        let cfg = self.inner.get_chain_config()?;
349        // Ignore set error: another thread may have raced us; re-read the winner.
350        let _ = self.chain_config.set(cfg);
351        Ok(*self.chain_config.get().unwrap_or(&cfg))
352    }
353
354    fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
355        // Check cache first
356        if let Some(code) = self.read_code()?.get(&code_hash).cloned() {
357            return Ok(code);
358        }
359
360        // Cache miss: query underlying database
361        let code = self.inner.get_account_code(code_hash)?;
362
363        // Populate cache (Code contains Bytes which is ref-counted, clone is cheap)
364        self.keep_code(&mut *self.write_code()?, code_hash, code.clone());
365
366        Ok(code)
367    }
368
369    fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
370        // Answer from resident code when there is any. The BAL warmer loads the block's
371        // bytecode into this cache, so a length read usually has the answer here under a
372        // shared read lock. Falling straight through would instead take the store's
373        // single mutex-guarded metadata cache, serializing every `EXTCODESIZE` across
374        // the parallel executor's threads.
375        if let Some(code) = self.read_code()?.get(&code_hash) {
376            return Ok(CodeMetadata {
377                length: u64::try_from(code.len()).unwrap_or(u64::MAX),
378            });
379        }
380        self.inner.get_code_metadata(code_hash)
381    }
382
383    fn precompile_cache(&self) -> Option<&PrecompileCache> {
384        self.precompile_cache.as_ref()
385    }
386
387    /// Warms every address through [`Self::prefetch_accounts`], then answers from the
388    /// cache under one read lock. Repeated addresses cost a single database read, and a
389    /// caller that wants the states pays one map lookup each rather than a full
390    /// `get_account_state` round trip per address.
391    ///
392    /// The read lock spans the whole assembly, so callers should keep `addresses` to a
393    /// bounded slice: a writer (an executor filling the same cache) waits on it.
394    fn get_account_states_batch(
395        &self,
396        addresses: &[Address],
397    ) -> Result<Vec<AccountState>, DatabaseError> {
398        self.prefetch_accounts(addresses)?;
399        let cache = self.read_accounts()?;
400        addresses
401            .iter()
402            .map(|addr| {
403                cache.get(addr).copied().ok_or_else(|| {
404                    DatabaseError::Custom(format!("account {addr:?} missing after prefetch"))
405                })
406            })
407            .collect()
408    }
409
410    fn code_cache_budget_bytes(&self) -> u64 {
411        self.inner.code_cache_budget_bytes()
412    }
413
414    /// Fetches the uncached bytecodes in one batch and inserts them, reading each
415    /// distinct hash once however many entries share it.
416    ///
417    /// Returns only the warmed byte count, not the bytecodes: the executor reads code
418    /// through [`Self::get_account_code`], which this turns into a cache hit. Assembling
419    /// a result vector here would hold the write lock that guards every executor code
420    /// read for the length of the batch, to build something a warming caller discards.
421    fn prefetch_codes(&self, code_hashes: &[H256]) -> Result<u64, DatabaseError> {
422        let missing: Vec<H256> = {
423            let cache = self.read_code()?;
424            let mut seen: SeededHashSet<H256> = Default::default();
425            code_hashes
426                .iter()
427                .copied()
428                .filter(|h| !cache.contains_key(h) && seen.insert(*h))
429                .collect()
430        };
431
432        if missing.is_empty() {
433            return Ok(0);
434        }
435
436        let codes = self.inner.get_account_codes_batch(&missing)?;
437        let mut warmed = 0u64;
438        let mut cache = self.write_code()?;
439        for (hash, code) in missing.into_iter().zip(codes.into_iter()) {
440            // An absent hash has nothing to warm; the executor reports it if reached.
441            if let Some(code) = code {
442                warmed = warmed.saturating_add(u64::try_from(code.size()).unwrap_or(u64::MAX));
443                self.keep_code(&mut cache, hash, code);
444            }
445        }
446        Ok(warmed)
447    }
448
449    fn prefetch_accounts(&self, addresses: &[Address]) -> Result<(), DatabaseError> {
450        // Filter out already-cached addresses before issuing the batch read.
451        let missing: Vec<Address> = {
452            let cache = self.read_accounts()?;
453            addresses
454                .iter()
455                .copied()
456                .filter(|a| !cache.contains_key(a))
457                .collect()
458        };
459        if missing.is_empty() {
460            return Ok(());
461        }
462        // Same gate as `prefetch_storage`: a large set of distinct COLD accounts is
463        // queue-depth bound. The inner batch path on the rocksdb-backed
464        // StoreVmDatabase used a single multi_get (queue depth 1, async_io off),
465        // which collapses on cold account-heavy blocks (coldbench: ~13x slower than
466        // the sharded batch). Route large/cold sets to the (now sharded) batch and
467        // small/warm sets to parallel point-gets. The gate counts MISSING (cold)
468        // accounts, so warm blocks stay on the point-get path however many accounts
469        // they touch. See `BLOATED_BATCH_THRESHOLD`.
470        let states = if missing.len() >= BLOATED_BATCH_THRESHOLD {
471            self.inner.get_account_states_batch(&missing)?
472        } else {
473            self.point_get_accounts_many(&missing)?
474        };
475        let mut cache = self.write_accounts()?;
476        for (addr, state) in missing.into_iter().zip(states.into_iter()) {
477            cache.entry(addr).or_insert(state);
478        }
479        Ok(())
480    }
481
482    fn prefetch_storage(&self, keys: &[(Address, H256)]) -> Result<(), DatabaseError> {
483        // Filter out already-cached slots before issuing the batch read.
484        let missing: Vec<(Address, H256)> = {
485            let cache = self.read_storage()?;
486            keys.iter()
487                .copied()
488                .filter(|k| !cache.contains_key(k))
489                .collect()
490        };
491        if missing.is_empty() {
492            return Ok(());
493        }
494        // Warm is the common case: a normal block touches relatively few storage
495        // slots and they are usually cache-resident, where per-slot point-gets
496        // (parallel fan-out) are warm-optimal. A block that instead reads a large
497        // number of distinct COLD slots is queue-depth bound: a per-slot fan-out
498        // is capped at ncpu reads in flight, and a single serial multi_get runs
499        // at queue depth 1 (async_io is off in our build), so cold throughput
500        // collapses (a sorted serial multi_get regressed bloated SLOAD ~4.5x).
501        // The sharded batch path restores it (sorted shards share RocksDB data
502        // blocks and run at high queue depth) and hardens validation against
503        // storage-bloat DoS. The gate counts MISSING (uncached, i.e. cold) slots,
504        // not total accesses, so a warm block never reaches it however many slots
505        // it touches; that is what keeps the path off normal traffic. The sharded
506        // win is already present once a block has this many cold slots (a cold
507        // benchmark shows ~1.4x at 16k and growing with size), while the warm cost
508        // it trades against is a few ms and effectively cannot fire, since warm
509        // slots are not counted here. See `BLOATED_BATCH_THRESHOLD`.
510        let values = if missing.len() >= BLOATED_BATCH_THRESHOLD {
511            // Dispatch to inner's batch path. For the rocksdb-backed
512            // StoreVmDatabase this is a sharded parallel multi_get on
513            // STORAGE_FLATKEYVALUE for the FKV-covered subset; the default impl
514            // loops for other backends.
515            self.inner.get_storage_values_batch(&missing)?
516        } else {
517            self.point_get_storage_many(&missing)?
518        };
519        let mut cache = self.write_storage()?;
520        for (key, value) in missing.into_iter().zip(values.into_iter()) {
521            cache.entry(key).or_insert(value);
522        }
523        Ok(())
524    }
525}
526
527#[cfg(test)]
528#[allow(
529    clippy::unwrap_used,
530    clippy::indexing_slicing,
531    clippy::arithmetic_side_effects,
532    clippy::as_conversions
533)]
534mod code_bytes_tests {
535    use super::*;
536    use bytes::Bytes;
537
538    /// Serves a distinct 4 KiB contract for every code hash.
539    struct BigCodeStore;
540
541    fn code_for(hash: H256) -> Code {
542        let mut bytecode = vec![0x5b; 4096];
543        bytecode[..32].copy_from_slice(hash.as_bytes());
544        Code::from_bytecode_unchecked(Bytes::from(bytecode), hash)
545    }
546
547    impl Database for BigCodeStore {
548        fn get_account_state(&self, _: Address) -> Result<AccountState, DatabaseError> {
549            Ok(AccountState::default())
550        }
551        fn get_storage_value(&self, _: Address, _: H256) -> Result<U256, DatabaseError> {
552            Ok(U256::zero())
553        }
554        fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
555            Ok(H256::zero())
556        }
557        fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
558            Ok(ChainConfig::default())
559        }
560        fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
561            Ok(code_for(code_hash))
562        }
563        fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
564            Ok(CodeMetadata {
565                length: code_for(code_hash).len() as u64,
566            })
567        }
568    }
569
570    /// `code_bytes` SHALL count every distinct code once, whether it was read on demand
571    /// or prefetched, so the prewarmer's stop condition sees what the map really holds.
572    #[test]
573    fn code_bytes_counts_each_distinct_code_once() {
574        let cache = CachingDatabase::new(Arc::new(BigCodeStore), false);
575        let size = code_for(H256::zero()).size() as u64;
576        let hashes: Vec<H256> = (1..=4u64).map(H256::from_low_u64_be).collect();
577
578        cache.get_account_code(hashes[0]).unwrap();
579        cache.get_account_code(hashes[0]).unwrap();
580        assert_eq!(cache.code_bytes(), size);
581
582        cache.prefetch_codes(&hashes).unwrap();
583        cache.prefetch_codes(&hashes).unwrap();
584        assert_eq!(cache.code_bytes(), 4 * size);
585        assert_eq!(cache.read_code().unwrap().len(), 4);
586    }
587}
588
589#[cfg(test)]
590#[allow(clippy::expect_used)]
591mod seeded_cache_tests {
592    use super::*;
593    use std::hash::BuildHasher;
594
595    /// Fails if `state` hashes a key like a fresh instance of its type does, as every
596    /// instance of an unseeded hasher does.
597    #[track_caller]
598    fn assert_seeded<S: BuildHasher + Default>(state: &S) {
599        let key = H256::repeat_byte(0x5a);
600        assert_ne!(state.hash_one(key), S::default().hash_one(key));
601    }
602
603    struct EmptyStore;
604
605    impl Database for EmptyStore {
606        fn get_account_state(&self, _: Address) -> Result<AccountState, DatabaseError> {
607            Ok(AccountState::default())
608        }
609        fn get_storage_value(&self, _: Address, _: H256) -> Result<U256, DatabaseError> {
610            Ok(U256::zero())
611        }
612        fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
613            Ok(H256::zero())
614        }
615        fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
616            Ok(ChainConfig::default())
617        }
618        fn get_account_code(&self, _: H256) -> Result<Code, DatabaseError> {
619            Ok(Code::default())
620        }
621        fn get_code_metadata(&self, _: H256) -> Result<CodeMetadata, DatabaseError> {
622            Ok(CodeMetadata { length: 0 })
623        }
624    }
625
626    /// Transactions choose which accounts and slots the warming cache holds, and which
627    /// deployed code it loads, so none of its maps may hash without a seed.
628    #[test]
629    fn warming_caches_are_seeded() {
630        let cache = CachingDatabase::new(Arc::new(EmptyStore), false);
631        assert_seeded(cache.read_accounts().expect("account cache lock").hasher());
632        assert_seeded(cache.read_storage().expect("storage cache lock").hasher());
633        assert_seeded(cache.read_code().expect("code cache lock").hasher());
634    }
635}