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};
6#[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
7use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
8use rustc_hash::FxHashMap;
9use std::sync::{Arc, OnceLock, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
10
11pub mod gen_db;
12
13// Type aliases for cache storage maps
14type AccountCache = FxHashMap<Address, AccountState>;
15type StorageCache = FxHashMap<(Address, H256), U256>;
16type CodeCache = FxHashMap<H256, Code>;
17/// Touched-key snapshot returned by [`CachingDatabase::touched_keys_where`].
18pub struct TouchedKeys {
19    /// Touched accounts with their storage roots.
20    pub accounts: Vec<(Address, H256)>,
21    /// Touched storage slots as `(account address, slot key)`.
22    pub slots: Vec<(Address, H256)>,
23}
24
25pub trait Database: Send + Sync {
26    fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError>;
27    fn get_storage_value(&self, address: Address, key: H256) -> Result<U256, DatabaseError>;
28    fn get_block_hash(&self, block_number: u64) -> Result<H256, DatabaseError>;
29    fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError>;
30    fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError>;
31    fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError>;
32    /// Access the precompile cache, if available at this database layer.
33    fn precompile_cache(&self) -> Option<&PrecompileCache> {
34        None
35    }
36    /// Batch lookup. Default: loop. Backends with a batched read path (e.g. rocksdb
37    /// `multi_get_cf` on the flat key-value table) should override this and the
38    /// caching layer above will dispatch to it.
39    fn get_account_states_batch(
40        &self,
41        addresses: &[Address],
42    ) -> Result<Vec<AccountState>, DatabaseError> {
43        addresses
44            .iter()
45            .map(|a| self.get_account_state(*a))
46            .collect()
47    }
48    /// Prefetch a batch of accounts into the cache. Default: sequential fallback.
49    fn prefetch_accounts(&self, addresses: &[Address]) -> Result<(), DatabaseError> {
50        for &addr in addresses {
51            self.get_account_state(addr)?;
52        }
53        Ok(())
54    }
55    /// Prefetch a batch of storage slots into the cache. Default: sequential fallback.
56    fn prefetch_storage(&self, keys: &[(Address, H256)]) -> Result<(), DatabaseError> {
57        for &(addr, key) in keys {
58            self.get_storage_value(addr, key)?;
59        }
60        Ok(())
61    }
62}
63
64/// A database wrapper that caches state lookups for parallel pre-warming.
65///
66/// This enables parallel warming workers to share cached data, and allows
67/// the sequential execution phase to reuse warmed state. Reduces redundant
68/// database/trie lookups when multiple transactions touch the same accounts.
69///
70/// Thread-safe via RwLock - optimized for read-heavy concurrent access.
71///
72/// This caching database is inspired by reth's overlay/proof worker cache.
73///
74/// Besides the per-block warmer/executor sharing above, the mempool
75/// prewarmer builds one instance per slot and publishes it across the block
76/// boundary: `execute_block_pipeline` seeds the *next* block's execution
77/// with it when the parent state and fork match (see
78/// `ethrex-blockchain::prewarm`).
79///
80/// # Invariant
81///
82/// Because one instance is shared across the block boundary (and the
83/// prewarmer may still be filling it while the next block executes), every
84/// cached entry must be a pure function of the parent state root. A cache
85/// layer whose entries also depend on the executing block (fork, number,
86/// timestamp, ...) needs a matching handoff guard in
87/// `execute_block_pipeline` — see `precompile_cache`, whose fork-dependent
88/// entries are covered by the fork-equality check there.
89pub struct CachingDatabase {
90    inner: Arc<dyn Database>,
91    /// Cached account states (balance, nonce, code_hash, storage_root)
92    accounts: RwLock<AccountCache>,
93    /// Cached storage values
94    storage: RwLock<StorageCache>,
95    /// Cached contract code
96    code: RwLock<CodeCache>,
97    /// Shared precompile result cache (warmer populates, executor reuses).
98    /// `None` when the cache is disabled via `BlockchainOptions::precompile_cache_enabled = false`.
99    precompile_cache: Option<PrecompileCache>,
100    /// Cached chain config (constant for the lifetime of this database)
101    chain_config: OnceLock<ChainConfig>,
102}
103
104impl CachingDatabase {
105    pub fn new(inner: Arc<dyn Database>, precompile_cache_enabled: bool) -> Self {
106        Self {
107            inner,
108            accounts: RwLock::new(FxHashMap::default()),
109            storage: RwLock::new(FxHashMap::default()),
110            code: RwLock::new(FxHashMap::default()),
111            precompile_cache: precompile_cache_enabled.then(PrecompileCache::new),
112            chain_config: OnceLock::new(),
113        }
114    }
115
116    fn read_accounts(&self) -> Result<RwLockReadGuard<'_, AccountCache>, DatabaseError> {
117        self.accounts.read().map_err(poison_error_to_db_error)
118    }
119
120    fn write_accounts(&self) -> Result<RwLockWriteGuard<'_, AccountCache>, DatabaseError> {
121        self.accounts.write().map_err(poison_error_to_db_error)
122    }
123
124    fn read_storage(&self) -> Result<RwLockReadGuard<'_, StorageCache>, DatabaseError> {
125        self.storage.read().map_err(poison_error_to_db_error)
126    }
127
128    fn write_storage(&self) -> Result<RwLockWriteGuard<'_, StorageCache>, DatabaseError> {
129        self.storage.write().map_err(poison_error_to_db_error)
130    }
131
132    fn read_code(&self) -> Result<RwLockReadGuard<'_, CodeCache>, DatabaseError> {
133        self.code.read().map_err(poison_error_to_db_error)
134    }
135
136    fn write_code(&self) -> Result<RwLockWriteGuard<'_, CodeCache>, DatabaseError> {
137        self.code.write().map_err(poison_error_to_db_error)
138    }
139
140    /// Snapshot of the touched key sets matching the given filters: cached
141    /// accounts (with their storage roots) and cached storage slot keys. The
142    /// filters let a caller that tracks already-processed keys collect only
143    /// the delta, keeping the per-call allocation O(new) while the scan
144    /// stays O(cache).
145    pub fn touched_keys_where(
146        &self,
147        account_filter: &dyn Fn(&Address) -> bool,
148        slot_filter: &dyn Fn(&(Address, H256)) -> bool,
149    ) -> TouchedKeys {
150        let accounts = self
151            .accounts
152            .read()
153            .map(|a| {
154                a.iter()
155                    .filter(|(addr, _)| account_filter(addr))
156                    .map(|(addr, st)| (*addr, st.storage_root))
157                    .collect()
158            })
159            .unwrap_or_default();
160        let storage = self
161            .storage
162            .read()
163            .map(|s| s.keys().filter(|k| slot_filter(k)).copied().collect())
164            .unwrap_or_default();
165        TouchedKeys {
166            accounts,
167            slots: storage,
168        }
169    }
170}
171
172fn poison_error_to_db_error<T>(err: PoisonError<T>) -> DatabaseError {
173    DatabaseError::Custom(format!("Cache lock poisoned: {err}"))
174}
175
176impl Database for CachingDatabase {
177    fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
178        // Check cache first
179        if let Some(state) = self.read_accounts()?.get(&address).copied() {
180            return Ok(state);
181        }
182
183        // Cache miss: query underlying database
184        let state = self.inner.get_account_state(address)?;
185
186        // Populate cache (AccountState is Copy, no clone needed)
187        self.write_accounts()?.insert(address, state);
188
189        Ok(state)
190    }
191
192    fn get_storage_value(&self, address: Address, key: H256) -> Result<U256, DatabaseError> {
193        // Check cache first
194        if let Some(value) = self.read_storage()?.get(&(address, key)).copied() {
195            return Ok(value);
196        }
197
198        // Cache miss: query underlying database
199        let value = self.inner.get_storage_value(address, key)?;
200
201        // Populate cache (U256 is Copy, no clone needed)
202        self.write_storage()?.insert((address, key), value);
203
204        Ok(value)
205    }
206
207    fn get_block_hash(&self, block_number: u64) -> Result<H256, DatabaseError> {
208        // Block hashes don't benefit much from caching here
209        // (they're already cached in StoreVmDatabase)
210        self.inner.get_block_hash(block_number)
211    }
212
213    fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
214        if let Some(cfg) = self.chain_config.get() {
215            return Ok(*cfg);
216        }
217        let cfg = self.inner.get_chain_config()?;
218        // Ignore set error: another thread may have raced us; re-read the winner.
219        let _ = self.chain_config.set(cfg);
220        Ok(*self.chain_config.get().unwrap_or(&cfg))
221    }
222
223    fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
224        // Check cache first
225        if let Some(code) = self.read_code()?.get(&code_hash).cloned() {
226            return Ok(code);
227        }
228
229        // Cache miss: query underlying database
230        let code = self.inner.get_account_code(code_hash)?;
231
232        // Populate cache (Code contains Bytes which is ref-counted, clone is cheap)
233        self.write_code()?.insert(code_hash, code.clone());
234
235        Ok(code)
236    }
237
238    fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
239        // Delegate directly to the underlying database.
240        // The underlying Store already has its own code_metadata_cache,
241        // so we don't need to duplicate caching here.
242        self.inner.get_code_metadata(code_hash)
243    }
244
245    fn precompile_cache(&self) -> Option<&PrecompileCache> {
246        self.precompile_cache.as_ref()
247    }
248
249    fn prefetch_accounts(&self, addresses: &[Address]) -> Result<(), DatabaseError> {
250        // Filter out already-cached addresses before issuing the batch read.
251        let missing: Vec<Address> = {
252            let cache = self.read_accounts()?;
253            addresses
254                .iter()
255                .copied()
256                .filter(|a| !cache.contains_key(a))
257                .collect()
258        };
259        if missing.is_empty() {
260            return Ok(());
261        }
262        // Dispatch to inner's batch path. For the rocksdb-backed StoreVmDatabase
263        // this collapses into a single multi_get on ACCOUNT_FLATKEYVALUE for the
264        // FKV-covered subset; default impl loops for other backends.
265        let states = self.inner.get_account_states_batch(&missing)?;
266        let mut cache = self.write_accounts()?;
267        for (addr, state) in missing.into_iter().zip(states.into_iter()) {
268            cache.entry(addr).or_insert(state);
269        }
270        Ok(())
271    }
272
273    #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
274    fn prefetch_storage(&self, keys: &[(Address, H256)]) -> Result<(), DatabaseError> {
275        // Fetch from inner in parallel (no lock contention), then single write-lock to populate cache.
276        let fetched: Vec<((Address, H256), U256)> = keys
277            .par_iter()
278            .map(|&(addr, key)| {
279                self.inner
280                    .get_storage_value(addr, key)
281                    .map(|v| ((addr, key), v))
282            })
283            .collect::<Result<_, _>>()?;
284        let mut cache = self.write_storage()?;
285        for (key, value) in fetched {
286            cache.entry(key).or_insert(value);
287        }
288        Ok(())
289    }
290}