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 update_info.storage.is_some() {
200                let mut revert_storage = HashMap::default();
201                for index in update_info
202                    .storage
203                    .as_ref()
204                    .unwrap()
205                    .keys()
206                {
207                    if let Some(s) = write_guard
208                        .accounts
209                        .get_storage(address, index)
210                    {
211                        revert_storage.insert(*index, s);
212                    }
213                }
214                revert_entry.storage = Some(revert_storage);
215            }
216            revert_updates.insert(*address, revert_entry);
217            write_guard
218                .accounts
219                .update_account(address, update_info);
220        }
221
222        Ok(revert_updates)
223    }
224
225    #[cfg(test)]
226    pub fn get_account_storage(&self) -> Result<AccountStorage, PreCachedDBError> {
227        self.read_inner()
228            .map(|guard| guard.accounts.clone())
229    }
230
231    /// If block is set, returns the number. Otherwise returns None.
232    pub fn block_number(&self) -> Result<Option<u64>, PreCachedDBError> {
233        self.read_inner().map(|guard| {
234            guard
235                .block
236                .as_ref()
237                .map(|header| header.number)
238        })
239    }
240
241    /// Clear all state from the database.
242    pub fn clear(&self) -> Result<(), PreCachedDBError> {
243        let mut write_guard = self.write_inner()?;
244        write_guard.accounts.clear();
245        write_guard.block = None;
246        Ok(())
247    }
248
249    fn read_inner(&self) -> Result<RwLockReadGuard<'_, PreCachedDBInner>, PreCachedDBError> {
250        self.inner
251            .read()
252            .map_err(|_| PreCachedDBError::Fatal("Tycho state db lock poisoned".into()))
253    }
254
255    fn write_inner(&self) -> Result<RwLockWriteGuard<'_, PreCachedDBInner>, PreCachedDBError> {
256        self.inner
257            .write()
258            .map_err(|_| PreCachedDBError::Fatal("Tycho state db lock poisoned".into()))
259    }
260}
261
262impl EngineDatabaseInterface for PreCachedDB {
263    type Error = PreCachedDBError;
264
265    /// Sets up a single account
266    ///
267    /// Full control over setting up an accounts. Allows to set up EOAs as well as smart contracts.
268    ///
269    /// # Arguments
270    ///
271    /// * `address` - Address of the account
272    /// * `account` - The account information
273    /// * `permanent_storage` - Storage to init the account with, this storage can only be updated
274    ///   manually
275    fn init_account(
276        &self,
277        address: Address,
278        account: AccountInfo,
279        permanent_storage: Option<HashMap<U256, U256>>,
280        _mocked: bool,
281    ) -> Result<(), <Self as EngineDatabaseInterface>::Error> {
282        if account.code.is_none() && account.code_hash != KECCAK_EMPTY {
283            warn!("Code is None for account {address} but code hash is not KECCAK_EMPTY");
284        } else if account.code.is_some() && account.code_hash == KECCAK_EMPTY {
285            warn!("Code is Some for account {address} but code hash is KECCAK_EMPTY");
286        }
287
288        self.write_inner()?
289            .accounts
290            .init_account(address, account, permanent_storage, true);
291
292        Ok(())
293    }
294
295    /// Deprecated in TychoDB
296    fn clear_temp_storage(&mut self) -> Result<(), <Self as EngineDatabaseInterface>::Error> {
297        debug!("Temp storage in TychoDB is never set, nothing to clear");
298
299        Ok(())
300    }
301
302    fn get_current_block(&self) -> Option<BlockHeader> {
303        self.inner.read().unwrap().block.clone()
304    }
305}
306
307impl DatabaseRef for PreCachedDB {
308    type Error = PreCachedDBError;
309    /// Retrieves basic information about an account.
310    ///
311    /// This function retrieves the basic account information for the specified address.
312    ///
313    /// # Arguments
314    ///
315    /// * `address`: The address of the account to retrieve the information for.
316    ///
317    /// # Returns
318    ///
319    /// Returns a `Result` containing the account information or an error if the account is not
320    /// found.
321    fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
322        self.read_inner()?
323            .accounts
324            .get_account_info(&address)
325            .map(|acc| Some(acc.clone()))
326            .ok_or(PreCachedDBError::MissingAccount(address))
327    }
328
329    fn code_by_hash_ref(&self, code_hash: B256) -> Result<Bytecode, Self::Error> {
330        Err(PreCachedDBError::Fatal(format!("Code by hash not supported: {code_hash}")))
331    }
332
333    /// Retrieves the storage value at the specified address and index.
334    ///
335    /// # Arguments
336    ///
337    /// * `address`: The address of the contract to retrieve the storage value from.
338    /// * `index`: The index of the storage value to retrieve.
339    ///
340    /// # Returns
341    ///
342    /// Returns a `Result` containing the storage value if it exists.
343    ///
344    /// # Errors
345    ///
346    /// Returns an error if the storage value is not found.
347    fn storage_ref(&self, address: Address, index: U256) -> Result<U256, Self::Error> {
348        debug!(%address, %index, "Requested storage of account");
349        let read_guard = self.read_inner()?;
350        if let Some(storage_value) = read_guard
351            .accounts
352            .get_storage(&address, &index)
353        {
354            debug!(%address, %index, %storage_value, "Got value locally");
355            Ok(storage_value)
356        } else {
357            // At this point we either don't know this address or we don't have anything at this
358            if read_guard
359                .accounts
360                .account_present(&address)
361            {
362                // As we only store non-zero values, if the account is present it means this
363                // slot is zero.
364                debug!(%address, %index, "Account found, but slot is zero");
365                Ok(U256::ZERO)
366            } else {
367                // At this point we know we don't have data for this address.
368                debug!(%address, %index, "Account not found");
369                Err(PreCachedDBError::MissingAccount(address))
370            }
371        }
372    }
373
374    /// If block header is set, returns the hash. Otherwise, returns a zero hash.
375    fn block_hash_ref(&self, _number: u64) -> Result<B256, Self::Error> {
376        match self.read_inner()?.block.clone() {
377            Some(header) => Ok(B256::from_slice(&header.hash)),
378            None => Ok(B256::default()),
379        }
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use std::{error::Error, str::FromStr};
386
387    use revm::primitives::U256;
388    use rstest::{fixture, rstest};
389    use tycho_common::Bytes;
390
391    use super::*;
392    use crate::evm::tycho_models::{AccountUpdate, Chain, ChangeType};
393
394    #[fixture]
395    pub fn mock_db() -> PreCachedDB {
396        PreCachedDB {
397            inner: Arc::new(RwLock::new(PreCachedDBInner {
398                accounts: AccountStorage::new(),
399                block: None,
400            })),
401        }
402    }
403
404    #[rstest]
405    #[tokio::test]
406    async fn test_account_get_acc_info(mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
407        // Tests if the provider has not been queried.
408        // Querying the mocked provider would cause a panic, therefore no assert is needed.
409        let mock_acc_address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
410        mock_db
411            .init_account(mock_acc_address, AccountInfo::default(), None, false)
412            .expect("Account init should succeed");
413
414        let acc_info = mock_db
415            .basic_ref(mock_acc_address)
416            .unwrap()
417            .unwrap();
418
419        assert_eq!(
420            mock_db
421                .basic_ref(mock_acc_address)
422                .unwrap()
423                .unwrap(),
424            acc_info
425        );
426        Ok(())
427    }
428
429    #[rstest]
430    fn test_account_storage(mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
431        let mock_acc_address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
432        let storage_address = U256::from(1);
433        let mut permanent_storage: HashMap<U256, U256> = HashMap::new();
434        permanent_storage.insert(storage_address, U256::from(10));
435        mock_db
436            .init_account(mock_acc_address, AccountInfo::default(), Some(permanent_storage), false)
437            .expect("Account init should succeed");
438
439        let storage = mock_db
440            .storage_ref(mock_acc_address, storage_address)
441            .unwrap();
442
443        assert_eq!(storage, U256::from(10));
444        Ok(())
445    }
446
447    #[rstest]
448    fn test_account_storage_zero(mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
449        let mock_acc_address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
450        let storage_address = U256::from(1);
451        mock_db
452            .init_account(mock_acc_address, AccountInfo::default(), None, false)
453            .expect("Account init should succeed");
454
455        let storage = mock_db
456            .storage_ref(mock_acc_address, storage_address)
457            .unwrap();
458
459        assert_eq!(storage, U256::ZERO);
460        Ok(())
461    }
462
463    #[rstest]
464    #[should_panic(
465        expected = "called `Result::unwrap()` on an `Err` value: MissingAccount(0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc)"
466    )]
467    fn test_account_storage_missing(mock_db: PreCachedDB) {
468        let mock_acc_address =
469            Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc").unwrap();
470        let storage_address = U256::from(1);
471
472        // This will panic because this account isn't initialized
473        mock_db
474            .storage_ref(mock_acc_address, storage_address)
475            .unwrap();
476    }
477
478    #[rstest]
479    #[tokio::test]
480    async fn test_update_state(mut mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
481        let address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
482        mock_db
483            .init_account(address, AccountInfo::default(), None, false)
484            .expect("Account init should succeed");
485
486        let mut new_storage = HashMap::default();
487        let new_storage_value_index = U256::from_limbs_slice(&[123]);
488        new_storage.insert(new_storage_value_index, new_storage_value_index);
489        let new_balance = U256::from_limbs_slice(&[500]);
490        let update = StateUpdate { storage: Some(new_storage), balance: Some(new_balance) };
491        let new_block = BlockHeader {
492            number: 1,
493            hash: Bytes::from_str(
494                "0xc6b994ec855fb2b31013c7ae65074406fac46679b5b963469104e0bfeddd66d9",
495            )
496            .unwrap(),
497            timestamp: 123,
498            ..Default::default()
499        };
500        let mut updates = HashMap::default();
501        updates.insert(address, update);
502
503        mock_db
504            .update_state(&updates, new_block)
505            .expect("State update should succeed");
506
507        assert_eq!(
508            mock_db
509                .get_storage(&address, &new_storage_value_index)
510                .unwrap(),
511            new_storage_value_index
512        );
513        let account_info = mock_db
514            .basic_ref(address)
515            .unwrap()
516            .unwrap();
517        assert_eq!(account_info.balance, new_balance);
518        let block = mock_db
519            .inner
520            .read()
521            .unwrap()
522            .block
523            .clone()
524            .expect("block is Some");
525        assert_eq!(block.number, 1);
526
527        Ok(())
528    }
529
530    #[rstest]
531    #[tokio::test]
532    async fn test_block_number_getter(mut mock_db: PreCachedDB) -> Result<(), Box<dyn Error>> {
533        let address = Address::from_str("0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")?;
534        mock_db
535            .init_account(address, AccountInfo::default(), None, false)
536            .expect("Account init should succeed");
537
538        let new_block = BlockHeader {
539            number: 1,
540            hash: Bytes::from_str(
541                "0xc6b994ec855fb2b31013c7ae65074406fac46679b5b963469104e0bfeddd66d9",
542            )
543            .unwrap(),
544            timestamp: 123,
545            ..Default::default()
546        };
547        let updates = HashMap::default();
548
549        mock_db
550            .update_state(&updates, new_block)
551            .expect("State update should succeed");
552
553        let block_number = mock_db.block_number();
554        assert_eq!(block_number.unwrap().unwrap(), 1);
555
556        Ok(())
557    }
558
559    #[rstest]
560    #[tokio::test]
561    async fn test_update() {
562        let mock_db = PreCachedDB {
563            inner: Arc::new(RwLock::new(PreCachedDBInner {
564                accounts: AccountStorage::new(),
565                block: None,
566            })),
567        };
568
569        let account_update = AccountUpdate::new(
570            Address::from_str("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D").unwrap(),
571            Chain::Ethereum,
572            HashMap::new(),
573            Some(U256::from(500)),
574            Some(Vec::<u8>::new()),
575            ChangeType::Creation,
576        );
577
578        let new_block = BlockHeader {
579            number: 1,
580            hash: Bytes::from_str(
581                "0xc6b994ec855fb2b31013c7ae65074406fac46679b5b963469104e0bfeddd66d9",
582            )
583            .unwrap(),
584            timestamp: 123,
585            ..Default::default()
586        };
587
588        mock_db
589            .update(vec![account_update], Some(new_block))
590            .unwrap();
591
592        let account_info = mock_db
593            .basic_ref(Address::from_str("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D").unwrap())
594            .unwrap()
595            .unwrap();
596
597        assert_eq!(
598            account_info,
599            AccountInfo {
600                nonce: 0,
601                balance: U256::from(500),
602                code_hash: B256::from_str(
603                    "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
604                )
605                .unwrap(),
606                code: Some(Bytecode::default()),
607            }
608        );
609
610        assert_eq!(
611            mock_db
612                .inner
613                .read()
614                .unwrap()
615                .block
616                .clone()
617                .expect("block is Some")
618                .number,
619            1
620        );
621    }
622
623    /// This test requires a running TychoDB instance.
624    ///
625    /// To run this test, start TychoDB with the following command:
626    /// ```bash
627    /// cargo run --release -- \
628    //     --endpoint https://mainnet.eth.streamingfast.io:443 \
629    //     --module map_changes \
630    //     --spkg substreams/ethereum-ambient/substreams-ethereum-ambient-v0.3.0.spkg
631    /// ```
632    /// 
633    /// Then run the test with:
634    /// ```bash
635    /// cargo test --package src --lib -- --ignored --exact --nocapture
636    /// evm::engine_db::tycho_db::tests::test_tycho_db_connection
637    /// ```
638    #[ignore]
639    #[rstest]
640    fn test_tycho_db_connection() {
641        tracing_subscriber::fmt()
642            .with_env_filter("debug")
643            .init();
644
645        let ambient_contract =
646            Address::from_str("0xaaaaaaaaa24eeeb8d57d431224f73832bc34f688").unwrap();
647
648        let db = PreCachedDB::new().expect("db should initialize");
649
650        let acc_info = db
651            .basic_ref(ambient_contract)
652            .unwrap()
653            .unwrap();
654
655        debug!(?acc_info, "Account info");
656    }
657}