Skip to main content

tycho_simulation/evm/engine_db/
tycho_db.rs

1use std::{
2    collections::HashMap,
3    sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard},
4};
5
6use alloy::primitives::{Address, Bytes as AlloyBytes, B256, U256};
7use revm::{
8    context::DBErrorMarker,
9    primitives::KECCAK_EMPTY,
10    state::{AccountInfo, Bytecode},
11    DatabaseRef,
12};
13use thiserror::Error;
14use tracing::{debug, error, instrument, warn};
15use tycho_client::feed::BlockHeader;
16
17use crate::evm::{
18    account_storage::{AccountStorage, StateUpdate},
19    engine_db::engine_db_interface::EngineDatabaseInterface,
20    tycho_models::{AccountUpdate, ChangeType},
21};
22
23#[derive(Error, Debug)]
24pub enum TychoClientError {
25    #[error("Failed to parse URI: {0}. Error: {1}")]
26    UriParsing(String, String),
27    #[error("Failed to format request: {0}")]
28    FormatRequest(String),
29    #[error("Unexpected HTTP client error: {0}")]
30    HttpClient(String),
31    #[error("Failed to parse response: {0}")]
32    ParseResponse(String),
33}
34
35#[derive(Error, Debug)]
36pub enum PreCachedDBError {
37    #[error("Account {0} not found")]
38    MissingAccount(Address),
39    #[error("Bad account update: {0} - {1:?}")]
40    BadUpdate(String, Box<AccountUpdate>),
41    #[error("Block needs to be set")]
42    BlockNotSet(),
43    #[error("Tycho Client error: {0}")]
44    TychoClientError(#[from] TychoClientError),
45    #[error("{0}")]
46    Fatal(String),
47}
48
49impl DBErrorMarker for PreCachedDBError {}
50
51#[derive(Clone, Debug)]
52pub struct PreCachedDBInner {
53    /// Storage for accounts
54    accounts: AccountStorage,
55    /// Current block
56    block: Option<BlockHeader>,
57}
58
59#[derive(Clone, Debug)]
60pub struct PreCachedDB {
61    /// Cached inner data
62    ///
63    /// `inner` encapsulates `PreCachedDBInner` using `RwLock` for safe concurrent read or
64    /// exclusive write access to the data and `Arc` for shared ownership of the lock across
65    /// threads.
66    pub inner: Arc<RwLock<PreCachedDBInner>>,
67}
68
69impl PreCachedDB {
70    /// Create a new PreCachedDB instance
71    pub fn new() -> Result<Self, PreCachedDBError> {
72        Ok(PreCachedDB {
73            inner: Arc::new(RwLock::new(PreCachedDBInner {
74                accounts: AccountStorage::new(),
75                block: None,
76            })),
77        })
78    }
79
80    #[instrument(skip_all)]
81    pub fn update(
82        &self,
83        account_updates: Vec<AccountUpdate>,
84        block: Option<BlockHeader>,
85    ) -> Result<(), PreCachedDBError> {
86        // Hold the write lock for the duration of the function so that no other thread can
87        // write to the storage.
88        let mut write_guard = self.write_inner()?;
89
90        write_guard.block = block;
91
92        for update in account_updates {
93            match update.change {
94                ChangeType::Update => {
95                    debug!(%update.address, "Updating account");
96
97                    // If the account is not present, the internal storage will handle throwing
98                    // an exception.
99                    write_guard.accounts.update_account(
100                        &update.address,
101                        &StateUpdate {
102                            storage: Some(update.slots.clone()),
103                            balance: update.balance,
104                        },
105                    );
106                }
107                ChangeType::Deletion => {
108                    debug!(%update.address, "Deleting account");
109
110                    warn!(%update.address, "Deletion not implemented");
111                }
112                ChangeType::Creation => {
113                    debug!(%update.address, "Creating account");
114
115                    // We expect the code to be present.
116                    let code = Bytecode::new_raw(AlloyBytes::from(
117                        update.code.clone().ok_or_else(|| {
118                            error!(%update.address, "MissingCode");
119                            PreCachedDBError::BadUpdate(
120                                "MissingCode".into(),
121                                Box::new(update.clone()),
122                            )
123                        })?,
124                    ));
125                    // If the balance is not present, we set it to zero.
126                    let balance = update.balance.unwrap_or(U256::ZERO);
127
128                    // Initialize the account.
129                    write_guard.accounts.init_account(
130                        update.address,
131                        AccountInfo::new(balance, 0, code.hash_slow(), code),
132                        Some(update.slots.clone()),
133                        true, /* Flag all accounts in TychoDB mocked to sign that we cannot
134                               * call an RPC provider for an update */
135                    );
136                }
137                ChangeType::Unspecified => {
138                    warn!(%update.address, "Unspecified change type");
139                }
140            }
141        }
142        Ok(())
143    }
144
145    /// Retrieves the storage value at the specified index for the given account, if it exists.
146    ///
147    /// If the account exists in the storage, the storage value at the specified `index` is returned
148    /// as a reference. Temp storage takes priority over permanent storage.
149    /// If the account does not exist, `None` is returned.
150    ///
151    /// # Arguments
152    ///
153    /// * `address`: A reference to the address of the account to retrieve the storage value from.
154    /// * `index`: A reference to the index of the storage value to retrieve.
155    ///
156    /// # Returns
157    ///
158    /// Returns an `Option` containing a reference to the storage value if it exists, otherwise
159    /// returns `None`.
160    pub fn get_storage(&self, address: &Address, index: &U256) -> Option<U256> {
161        self.inner
162            .read()
163            .unwrap()
164            .accounts
165            .get_storage(address, index)
166    }
167
168    /// Update the simulation state.
169    ///
170    /// This method modifies the current state of the simulation by applying the provided updates to
171    /// the accounts in the smart contract storage. These changes correspond to a particular
172    /// block in the blockchain.
173    ///
174    /// # Arguments
175    ///
176    /// * `new_state`: A struct containing all the state changes for a particular block.
177    pub fn update_state(
178        &mut self,
179        updates: &HashMap<Address, StateUpdate>,
180        block: BlockHeader,
181    ) -> Result<HashMap<Address, StateUpdate>, PreCachedDBError> {
182        // Hold the write lock for the duration of the function so that no other thread can
183        // write to the storage.
184        let mut write_guard = self.write_inner()?;
185
186        let mut revert_updates = HashMap::new();
187        write_guard.block = Some(block);
188
189        for (address, update_info) in updates.iter() {
190            let mut revert_entry = StateUpdate::default();
191
192            if let Some(current_account) = write_guard
193                .accounts
194                .get_account_info(address)
195            {
196                revert_entry.balance = Some(current_account.balance);
197            }
198
199            if let Some(storage) = &update_info.storage {
200                let mut revert_storage = HashMap::default();
201                for index in storage.keys() {
202                    if let Some(s) = write_guard
203                        .accounts
204                        .get_storage(address, index)
205                    {
206                        revert_storage.insert(*index, s);
207                    }
208                }
209                revert_entry.storage = Some(revert_storage);
210            }
211            revert_updates.insert(*address, revert_entry);
212            write_guard
213                .accounts
214                .update_account(address, update_info);
215        }
216
217        Ok(revert_updates)
218    }
219
220    #[cfg(test)]
221    pub fn get_account_storage(&self) -> Result<AccountStorage, PreCachedDBError> {
222        self.read_inner()
223            .map(|guard| guard.accounts.clone())
224    }
225
226    /// If block is set, returns the number. Otherwise returns None.
227    pub fn block_number(&self) -> Result<Option<u64>, PreCachedDBError> {
228        self.read_inner().map(|guard| {
229            guard
230                .block
231                .as_ref()
232                .map(|header| header.number)
233        })
234    }
235
236    /// Clear all state from the database.
237    pub fn clear(&self) -> Result<(), PreCachedDBError> {
238        let mut write_guard = self.write_inner()?;
239        write_guard.accounts.clear();
240        write_guard.block = None;
241        Ok(())
242    }
243
244    fn read_inner(&self) -> Result<RwLockReadGuard<'_, PreCachedDBInner>, PreCachedDBError> {
245        self.inner
246            .read()
247            .map_err(|_| PreCachedDBError::Fatal("Tycho state db lock poisoned".into()))
248    }
249
250    fn write_inner(&self) -> Result<RwLockWriteGuard<'_, PreCachedDBInner>, PreCachedDBError> {
251        self.inner
252            .write()
253            .map_err(|_| PreCachedDBError::Fatal("Tycho state db lock poisoned".into()))
254    }
255}
256
257impl EngineDatabaseInterface for PreCachedDB {
258    type Error = PreCachedDBError;
259
260    /// Sets up a single account
261    ///
262    /// Full control over setting up an accounts. Allows to set up EOAs as well as smart contracts.
263    ///
264    /// # Arguments
265    ///
266    /// * `address` - Address of the account
267    /// * `account` - The account information
268    /// * `permanent_storage` - Storage to init the account with, this storage can only be updated
269    ///   manually
270    fn init_account(
271        &self,
272        address: Address,
273        account: AccountInfo,
274        permanent_storage: Option<HashMap<U256, U256>>,
275        _mocked: bool,
276    ) -> Result<(), <Self as EngineDatabaseInterface>::Error> {
277        if account.code.is_none() && account.code_hash != KECCAK_EMPTY {
278            warn!("Code is None for account {address} but code hash is not KECCAK_EMPTY");
279        } else if account.code.is_some() && account.code_hash == KECCAK_EMPTY {
280            warn!("Code is Some for account {address} but code hash is KECCAK_EMPTY");
281        }
282
283        self.write_inner()?
284            .accounts
285            .init_account(address, account, permanent_storage, true);
286
287        Ok(())
288    }
289
290    /// Deprecated in TychoDB
291    fn clear_temp_storage(&mut self) -> Result<(), <Self as EngineDatabaseInterface>::Error> {
292        debug!("Temp storage in TychoDB is never set, nothing to clear");
293
294        Ok(())
295    }
296
297    fn get_current_block(&self) -> Option<BlockHeader> {
298        self.inner.read().unwrap().block.clone()
299    }
300}
301
302impl DatabaseRef for PreCachedDB {
303    type Error = PreCachedDBError;
304    /// Retrieves basic information about an account.
305    ///
306    /// This function retrieves the basic account information for the specified address.
307    ///
308    /// # Arguments
309    ///
310    /// * `address`: The address of the account to retrieve the information for.
311    ///
312    /// # Returns
313    ///
314    /// Returns a `Result` containing the account information or an error if the account is not
315    /// found.
316    fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
317        self.read_inner()?
318            .accounts
319            .get_account_info(&address)
320            .map(|acc| Some(acc.clone()))
321            .ok_or(PreCachedDBError::MissingAccount(address))
322    }
323
324    fn code_by_hash_ref(&self, code_hash: B256) -> Result<Bytecode, Self::Error> {
325        Err(PreCachedDBError::Fatal(format!("Code by hash not supported: {code_hash}")))
326    }
327
328    /// Retrieves the storage value at the specified address and index.
329    ///
330    /// # Arguments
331    ///
332    /// * `address`: The address of the contract to retrieve the storage value from.
333    /// * `index`: The index of the storage value to retrieve.
334    ///
335    /// # Returns
336    ///
337    /// Returns a `Result` containing the storage value if it exists.
338    ///
339    /// # Errors
340    ///
341    /// Returns an error if the storage value is not found.
342    fn storage_ref(&self, address: Address, index: U256) -> Result<U256, Self::Error> {
343        debug!(%address, %index, "Requested storage of account");
344        let read_guard = self.read_inner()?;
345        if let Some(storage_value) = read_guard
346            .accounts
347            .get_storage(&address, &index)
348        {
349            debug!(%address, %index, %storage_value, "Got value locally");
350            Ok(storage_value)
351        } else {
352            // At this point we either don't know this address or we don't have anything at this
353            if read_guard
354                .accounts
355                .account_present(&address)
356            {
357                // As we only store non-zero values, if the account is present it means this
358                // slot is zero.
359                debug!(%address, %index, "Account found, but slot is zero");
360                Ok(U256::ZERO)
361            } else {
362                // At this point we know we don't have data for this address.
363                debug!(%address, %index, "Account not found");
364                Err(PreCachedDBError::MissingAccount(address))
365            }
366        }
367    }
368
369    /// If block header is set, returns the hash. Otherwise, returns a zero hash.
370    fn block_hash_ref(&self, _number: u64) -> Result<B256, Self::Error> {
371        match self.read_inner()?.block.clone() {
372            Some(header) => Ok(B256::from_slice(&header.hash)),
373            None => Ok(B256::default()),
374        }
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use std::{error::Error, str::FromStr};
381
382    use revm::primitives::U256;
383    use rstest::{fixture, rstest};
384    use tycho_common::Bytes;
385
386    use super::*;
387    use crate::evm::tycho_models::{AccountUpdate, Chain, ChangeType};
388
389    #[fixture]
390    pub fn mock_db() -> PreCachedDB {
391        PreCachedDB {
392            inner: Arc::new(RwLock::new(PreCachedDBInner {
393                accounts: AccountStorage::new(),
394                block: None,
395            })),
396        }
397    }
398
399    #[rstest]
400    #[tokio::test]
401    async fn test_account_get_acc_info(mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
402        // Tests if the provider has not been queried.
403        // Querying the mocked provider would cause a panic, therefore no assert is needed.
404        let mock_acc_address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
405        mock_db
406            .init_account(mock_acc_address, AccountInfo::default(), None, false)
407            .expect("Account init should succeed");
408
409        let acc_info = mock_db
410            .basic_ref(mock_acc_address)
411            .unwrap()
412            .unwrap();
413
414        assert_eq!(
415            mock_db
416                .basic_ref(mock_acc_address)
417                .unwrap()
418                .unwrap(),
419            acc_info
420        );
421        Ok(())
422    }
423
424    #[rstest]
425    fn test_account_storage(mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
426        let mock_acc_address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
427        let storage_address = U256::from(1);
428        let mut permanent_storage: HashMap<U256, U256> = HashMap::new();
429        permanent_storage.insert(storage_address, U256::from(10));
430        mock_db
431            .init_account(mock_acc_address, AccountInfo::default(), Some(permanent_storage), false)
432            .expect("Account init should succeed");
433
434        let storage = mock_db
435            .storage_ref(mock_acc_address, storage_address)
436            .unwrap();
437
438        assert_eq!(storage, U256::from(10));
439        Ok(())
440    }
441
442    #[rstest]
443    fn test_account_storage_zero(mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
444        let mock_acc_address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
445        let storage_address = U256::from(1);
446        mock_db
447            .init_account(mock_acc_address, AccountInfo::default(), None, false)
448            .expect("Account init should succeed");
449
450        let storage = mock_db
451            .storage_ref(mock_acc_address, storage_address)
452            .unwrap();
453
454        assert_eq!(storage, U256::ZERO);
455        Ok(())
456    }
457
458    #[rstest]
459    #[should_panic(
460        expected = "called `Result::unwrap()` on an `Err` value: MissingAccount(0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc)"
461    )]
462    fn test_account_storage_missing(mock_db: PreCachedDB) {
463        let mock_acc_address =
464            Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc").unwrap();
465        let storage_address = U256::from(1);
466
467        // This will panic because this account isn't initialized
468        mock_db
469            .storage_ref(mock_acc_address, storage_address)
470            .unwrap();
471    }
472
473    #[rstest]
474    #[tokio::test]
475    async fn test_update_state(mut mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
476        let address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
477        mock_db
478            .init_account(address, AccountInfo::default(), None, false)
479            .expect("Account init should succeed");
480
481        let mut new_storage = HashMap::default();
482        let new_storage_value_index = U256::from_limbs_slice(&[123]);
483        new_storage.insert(new_storage_value_index, new_storage_value_index);
484        let new_balance = U256::from_limbs_slice(&[500]);
485        let update = StateUpdate { storage: Some(new_storage), balance: Some(new_balance) };
486        let new_block = BlockHeader {
487            number: 1,
488            hash: Bytes::from_str(
489                "0xc6b994ec855fb2b31013c7ae65074406fac46679b5b963469104e0bfeddd66d9",
490            )
491            .unwrap(),
492            timestamp: 123,
493            ..Default::default()
494        };
495        let mut updates = HashMap::default();
496        updates.insert(address, update);
497
498        mock_db
499            .update_state(&updates, new_block)
500            .expect("State update should succeed");
501
502        assert_eq!(
503            mock_db
504                .get_storage(&address, &new_storage_value_index)
505                .unwrap(),
506            new_storage_value_index
507        );
508        let account_info = mock_db
509            .basic_ref(address)
510            .unwrap()
511            .unwrap();
512        assert_eq!(account_info.balance, new_balance);
513        let block = mock_db
514            .inner
515            .read()
516            .unwrap()
517            .block
518            .clone()
519            .expect("block is Some");
520        assert_eq!(block.number, 1);
521
522        Ok(())
523    }
524
525    #[rstest]
526    #[tokio::test]
527    async fn test_block_number_getter(mut mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
528        let address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
529        mock_db
530            .init_account(address, AccountInfo::default(), None, false)
531            .expect("Account init should succeed");
532
533        let new_block = BlockHeader {
534            number: 1,
535            hash: Bytes::from_str(
536                "0xc6b994ec855fb2b31013c7ae65074406fac46679b5b963469104e0bfeddd66d9",
537            )
538            .unwrap(),
539            timestamp: 123,
540            ..Default::default()
541        };
542        let updates = HashMap::default();
543
544        mock_db
545            .update_state(&updates, new_block)
546            .expect("State update should succeed");
547
548        let block_number = mock_db.block_number();
549        assert_eq!(block_number.unwrap().unwrap(), 1);
550
551        Ok(())
552    }
553
554    #[rstest]
555    #[tokio::test]
556    async fn test_update() {
557        let mock_db = PreCachedDB {
558            inner: Arc::new(RwLock::new(PreCachedDBInner {
559                accounts: AccountStorage::new(),
560                block: None,
561            })),
562        };
563
564        let account_update = AccountUpdate::new(
565            Address::from_str("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D").unwrap(),
566            Chain::Ethereum,
567            HashMap::new(),
568            Some(U256::from(500)),
569            Some(Vec::<u8>::new()),
570            ChangeType::Creation,
571        );
572
573        let new_block = BlockHeader {
574            number: 1,
575            hash: Bytes::from_str(
576                "0xc6b994ec855fb2b31013c7ae65074406fac46679b5b963469104e0bfeddd66d9",
577            )
578            .unwrap(),
579            timestamp: 123,
580            ..Default::default()
581        };
582
583        mock_db
584            .update(vec![account_update], Some(new_block))
585            .unwrap();
586
587        let account_info = mock_db
588            .basic_ref(Address::from_str("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D").unwrap())
589            .unwrap()
590            .unwrap();
591
592        assert_eq!(
593            account_info,
594            AccountInfo {
595                nonce: 0,
596                balance: U256::from(500),
597                code_hash: B256::from_str(
598                    "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
599                )
600                .unwrap(),
601                code: Some(Bytecode::default()),
602            }
603        );
604
605        assert_eq!(
606            mock_db
607                .inner
608                .read()
609                .unwrap()
610                .block
611                .clone()
612                .expect("block is Some")
613                .number,
614            1
615        );
616    }
617
618    /// This test requires a running TychoDB instance.
619    ///
620    /// To run this test, start TychoDB with the following command:
621    /// ```bash
622    /// cargo run --release -- \
623    //     --endpoint https://mainnet.eth.streamingfast.io:443 \
624    //     --module map_changes \
625    //     --spkg substreams/ethereum-ambient/substreams-ethereum-ambient-v0.3.0.spkg
626    /// ```
627    /// 
628    /// Then run the test with:
629    /// ```bash
630    /// cargo test --package src --lib -- --ignored --exact --nocapture
631    /// evm::engine_db::tycho_db::tests::test_tycho_db_connection
632    /// ```
633    #[ignore]
634    #[rstest]
635    fn test_tycho_db_connection() {
636        tracing_subscriber::fmt()
637            .with_env_filter("debug")
638            .init();
639
640        let ambient_contract =
641            Address::from_str("0xaaaaaaaaa24eeeb8d57d431224f73832bc34f688").unwrap();
642
643        let db = PreCachedDB::new().expect("db should initialize");
644
645        let acc_info = db
646            .basic_ref(ambient_contract)
647            .unwrap()
648            .unwrap();
649
650        debug!(?acc_info, "Account info");
651    }
652}