Skip to main content

ethrex_levm/db/
mod.rs

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