├── .gitignore ├── Cargo.toml ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rethdb" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | time = "0.3.36" 8 | reth-chainspec = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.1" } 9 | reth-db = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.1" } 10 | reth-db-api = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.1" } 11 | reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.1" } 12 | reth-blockchain-tree = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.1" } 13 | reth-primitives = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.1" } 14 | reth-provider = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.1" } 15 | eyre = "0.6.12" 16 | revm = { version = "11.0.0" } 17 | once_cell = "1.19.0" 18 | dashmap = "6.0.1" 19 | alloy-chains = "0.1.14" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RethDB 2 | 3 | A thread-safe implementation of revm `Database` that directly fetch data from Reth database. 4 | 5 | ## Usage 6 | 7 | ```rust 8 | 9 | let db = RethDB::new( 10 | Chain::mainnet(), "path/to/reth/db", Some(block_number) 11 | ); 12 | 13 | // use it in the same way as other revm DBs. 14 | ``` 15 | 16 | ## Performance 17 | 18 | For replaying blocks, approximately 40x faster than AlloyDB using IPC and 100x faster than AlloyDB using HTTP RPC. -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use reth_primitives::StorageKey; 2 | use std::path::Path; 3 | use std::sync::{Arc, Mutex}; 4 | use std::sync::atomic::{AtomicU64, Ordering}; 5 | use std::time::Duration; 6 | use alloy_chains::{Chain, NamedChain}; 7 | use dashmap::{DashMap, Entry}; 8 | use eyre::{bail, Context}; 9 | use reth_db::{DatabaseEnv, open_db_read_only}; 10 | use reth_provider::{BlockNumReader, DatabaseProviderRO, ProviderFactory, StateProviderBox}; 11 | use once_cell::sync::Lazy; 12 | use reth_chainspec::ChainSpecBuilder; 13 | use reth_db::mdbx::DatabaseArguments; 14 | use reth_db_api::models::ClientVersion; 15 | use reth_provider::providers::StaticFileProvider; 16 | use revm::{Database, DatabaseRef, interpreter}; 17 | use revm::primitives::{AccountInfo, Address, B256, Bytecode, KECCAK_EMPTY, U256}; 18 | 19 | type DBFactory = ProviderFactory>; 20 | type Lock = Arc>; 21 | 22 | static DB_LOCK: Lazy> = Lazy::new(|| Default::default()); 23 | 24 | pub struct RethDB { 25 | provider: StateProviderBox, 26 | 27 | measure_rpc_time: bool, 28 | cumulative_rpc_time: AtomicU64, 29 | } 30 | 31 | impl RethDB { 32 | pub fn new(chain: Chain, db_path: &str, block: Option) -> eyre::Result { 33 | let (database, lock) = match DB_LOCK.entry(db_path.to_string()) { 34 | Entry::Occupied(entry) => entry.get().clone(), 35 | Entry::Vacant(entry) => { 36 | let db_path = Path::new(db_path); 37 | 38 | let db_env = open_db_read_only( 39 | db_path.join("db").as_path(), 40 | DatabaseArguments::new(ClientVersion::default()), 41 | ) 42 | .context("fail to open db env")?; 43 | let db_env = Arc::new(db_env); 44 | 45 | let static_file_provider = StaticFileProvider::read_only(db_path.join("static_files")) 46 | .context("fail to open static file provider")?; 47 | 48 | let spec = match chain.named() { 49 | Some(NamedChain::Mainnet) => ChainSpecBuilder::mainnet().build(), 50 | _ => panic!("unsupported chain"), 51 | }; 52 | let spec = Arc::new(spec); 53 | 54 | let database = ProviderFactory::new(db_env.clone(), spec.clone(), static_file_provider); 55 | 56 | let lock = Lock::default(); 57 | entry.insert((database.clone(), lock.clone())); 58 | 59 | (database, lock) 60 | } 61 | }; 62 | 63 | // database.provider will initiate a new transaction to the DB, and it's not 64 | // allowed to be shared between threads. So we need a lock to make sure we open 65 | // the transaction sequentially. 66 | let _guard = lock.lock().unwrap(); 67 | 68 | let db_provider = database.provider().context("fail to get provider")?; 69 | 70 | let state_provider = if let Some(block) = block { 71 | Self::get_state_provider_with_retry(db_provider, block, 500, Duration::from_millis(10))? 72 | } else { 73 | database.latest().context("fail to get latest state provider")? 74 | }; 75 | 76 | Ok(Self { 77 | provider: state_provider, 78 | 79 | measure_rpc_time: false, 80 | cumulative_rpc_time: AtomicU64::new(0), 81 | }) 82 | } 83 | 84 | fn get_state_provider_with_retry( 85 | db_provider: DatabaseProviderRO>, 86 | block: u64, 87 | mut max_retries: usize, 88 | interval: Duration, 89 | ) -> eyre::Result { 90 | max_retries += 1; 91 | 92 | while max_retries > 0 { 93 | let best_block = db_provider 94 | .best_block_number() 95 | .context("fail to get best block number")?; 96 | 97 | if block > best_block { 98 | std::thread::sleep(interval); 99 | max_retries -= 1; 100 | continue; 101 | } 102 | 103 | let s_provider = db_provider 104 | .state_provider_by_block_number(block) 105 | .context("fail to state provider by block")?; 106 | 107 | return Ok(s_provider); 108 | } 109 | 110 | bail!("timeout waiting for latest block") 111 | } 112 | 113 | fn request(&self, f: F) -> Result 114 | where 115 | F: FnOnce(&StateProviderBox) -> Result, 116 | { 117 | if self.measure_rpc_time { 118 | let start = std::time::Instant::now(); 119 | let result = f(&self.provider); 120 | 121 | let elapsed = start.elapsed().as_nanos() as u64; 122 | self.cumulative_rpc_time.fetch_add(elapsed, Ordering::Relaxed); 123 | 124 | result 125 | } else { 126 | let result = f(&self.provider); 127 | 128 | result 129 | } 130 | } 131 | 132 | pub fn set_measure_rpc_time(&mut self, enable: bool) { 133 | self.measure_rpc_time = enable; 134 | } 135 | 136 | pub fn get_rpc_time(&self) -> Duration { 137 | Duration::from_nanos(self.cumulative_rpc_time.load(Ordering::Relaxed)) 138 | } 139 | 140 | pub fn reset_rpc_time(&mut self) { 141 | self.cumulative_rpc_time.store(0, Ordering::Relaxed); 142 | } 143 | } 144 | 145 | impl Database for RethDB { 146 | type Error = eyre::Error; 147 | 148 | fn basic(&mut self, address: Address) -> Result, Self::Error> { 149 | Self::basic_ref(self, address) 150 | } 151 | 152 | fn code_by_hash(&mut self, _code_hash: B256) -> Result { 153 | panic!("This should not be called, as the code is already loaded"); 154 | } 155 | 156 | fn storage(&mut self, address: Address, index: U256) -> Result { 157 | Self::storage_ref(self, address, index) 158 | } 159 | 160 | fn block_hash(&mut self, number: u64) -> Result { 161 | Self::block_hash_ref(self, number) 162 | } 163 | } 164 | 165 | impl DatabaseRef for RethDB { 166 | type Error = eyre::Error; 167 | 168 | fn basic_ref(&self, address: Address) -> Result, Self::Error> { 169 | let account = self 170 | .request(|provider| provider.basic_account(address))? 171 | .unwrap_or_default(); 172 | let code = self 173 | .request(|provider| provider.account_code(address))? 174 | .unwrap_or_default(); 175 | 176 | let code = interpreter::analysis::to_analysed(Bytecode::new_raw(code.original_bytes())); 177 | 178 | Ok(Some(AccountInfo::new( 179 | account.balance, 180 | account.nonce, 181 | code.hash_slow(), 182 | code, 183 | ))) 184 | } 185 | 186 | fn code_by_hash_ref(&self, _code_hash: B256) -> Result { 187 | panic!("This should not be called, as the code is already loaded"); 188 | } 189 | 190 | fn storage_ref(&self, address: Address, index: U256) -> Result { 191 | let value = self.request(|provider| provider.storage(address, StorageKey::from(index)))?; 192 | 193 | Ok(value.unwrap_or_default()) 194 | } 195 | 196 | fn block_hash_ref(&self, number: u64) -> Result { 197 | let blockhash = self.request(|provider| provider.block_hash(number))?; 198 | 199 | if let Some(hash) = blockhash { 200 | Ok(B256::new(hash.0)) 201 | } else { 202 | Ok(KECCAK_EMPTY) 203 | } 204 | } 205 | } 206 | --------------------------------------------------------------------------------