Skip to main content

foundry_evm_core/fork/
database.rs

1//! A revm database that forks off a remote client
2
3use crate::{
4    backend::{RevertStateSnapshotAction, StateSnapshot},
5    state_snapshot::StateSnapshots,
6};
7use alloy_network::Network;
8use alloy_primitives::{Address, B256, U256};
9use alloy_rpc_types::BlockId;
10use foundry_fork_db::{BlockchainDb, DatabaseError, ForkBlockEnv, SharedBackend};
11use parking_lot::Mutex;
12use revm::{
13    Database, DatabaseCommit,
14    bytecode::Bytecode,
15    context::BlockEnv,
16    database::{AccountState, CacheDB, DatabaseRef},
17    primitives::AddressMap,
18    state::{Account, AccountInfo},
19};
20use std::sync::Arc;
21
22/// a [revm::Database] that's forked off another client
23///
24/// The `backend` is used to retrieve (missing) data, which is then fetched from the remote
25/// endpoint. The inner in-memory database holds this storage and will be used for write operations.
26/// This database uses the `backend` for read and the `db` for write operations. But note the
27/// `backend` will also write (missing) data to the `db` in the background
28#[derive(Clone, Debug)]
29pub struct ForkedDatabase<N: Network, B: ForkBlockEnv = BlockEnv> {
30    /// Responsible for fetching missing data.
31    ///
32    /// This is responsible for getting data.
33    backend: SharedBackend<N, B>,
34    /// Cached Database layer, ensures that changes are not written to the database that
35    /// exclusively stores the state of the remote client.
36    ///
37    /// This separates Read/Write operations
38    ///   - reads from the `SharedBackend as DatabaseRef` writes to the internal cache storage.
39    cache_db: CacheDB<SharedBackend<N, B>>,
40    /// Contains all the data already fetched.
41    ///
42    /// This exclusively stores the _unchanged_ remote client state.
43    db: BlockchainDb<B>,
44    /// Holds the state snapshots of a blockchain.
45    state_snapshots: Arc<Mutex<StateSnapshots<ForkDbStateSnapshot<N, B>>>>,
46}
47
48impl<N: Network, B: ForkBlockEnv> ForkedDatabase<N, B> {
49    /// Creates a new instance of this DB
50    pub fn new(backend: SharedBackend<N, B>, db: BlockchainDb<B>) -> Self {
51        Self {
52            cache_db: CacheDB::new(backend.clone()),
53            backend,
54            db,
55            state_snapshots: Arc::new(Mutex::new(Default::default())),
56        }
57    }
58
59    pub const fn database(&self) -> &CacheDB<SharedBackend<N, B>> {
60        &self.cache_db
61    }
62
63    pub const fn database_mut(&mut self) -> &mut CacheDB<SharedBackend<N, B>> {
64        &mut self.cache_db
65    }
66
67    pub const fn state_snapshots(&self) -> &Arc<Mutex<StateSnapshots<ForkDbStateSnapshot<N, B>>>> {
68        &self.state_snapshots
69    }
70
71    /// Reset the fork to a fresh forked state, and optionally update the fork config
72    pub fn reset(
73        &mut self,
74        _urls: Vec<String>,
75        block_number: impl Into<BlockId>,
76    ) -> Result<(), String> {
77        self.backend.set_pinned_block(block_number).map_err(|err| err.to_string())?;
78
79        // TODO need to find a way to update generic provider via url
80
81        // wipe the storage retrieved from remote
82        self.inner().db().clear();
83        // create a fresh `CacheDB`, effectively wiping modified state
84        self.cache_db = CacheDB::new(self.backend.clone());
85        trace!(target: "backend::forkdb", "Cleared database");
86        Ok(())
87    }
88
89    /// Flushes the cache to disk if configured
90    pub fn flush_cache(&self) {
91        self.db.cache().flush()
92    }
93
94    /// Returns the database that holds the remote state
95    pub const fn inner(&self) -> &BlockchainDb<B> {
96        &self.db
97    }
98
99    pub fn create_state_snapshot(&self) -> ForkDbStateSnapshot<N, B> {
100        let db = self.db.db();
101        let state_snapshot = StateSnapshot {
102            accounts: db.accounts.read().clone(),
103            storage: db.storage.read().clone(),
104            block_hashes: db.block_hashes.read().clone(),
105            account_states: None,
106        };
107        ForkDbStateSnapshot { local: self.cache_db.clone(), state_snapshot }
108    }
109
110    pub fn insert_state_snapshot(&self) -> U256 {
111        let state_snapshot = self.create_state_snapshot();
112        let mut state_snapshots = self.state_snapshots().lock();
113        let id = state_snapshots.insert(state_snapshot);
114        trace!(target: "backend::forkdb", "Created new snapshot {}", id);
115        id
116    }
117
118    /// Removes the snapshot from the tracked snapshot and sets it as the current state
119    pub fn revert_state_snapshot(&mut self, id: U256, action: RevertStateSnapshotAction) -> bool {
120        let state_snapshot = { self.state_snapshots().lock().remove(id) };
121        if let Some(state_snapshot) = state_snapshot {
122            if action.is_keep() {
123                self.state_snapshots().lock().insert_at(state_snapshot.clone(), id);
124            }
125            let ForkDbStateSnapshot {
126                local,
127                state_snapshot: StateSnapshot { accounts, storage, block_hashes, .. },
128            } = state_snapshot;
129            let db = self.inner().db();
130            {
131                let mut accounts_lock = db.accounts.write();
132                accounts_lock.clear();
133                accounts_lock.extend(accounts);
134            }
135            {
136                let mut storage_lock = db.storage.write();
137                storage_lock.clear();
138                storage_lock.extend(storage);
139            }
140            {
141                let mut block_hashes_lock = db.block_hashes.write();
142                block_hashes_lock.clear();
143                block_hashes_lock.extend(block_hashes);
144            }
145
146            self.cache_db = local;
147
148            trace!(target: "backend::forkdb", "Reverted snapshot {}", id);
149            true
150        } else {
151            warn!(target: "backend::forkdb", "No snapshot to revert for {}", id);
152            false
153        }
154    }
155}
156
157impl<N: Network, B: ForkBlockEnv> Database for ForkedDatabase<N, B> {
158    type Error = DatabaseError;
159
160    fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
161        // Note: this will always return Some, since the `SharedBackend` will always load the
162        // account, this differs from `<CacheDB as Database>::basic`, See also
163        // [MemDb::ensure_loaded](crate::backend::MemDb::ensure_loaded)
164        Database::basic(&mut self.cache_db, address)
165    }
166
167    fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error> {
168        Database::code_by_hash(&mut self.cache_db, code_hash)
169    }
170
171    fn storage(&mut self, address: Address, index: U256) -> Result<U256, Self::Error> {
172        Database::storage(&mut self.cache_db, address, index)
173    }
174
175    fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error> {
176        Database::block_hash(&mut self.cache_db, number)
177    }
178}
179
180impl<N: Network, B: ForkBlockEnv> DatabaseRef for ForkedDatabase<N, B> {
181    type Error = DatabaseError;
182
183    fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
184        self.cache_db.basic_ref(address)
185    }
186
187    fn code_by_hash_ref(&self, code_hash: B256) -> Result<Bytecode, Self::Error> {
188        self.cache_db.code_by_hash_ref(code_hash)
189    }
190
191    fn storage_ref(&self, address: Address, index: U256) -> Result<U256, Self::Error> {
192        DatabaseRef::storage_ref(&self.cache_db, address, index)
193    }
194
195    fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
196        self.cache_db.block_hash_ref(number)
197    }
198}
199
200impl<N: Network, B: ForkBlockEnv> DatabaseCommit for ForkedDatabase<N, B> {
201    fn commit(&mut self, changes: AddressMap<Account>) {
202        self.database_mut().commit(changes)
203    }
204}
205
206/// Represents a snapshot of the database
207///
208/// This mimics `revm::CacheDB`
209#[derive(Clone, Debug)]
210pub struct ForkDbStateSnapshot<N: Network, B: ForkBlockEnv = BlockEnv> {
211    pub local: CacheDB<SharedBackend<N, B>>,
212    pub state_snapshot: StateSnapshot,
213}
214
215impl<N: Network, B: ForkBlockEnv> ForkDbStateSnapshot<N, B> {
216    /// Lookup storage in `state_snapshot`, then fall back to the backend (remote RPC).
217    fn storage_from_snapshot_or_backend(
218        &self,
219        address: Address,
220        index: U256,
221    ) -> Result<U256, DatabaseError> {
222        // Check state_snapshot.storage first (data fetched by SharedBackend / disk cache).
223        if let Some(val) = self.state_snapshot.storage.get(&address).and_then(|s| s.get(&index)) {
224            return Ok(*val);
225        }
226        // Fall back to the underlying backend (SharedBackend → remote RPC).
227        DatabaseRef::storage_ref(&self.local, address, index)
228    }
229}
230
231// This `DatabaseRef` implementation works similar to `CacheDB` which prioritizes modified elements,
232// and uses another db as fallback
233// We prioritize stored changed accounts/storage
234impl<N: Network, B: ForkBlockEnv> DatabaseRef for ForkDbStateSnapshot<N, B> {
235    type Error = DatabaseError;
236
237    fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
238        match self.local.cache.accounts.get(&address) {
239            Some(account) => Ok(Some(account.info.clone())),
240            None => {
241                let mut acc = self.state_snapshot.accounts.get(&address).cloned();
242
243                if acc.is_none() {
244                    acc = self.local.basic_ref(address)?;
245                }
246                Ok(acc)
247            }
248        }
249    }
250
251    fn code_by_hash_ref(&self, code_hash: B256) -> Result<Bytecode, Self::Error> {
252        self.local.code_by_hash_ref(code_hash)
253    }
254
255    fn storage_ref(&self, address: Address, index: U256) -> Result<U256, Self::Error> {
256        match self.local.cache.accounts.get(&address) {
257            Some(account) => match account.storage.get(&index) {
258                Some(entry) => Ok(*entry),
259                None if matches!(
260                    account.account_state,
261                    AccountState::NotExisting | AccountState::StorageCleared
262                ) =>
263                {
264                    Ok(U256::ZERO)
265                }
266                None => self.storage_from_snapshot_or_backend(address, index),
267            },
268            None => self.storage_from_snapshot_or_backend(address, index),
269        }
270    }
271
272    fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
273        match self.state_snapshot.block_hashes.get(&U256::from(number)).copied() {
274            None => self.local.block_hash_ref(number),
275            Some(block_hash) => Ok(block_hash),
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::backend::BlockchainDbMeta;
284    use alloy_network::AnyNetwork;
285    use alloy_provider::{ProviderBuilder, mock::Asserter};
286    use foundry_common::provider::get_http_provider;
287    use revm::database::DbAccount;
288
289    fn fork_snapshot_with_closed_backend() -> ForkDbStateSnapshot<AnyNetwork> {
290        let provider =
291            ProviderBuilder::<_, _, AnyNetwork>::default().connect_mocked_client(Asserter::new());
292        let db = BlockchainDb::new(
293            BlockchainDbMeta::new(BlockEnv::default(), "http://localhost".to_string()),
294            None,
295        );
296        let (backend, handler) = SharedBackend::new(provider, db, None);
297        drop(handler);
298        ForkDbStateSnapshot {
299            local: CacheDB::new(backend),
300            state_snapshot: StateSnapshot::default(),
301        }
302    }
303
304    #[test]
305    fn fork_db_state_snapshot_respects_local_storage_clearing() {
306        let address = Address::random();
307        let slot = U256::from(42);
308        for account_state in [AccountState::NotExisting, AccountState::StorageCleared] {
309            let mut snapshot = fork_snapshot_with_closed_backend();
310            snapshot.state_snapshot.storage.entry(address).or_default().insert(slot, U256::from(7));
311            snapshot.local.cache.accounts.insert(
312                address,
313                DbAccount { account_state: account_state.clone(), ..Default::default() },
314            );
315
316            assert_eq!(snapshot.storage_ref(address, slot).unwrap(), U256::ZERO);
317            assert_eq!(snapshot.storage_ref(address, slot + U256::ONE).unwrap(), U256::ZERO);
318
319            if account_state.is_storage_cleared() {
320                snapshot
321                    .local
322                    .cache
323                    .accounts
324                    .get_mut(&address)
325                    .unwrap()
326                    .storage
327                    .insert(slot, U256::from(9));
328                assert_eq!(snapshot.storage_ref(address, slot).unwrap(), U256::from(9));
329            }
330        }
331    }
332
333    /// Demonstrates that `Database::basic` for `ForkedDatabase` will always return the
334    /// `AccountInfo`
335    #[tokio::test(flavor = "multi_thread")]
336    async fn fork_db_insert_basic_default() {
337        let rpc = foundry_test_utils::rpc::next_http_rpc_endpoint();
338        let provider = get_http_provider(rpc.clone());
339        let meta = BlockchainDbMeta::new(BlockEnv::default(), rpc);
340
341        let db = BlockchainDb::new(meta, None);
342
343        let backend = SharedBackend::spawn_backend(Arc::new(provider), db.clone(), None).await;
344
345        let mut db = ForkedDatabase::new(backend, db);
346        let address = Address::random();
347
348        let info = Database::basic(&mut db, address).unwrap();
349        assert!(info.is_some());
350        let mut info = info.unwrap();
351        info.balance = U256::from(500u64);
352
353        // insert the modified account info
354        db.database_mut().insert_account_info(address, info.clone());
355
356        let loaded = Database::basic(&mut db, address).unwrap();
357        assert!(loaded.is_some());
358        assert_eq!(loaded.unwrap(), info);
359    }
360
361    /// Reads cached remote storage without consulting the backend, including when the local
362    /// account is present but does not override the slot.
363    #[test]
364    fn fork_db_state_snapshot_reads_storage_from_snapshot() {
365        let mut snapshot = fork_snapshot_with_closed_backend();
366        let address = Address::random();
367        let slot = U256::from(42u64);
368        let expected = U256::from(0xdeadbeefu64);
369
370        snapshot.state_snapshot.storage.entry(address).or_default().insert(slot, expected);
371
372        assert_eq!(snapshot.storage_ref(address, slot).unwrap(), expected);
373        snapshot.local.cache.accounts.insert(address, DbAccount::default());
374        assert_eq!(snapshot.storage_ref(address, slot).unwrap(), expected);
375    }
376
377    #[tokio::test(flavor = "multi_thread")]
378    async fn fork_db_revert_invalidates_newer_snapshots() {
379        let rpc = foundry_test_utils::rpc::next_http_rpc_endpoint();
380        let provider = get_http_provider(rpc.clone());
381        let meta = BlockchainDbMeta::new(BlockEnv::default(), rpc);
382        let db = BlockchainDb::new(meta, None);
383        let backend = SharedBackend::spawn_backend(Arc::new(provider), db.clone(), None).await;
384        let mut db = ForkedDatabase::new(backend, db);
385
386        let first = db.insert_state_snapshot();
387        let second = db.insert_state_snapshot();
388
389        assert!(db.revert_state_snapshot(first, RevertStateSnapshotAction::RevertRemove));
390        assert!(!db.revert_state_snapshot(second, RevertStateSnapshotAction::RevertRemove));
391    }
392}