kona_executor/db/
mod.rs

1//! This module contains an implementation of an in-memory Trie DB for [revm], that allows for
2//! incremental updates through fetching node preimages on the fly during execution.
3
4use crate::errors::{TrieDBError, TrieDBResult};
5use alloc::{string::ToString, vec::Vec};
6use alloy_consensus::{Header, Sealed, EMPTY_ROOT_HASH};
7use alloy_primitives::{keccak256, Address, B256, U256};
8use alloy_rlp::{Decodable, Encodable};
9use kona_mpt::{Nibbles, TrieHinter, TrieNode, TrieNodeError};
10use revm::{
11    db::{states::StorageSlot, BundleState},
12    primitives::{AccountInfo, Bytecode, HashMap, BLOCK_HASH_HISTORY},
13    Database,
14};
15
16mod account;
17pub use account::TrieAccount;
18
19mod traits;
20pub use traits::{NoopTrieDBProvider, TrieDBProvider};
21
22/// A Trie DB that caches open state in-memory.
23///
24/// When accounts that don't already exist within the cached [TrieNode] are queried, the database
25/// fetches the preimages of the trie nodes on the path to the account using the `PreimageFetcher`
26/// (`F` generic). This allows for data to be fetched in a verifiable manner given an initial
27/// trusted state root as it is needed during execution.
28///
29/// The [TrieDB] is intended to be wrapped by a [State], which is then used by the [revm::Evm] to
30/// capture state transitions during block execution.
31///
32/// **Behavior**:
33/// - When an account is queried and the trie path has not already been opened by [Self::basic], we
34///   fall through to the `PreimageFetcher` to fetch the preimages of the trie nodes on the path to
35///   the account. After it has been fetched, the path will be cached until the next call to
36///   [Self::state_root].
37/// - When querying for the code hash of an account, the `CodeHashFetcher` is consulted to fetch the
38///   code hash of the account.
39/// - When a [BundleState] changeset is committed to the parent [State] database, the changes are
40///   first applied to the [State]'s cache, then the trie hash is recomputed with
41///   [Self::state_root].
42/// - When the block hash of a block number is needed via [Self::block_hash], the
43///   `HeaderByHashFetcher` is consulted to walk back to the desired block number by revealing the
44///   parent hash of block headers until the desired block number is reached, up to a maximum of
45///   [BLOCK_HASH_HISTORY] blocks back relative to the current parent block hash.
46///
47/// **Example Construction**:
48/// ```rust
49/// use alloy_consensus::{Header, Sealable};
50/// use alloy_primitives::{Bytes, B256};
51/// use anyhow::Result;
52/// use kona_executor::{NoopTrieDBProvider, TrieDB};
53/// use kona_mpt::NoopTrieHinter;
54/// use revm::{db::states::bundle_state::BundleRetention, EvmBuilder, StateBuilder};
55///
56/// let mock_starting_root = B256::default();
57/// let mock_parent_block_header = Header::default();
58///
59/// let trie_db = TrieDB::new(
60///     mock_starting_root,
61///     mock_parent_block_header.seal_slow(),
62///     NoopTrieDBProvider,
63///     NoopTrieHinter,
64/// );
65/// let mut state = StateBuilder::new_with_database(trie_db).with_bundle_update().build();
66/// let evm = EvmBuilder::default().with_db(&mut state).build();
67///
68/// // Execute your block's transactions...
69///
70/// // Drop the EVM prior to merging the state transitions.
71/// drop(evm);
72///
73/// state.merge_transitions(BundleRetention::Reverts);
74/// let bundle = state.take_bundle();
75/// let state_root = state.database.state_root(&bundle).expect("Failed to compute state root");
76/// ```
77///
78/// [State]: revm::State
79#[derive(Debug, Clone)]
80pub struct TrieDB<F, H>
81where
82    F: TrieDBProvider,
83    H: TrieHinter,
84{
85    /// The [TrieNode] representation of the root node.
86    root_node: TrieNode,
87    /// Storage roots of accounts within the trie.
88    storage_roots: HashMap<Address, TrieNode>,
89    /// The parent block hash of the current block.
90    parent_block_header: Sealed<Header>,
91    /// The [TrieDBProvider]
92    fetcher: F,
93    /// The [TrieHinter]
94    hinter: H,
95}
96
97impl<F, H> TrieDB<F, H>
98where
99    F: TrieDBProvider,
100    H: TrieHinter,
101{
102    /// Creates a new [TrieDB] with the given root node.
103    pub fn new(root: B256, parent_block_header: Sealed<Header>, fetcher: F, hinter: H) -> Self {
104        Self {
105            root_node: TrieNode::new_blinded(root),
106            storage_roots: Default::default(),
107            parent_block_header,
108            fetcher,
109            hinter,
110        }
111    }
112
113    /// Consumes `Self` and takes the current state root of the trie DB.
114    pub fn take_root_node(self) -> TrieNode {
115        self.root_node
116    }
117
118    /// Returns a shared reference to the root [TrieNode] of the trie DB.
119    pub const fn root(&self) -> &TrieNode {
120        &self.root_node
121    }
122
123    /// Returns the mapping of [Address]es to storage roots.
124    pub const fn storage_roots(&self) -> &HashMap<Address, TrieNode> {
125        &self.storage_roots
126    }
127
128    /// Returns a reference to the current parent block header of the trie DB.
129    pub const fn parent_block_header(&self) -> &Sealed<Header> {
130        &self.parent_block_header
131    }
132
133    /// Sets the parent block header of the trie DB. Should be called after a block has been
134    /// executed and the Header has been created.
135    ///
136    /// ## Takes
137    /// - `parent_block_header`: The parent block header of the current block.
138    pub fn set_parent_block_header(&mut self, parent_block_header: Sealed<Header>) {
139        self.parent_block_header = parent_block_header;
140    }
141
142    /// Applies a [BundleState] changeset to the [TrieNode] and recomputes the state root hash.
143    ///
144    /// ## Takes
145    /// - `bundle`: The [BundleState] changeset to apply to the trie DB.
146    ///
147    /// ## Returns
148    /// - `Ok(B256)`: The new state root hash of the trie DB.
149    /// - `Err(_)`: If the state root hash could not be computed.
150    pub fn state_root(&mut self, bundle: &BundleState) -> TrieDBResult<B256> {
151        debug!(target: "client_executor", "Recomputing state root");
152
153        // Update the accounts in the trie with the changeset.
154        self.update_accounts(bundle)?;
155
156        // Recompute the root hash of the trie.
157        self.root_node.blind();
158
159        debug!(
160            target: "client_executor",
161            "Recomputed state root: {commitment:?}",
162            commitment = self.root_node.blinded_commitment()
163        );
164
165        // Extract the new state root from the root node.
166        self.root_node.blinded_commitment().ok_or(TrieDBError::RootNotBlinded)
167    }
168
169    /// Fetches the [TrieAccount] of an account from the trie DB.
170    ///
171    /// ## Takes
172    /// - `address`: The address of the account.
173    ///
174    /// ## Returns
175    /// - `Ok(Some(TrieAccount))`: The [TrieAccount] of the account.
176    /// - `Ok(None)`: If the account does not exist in the trie.
177    /// - `Err(_)`: If the account could not be fetched.
178    pub fn get_trie_account(&mut self, address: &Address) -> TrieDBResult<Option<TrieAccount>> {
179        // Send a hint to the host to fetch the account proof.
180        self.hinter
181            .hint_account_proof(*address, self.parent_block_header.number)
182            .map_err(|e| TrieDBError::Provider(e.to_string()))?;
183
184        // Fetch the account from the trie.
185        let hashed_address_nibbles = Nibbles::unpack(keccak256(address.as_slice()));
186        let Some(trie_account_rlp) = self.root_node.open(&hashed_address_nibbles, &self.fetcher)?
187        else {
188            return Ok(None);
189        };
190
191        // Decode the trie account from the RLP bytes.
192        TrieAccount::decode(&mut trie_account_rlp.as_ref())
193            .map_err(TrieNodeError::RLPError)
194            .map_err(Into::into)
195            .map(Some)
196    }
197
198    /// Modifies the accounts in the storage trie with the given [BundleState] changeset.
199    ///
200    /// ## Takes
201    /// - `bundle`: The [BundleState] changeset to apply to the trie DB.
202    ///
203    /// ## Returns
204    /// - `Ok(())` if the accounts were successfully updated.
205    /// - `Err(_)` if the accounts could not be updated.
206    fn update_accounts(&mut self, bundle: &BundleState) -> TrieDBResult<()> {
207        for (address, bundle_account) in bundle.state() {
208            if bundle_account.status.is_not_modified() {
209                continue;
210            }
211
212            // Compute the path to the account in the trie.
213            let account_path = Nibbles::unpack(keccak256(address.as_slice()));
214
215            // If the account was destroyed, delete it from the trie.
216            if bundle_account.was_destroyed() {
217                self.root_node.delete(&account_path, &self.fetcher, &self.hinter)?;
218                self.storage_roots.remove(address);
219                continue;
220            }
221
222            let account_info =
223                bundle_account.account_info().ok_or(TrieDBError::MissingAccountInfo)?;
224            let mut trie_account = TrieAccount {
225                balance: account_info.balance,
226                nonce: account_info.nonce,
227                code_hash: account_info.code_hash,
228                ..Default::default()
229            };
230
231            // Update the account's storage root
232            let acc_storage_root = self
233                .storage_roots
234                .entry(*address)
235                .or_insert_with(|| TrieNode::new_blinded(EMPTY_ROOT_HASH));
236            bundle_account.storage.iter().try_for_each(|(index, value)| {
237                Self::change_storage(acc_storage_root, *index, value, &self.fetcher, &self.hinter)
238            })?;
239
240            // Recompute the account storage root.
241            acc_storage_root.blind();
242
243            let commitment =
244                acc_storage_root.blinded_commitment().ok_or(TrieDBError::RootNotBlinded)?;
245            trie_account.storage_root = commitment;
246
247            // RLP encode the trie account for insertion.
248            let mut account_buf = Vec::with_capacity(trie_account.length());
249            trie_account.encode(&mut account_buf);
250
251            // Insert or update the account in the trie.
252            self.root_node.insert(&account_path, account_buf.into(), &self.fetcher)?;
253        }
254
255        Ok(())
256    }
257
258    /// Modifies a storage slot of an account in the Merkle Patricia Trie.
259    ///
260    /// ## Takes
261    /// - `address`: The address of the account.
262    /// - `index`: The index of the storage slot.
263    /// - `value`: The new value of the storage slot.
264    ///
265    /// ## Returns
266    /// - `Ok(())` if the storage slot was successfully modified.
267    /// - `Err(_)` if the storage slot could not be modified.
268    fn change_storage(
269        storage_root: &mut TrieNode,
270        index: U256,
271        value: &StorageSlot,
272        fetcher: &F,
273        hinter: &H,
274    ) -> TrieDBResult<()> {
275        if !value.is_changed() {
276            return Ok(());
277        }
278
279        // RLP encode the storage slot value.
280        let mut rlp_buf = Vec::with_capacity(value.present_value.length());
281        value.present_value.encode(&mut rlp_buf);
282
283        // Insert or update the storage slot in the trie.
284        let hashed_slot_key = Nibbles::unpack(keccak256(index.to_be_bytes::<32>().as_slice()));
285        if value.present_value.is_zero() {
286            // If the storage slot is being set to zero, prune it from the trie.
287            storage_root.delete(&hashed_slot_key, fetcher, hinter)?;
288        } else {
289            // Otherwise, update the storage slot.
290            storage_root.insert(&hashed_slot_key, rlp_buf.into(), fetcher)?;
291        }
292
293        Ok(())
294    }
295}
296
297impl<F, H> Database for TrieDB<F, H>
298where
299    F: TrieDBProvider,
300    H: TrieHinter,
301{
302    type Error = TrieDBError;
303
304    fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
305        // Fetch the account from the trie.
306        let Some(trie_account) = self.get_trie_account(&address)? else {
307            // If the account does not exist in the trie, return `Ok(None)`.
308            return Ok(None);
309        };
310
311        // Insert the account's storage root into the cache.
312        self.storage_roots.insert(address, TrieNode::new_blinded(trie_account.storage_root));
313
314        // Return a partial DB account. The storage and code are not loaded out-right, and are
315        // loaded optimistically in the `Database` + `DatabaseRef` trait implementations.
316        Ok(Some(AccountInfo {
317            balance: trie_account.balance,
318            nonce: trie_account.nonce,
319            code_hash: trie_account.code_hash,
320            code: None,
321        }))
322    }
323
324    fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error> {
325        self.fetcher
326            .bytecode_by_hash(code_hash)
327            .map(Bytecode::new_raw)
328            .map_err(|e| TrieDBError::Provider(e.to_string()))
329    }
330
331    fn storage(&mut self, address: Address, index: U256) -> Result<U256, Self::Error> {
332        // Send a hint to the host to fetch the storage proof.
333        self.hinter
334            .hint_storage_proof(address, index, self.parent_block_header.number)
335            .map_err(|e| TrieDBError::Provider(e.to_string()))?;
336
337        // Fetch the account's storage root from the cache. If storage is being accessed, the
338        // account should have been loaded into the cache by the `basic` method. If the account was
339        // non-existing, the storage root will not be present.
340        match self.storage_roots.get_mut(&address) {
341            None => {
342                // If the storage root for the account does not exist, return zero.
343                Ok(U256::ZERO)
344            }
345            Some(storage_root) => {
346                // Fetch the storage slot from the trie.
347                let hashed_slot_key = keccak256(index.to_be_bytes::<32>().as_slice());
348                match storage_root.open(&Nibbles::unpack(hashed_slot_key), &self.fetcher)? {
349                    Some(slot_value) => {
350                        // Decode the storage slot value.
351                        let int_slot = U256::decode(&mut slot_value.as_ref())
352                            .map_err(TrieNodeError::RLPError)?;
353                        Ok(int_slot)
354                    }
355                    None => {
356                        // If the storage slot does not exist, return zero.
357                        Ok(U256::ZERO)
358                    }
359                }
360            }
361        }
362    }
363
364    fn block_hash(&mut self, block_number: u64) -> Result<B256, Self::Error> {
365        // Copy the current header
366        let mut header = self.parent_block_header.inner().clone();
367
368        // Check if the block number is in range. If not, we can fail early.
369        if block_number > header.number ||
370            header.number.saturating_sub(block_number) > BLOCK_HASH_HISTORY
371        {
372            return Ok(B256::default());
373        }
374
375        // Walk back the block headers to the desired block number.
376        while header.number > block_number {
377            header = self
378                .fetcher
379                .header_by_hash(header.parent_hash)
380                .map_err(|e| TrieDBError::Provider(e.to_string()))?;
381        }
382
383        Ok(header.hash_slow())
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use alloy_consensus::Sealable;
391    use alloy_primitives::b256;
392    use kona_mpt::NoopTrieHinter;
393
394    fn new_test_db() -> TrieDB<NoopTrieDBProvider, NoopTrieHinter> {
395        TrieDB::new(
396            B256::default(),
397            Header::default().seal_slow(),
398            NoopTrieDBProvider,
399            NoopTrieHinter,
400        )
401    }
402
403    #[test]
404    fn test_trie_db_take_root_node() {
405        let db = new_test_db();
406        let root_node = db.take_root_node();
407        assert_eq!(root_node.blinded_commitment(), Some(B256::default()));
408    }
409
410    #[test]
411    fn test_trie_db_root_node_ref() {
412        let db = new_test_db();
413        let root_node = db.root();
414        assert_eq!(root_node.blinded_commitment(), Some(B256::default()));
415    }
416
417    #[test]
418    fn test_trie_db_storage_roots() {
419        let db = new_test_db();
420        let storage_roots = db.storage_roots();
421        assert!(storage_roots.is_empty());
422    }
423
424    #[test]
425    fn test_block_hash_above_range() {
426        let mut db = new_test_db();
427        db.parent_block_header = Header { number: 10, ..Default::default() }.seal_slow();
428        let block_number = 11;
429        let block_hash = db.block_hash(block_number).unwrap();
430        assert_eq!(block_hash, B256::default());
431    }
432
433    #[test]
434    fn test_block_hash_below_range() {
435        let mut db = new_test_db();
436        db.parent_block_header =
437            Header { number: BLOCK_HASH_HISTORY + 10, ..Default::default() }.seal_slow();
438        let block_number = 0;
439        let block_hash = db.block_hash(block_number).unwrap();
440        assert_eq!(block_hash, B256::default());
441    }
442
443    #[test]
444    fn test_block_hash_provider_missing_hash() {
445        let mut db = new_test_db();
446        db.parent_block_header = Header { number: 10, ..Default::default() }.seal_slow();
447        let block_number = 5;
448        let block_hash = db.block_hash(block_number).unwrap();
449        assert_eq!(
450            block_hash,
451            b256!("78dec18c6d7da925bbe773c315653cdc70f6444ed6c1de9ac30bdb36cff74c3b")
452        );
453    }
454}