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
18pub trait Database: Send + Sync {
19    fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError>;
20    fn get_storage_value(&self, address: Address, key: H256) -> Result<U256, DatabaseError>;
21    fn get_block_hash(&self, block_number: u64) -> Result<H256, DatabaseError>;
22    fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError>;
23    fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError>;
24    fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError>;
25    /// Access the precompile cache, if available at this database layer.
26    fn precompile_cache(&self) -> Option<&PrecompileCache> {
27        None
28    }
29    /// Prefetch a batch of accounts into the cache. Default: sequential fallback.
30    fn prefetch_accounts(&self, addresses: &[Address]) -> Result<(), DatabaseError> {
31        for &addr in addresses {
32            self.get_account_state(addr)?;
33        }
34        Ok(())
35    }
36    /// Prefetch a batch of storage slots into the cache. Default: sequential fallback.
37    fn prefetch_storage(&self, keys: &[(Address, H256)]) -> Result<(), DatabaseError> {
38        for &(addr, key) in keys {
39            self.get_storage_value(addr, key)?;
40        }
41        Ok(())
42    }
43}
44
45/// A database wrapper that caches state lookups for parallel pre-warming.
46///
47/// This enables parallel warming workers to share cached data, and allows
48/// the sequential execution phase to reuse warmed state. Reduces redundant
49/// database/trie lookups when multiple transactions touch the same accounts.
50///
51/// Thread-safe via RwLock - optimized for read-heavy concurrent access.
52///
53/// This caching database is inspired by reth's overlay/proof worker cache.
54pub struct CachingDatabase {
55    inner: Arc<dyn Database>,
56    /// Cached account states (balance, nonce, code_hash, storage_root)
57    accounts: RwLock<AccountCache>,
58    /// Cached storage values
59    storage: RwLock<StorageCache>,
60    /// Cached contract code
61    code: RwLock<CodeCache>,
62    /// Shared precompile result cache (warmer populates, executor reuses).
63    /// `None` when the cache is disabled via `BlockchainOptions::precompile_cache_enabled = false`.
64    precompile_cache: Option<PrecompileCache>,
65    /// Cached chain config (constant for the lifetime of this database)
66    chain_config: OnceLock<ChainConfig>,
67}
68
69impl CachingDatabase {
70    pub fn new(inner: Arc<dyn Database>, precompile_cache_enabled: bool) -> Self {
71        Self {
72            inner,
73            accounts: RwLock::new(FxHashMap::default()),
74            storage: RwLock::new(FxHashMap::default()),
75            code: RwLock::new(FxHashMap::default()),
76            precompile_cache: precompile_cache_enabled.then(PrecompileCache::new),
77            chain_config: OnceLock::new(),
78        }
79    }
80
81    fn read_accounts(&self) -> Result<RwLockReadGuard<'_, AccountCache>, DatabaseError> {
82        self.accounts.read().map_err(poison_error_to_db_error)
83    }
84
85    fn write_accounts(&self) -> Result<RwLockWriteGuard<'_, AccountCache>, DatabaseError> {
86        self.accounts.write().map_err(poison_error_to_db_error)
87    }
88
89    fn read_storage(&self) -> Result<RwLockReadGuard<'_, StorageCache>, DatabaseError> {
90        self.storage.read().map_err(poison_error_to_db_error)
91    }
92
93    fn write_storage(&self) -> Result<RwLockWriteGuard<'_, StorageCache>, DatabaseError> {
94        self.storage.write().map_err(poison_error_to_db_error)
95    }
96
97    fn read_code(&self) -> Result<RwLockReadGuard<'_, CodeCache>, DatabaseError> {
98        self.code.read().map_err(poison_error_to_db_error)
99    }
100
101    fn write_code(&self) -> Result<RwLockWriteGuard<'_, CodeCache>, DatabaseError> {
102        self.code.write().map_err(poison_error_to_db_error)
103    }
104}
105
106fn poison_error_to_db_error<T>(err: PoisonError<T>) -> DatabaseError {
107    DatabaseError::Custom(format!("Cache lock poisoned: {err}"))
108}
109
110impl Database for CachingDatabase {
111    fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
112        // Check cache first
113        if let Some(state) = self.read_accounts()?.get(&address).copied() {
114            return Ok(state);
115        }
116
117        // Cache miss: query underlying database
118        let state = self.inner.get_account_state(address)?;
119
120        // Populate cache (AccountState is Copy, no clone needed)
121        self.write_accounts()?.insert(address, state);
122
123        Ok(state)
124    }
125
126    fn get_storage_value(&self, address: Address, key: H256) -> Result<U256, DatabaseError> {
127        // Check cache first
128        if let Some(value) = self.read_storage()?.get(&(address, key)).copied() {
129            return Ok(value);
130        }
131
132        // Cache miss: query underlying database
133        let value = self.inner.get_storage_value(address, key)?;
134
135        // Populate cache (U256 is Copy, no clone needed)
136        self.write_storage()?.insert((address, key), value);
137
138        Ok(value)
139    }
140
141    fn get_block_hash(&self, block_number: u64) -> Result<H256, DatabaseError> {
142        // Block hashes don't benefit much from caching here
143        // (they're already cached in StoreVmDatabase)
144        self.inner.get_block_hash(block_number)
145    }
146
147    fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
148        if let Some(cfg) = self.chain_config.get() {
149            return Ok(*cfg);
150        }
151        let cfg = self.inner.get_chain_config()?;
152        // Ignore set error: another thread may have raced us; re-read the winner.
153        let _ = self.chain_config.set(cfg);
154        Ok(*self.chain_config.get().unwrap_or(&cfg))
155    }
156
157    fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
158        // Check cache first
159        if let Some(code) = self.read_code()?.get(&code_hash).cloned() {
160            return Ok(code);
161        }
162
163        // Cache miss: query underlying database
164        let code = self.inner.get_account_code(code_hash)?;
165
166        // Populate cache (Code contains Bytes which is ref-counted, clone is cheap)
167        self.write_code()?.insert(code_hash, code.clone());
168
169        Ok(code)
170    }
171
172    fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
173        // Delegate directly to the underlying database.
174        // The underlying Store already has its own code_metadata_cache,
175        // so we don't need to duplicate caching here.
176        self.inner.get_code_metadata(code_hash)
177    }
178
179    fn precompile_cache(&self) -> Option<&PrecompileCache> {
180        self.precompile_cache.as_ref()
181    }
182
183    #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
184    fn prefetch_accounts(&self, addresses: &[Address]) -> Result<(), DatabaseError> {
185        // Fetch from inner in parallel (no lock contention), then single write-lock to populate cache.
186        let fetched: Vec<(Address, AccountState)> = addresses
187            .par_iter()
188            .map(|&addr| self.inner.get_account_state(addr).map(|s| (addr, s)))
189            .collect::<Result<_, _>>()?;
190        let mut cache = self.write_accounts()?;
191        for (addr, state) in fetched {
192            cache.entry(addr).or_insert(state);
193        }
194        Ok(())
195    }
196
197    #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
198    fn prefetch_storage(&self, keys: &[(Address, H256)]) -> Result<(), DatabaseError> {
199        // Fetch from inner in parallel (no lock contention), then single write-lock to populate cache.
200        let fetched: Vec<((Address, H256), U256)> = keys
201            .par_iter()
202            .map(|&(addr, key)| {
203                self.inner
204                    .get_storage_value(addr, key)
205                    .map(|v| ((addr, key), v))
206            })
207            .collect::<Result<_, _>>()?;
208        let mut cache = self.write_storage()?;
209        for (key, value) in fetched {
210            cache.entry(key).or_insert(value);
211        }
212        Ok(())
213    }
214}