reth_trie_db/
state.rs

1use crate::{DatabaseHashedCursorFactory, DatabaseTrieCursorFactory, PrefixSetLoader};
2use alloy_primitives::{map::B256Map, BlockNumber, B256};
3use reth_db_api::{
4    cursor::DbCursorRO,
5    models::{AccountBeforeTx, BlockNumberAddress, BlockNumberAddressRange},
6    tables,
7    transaction::DbTx,
8    DatabaseError,
9};
10use reth_execution_errors::StateRootError;
11use reth_trie::{
12    hashed_cursor::HashedPostStateCursorFactory, trie_cursor::InMemoryTrieCursorFactory,
13    updates::TrieUpdates, HashedPostStateSorted, HashedStorageSorted, KeccakKeyHasher, KeyHasher,
14    StateRoot, StateRootProgress, TrieInputSorted,
15};
16use std::{
17    collections::HashSet,
18    ops::{RangeBounds, RangeInclusive},
19};
20use tracing::{debug, instrument};
21
22/// Extends [`StateRoot`] with operations specific for working with a database transaction.
23pub trait DatabaseStateRoot<'a, TX>: Sized {
24    /// Create a new [`StateRoot`] instance.
25    fn from_tx(tx: &'a TX) -> Self;
26
27    /// Given a block number range, identifies all the accounts and storage keys that
28    /// have changed.
29    ///
30    /// # Returns
31    ///
32    /// An instance of state root calculator with account and storage prefixes loaded.
33    fn incremental_root_calculator(
34        tx: &'a TX,
35        range: RangeInclusive<BlockNumber>,
36    ) -> Result<Self, StateRootError>;
37
38    /// Computes the state root of the trie with the changed account and storage prefixes and
39    /// existing trie nodes.
40    ///
41    /// # Returns
42    ///
43    /// The updated state root.
44    fn incremental_root(
45        tx: &'a TX,
46        range: RangeInclusive<BlockNumber>,
47    ) -> Result<B256, StateRootError>;
48
49    /// Computes the state root of the trie with the changed account and storage prefixes and
50    /// existing trie nodes collecting updates in the process.
51    ///
52    /// Ignores the threshold.
53    ///
54    /// # Returns
55    ///
56    /// The updated state root and the trie updates.
57    fn incremental_root_with_updates(
58        tx: &'a TX,
59        range: RangeInclusive<BlockNumber>,
60    ) -> Result<(B256, TrieUpdates), StateRootError>;
61
62    /// Computes the state root of the trie with the changed account and storage prefixes and
63    /// existing trie nodes collecting updates in the process.
64    ///
65    /// # Returns
66    ///
67    /// The intermediate progress of state root computation.
68    fn incremental_root_with_progress(
69        tx: &'a TX,
70        range: RangeInclusive<BlockNumber>,
71    ) -> Result<StateRootProgress, StateRootError>;
72
73    /// Calculate the state root for this [`HashedPostStateSorted`].
74    /// Internally, this method retrieves prefixsets and uses them
75    /// to calculate incremental state root.
76    ///
77    /// # Example
78    ///
79    /// ```
80    /// use alloy_primitives::U256;
81    /// use reth_db::test_utils::create_test_rw_db;
82    /// use reth_db_api::database::Database;
83    /// use reth_primitives_traits::Account;
84    /// use reth_trie::{updates::TrieUpdates, HashedPostState, StateRoot};
85    /// use reth_trie_db::DatabaseStateRoot;
86    ///
87    /// // Initialize the database
88    /// let db = create_test_rw_db();
89    ///
90    /// // Initialize hashed post state
91    /// let mut hashed_state = HashedPostState::default();
92    /// hashed_state.accounts.insert(
93    ///     [0x11; 32].into(),
94    ///     Some(Account { nonce: 1, balance: U256::from(10), bytecode_hash: None }),
95    /// );
96    ///
97    /// // Calculate the state root
98    /// let tx = db.tx().expect("failed to create transaction");
99    /// let state_root = StateRoot::overlay_root(&tx, &hashed_state.into_sorted());
100    /// ```
101    ///
102    /// # Returns
103    ///
104    /// The state root for this [`HashedPostStateSorted`].
105    fn overlay_root(tx: &'a TX, post_state: &HashedPostStateSorted)
106        -> Result<B256, StateRootError>;
107
108    /// Calculates the state root for this [`HashedPostStateSorted`] and returns it alongside trie
109    /// updates. See [`Self::overlay_root`] for more info.
110    fn overlay_root_with_updates(
111        tx: &'a TX,
112        post_state: &HashedPostStateSorted,
113    ) -> Result<(B256, TrieUpdates), StateRootError>;
114
115    /// Calculates the state root for provided [`HashedPostStateSorted`] using cached intermediate
116    /// nodes.
117    fn overlay_root_from_nodes(tx: &'a TX, input: TrieInputSorted) -> Result<B256, StateRootError>;
118
119    /// Calculates the state root and trie updates for provided [`HashedPostStateSorted`] using
120    /// cached intermediate nodes.
121    fn overlay_root_from_nodes_with_updates(
122        tx: &'a TX,
123        input: TrieInputSorted,
124    ) -> Result<(B256, TrieUpdates), StateRootError>;
125}
126
127/// Extends [`HashedPostStateSorted`] with operations specific for working with a database
128/// transaction.
129pub trait DatabaseHashedPostState<TX>: Sized {
130    /// Initializes [`HashedPostStateSorted`] from reverts. Iterates over state reverts in the
131    /// specified range and aggregates them into sorted hashed state.
132    fn from_reverts<KH: KeyHasher>(
133        tx: &TX,
134        range: impl RangeBounds<BlockNumber>,
135    ) -> Result<HashedPostStateSorted, DatabaseError>;
136}
137
138impl<'a, TX: DbTx> DatabaseStateRoot<'a, TX>
139    for StateRoot<DatabaseTrieCursorFactory<&'a TX>, DatabaseHashedCursorFactory<&'a TX>>
140{
141    fn from_tx(tx: &'a TX) -> Self {
142        Self::new(DatabaseTrieCursorFactory::new(tx), DatabaseHashedCursorFactory::new(tx))
143    }
144
145    fn incremental_root_calculator(
146        tx: &'a TX,
147        range: RangeInclusive<BlockNumber>,
148    ) -> Result<Self, StateRootError> {
149        let loaded_prefix_sets = PrefixSetLoader::<_, KeccakKeyHasher>::new(tx).load(range)?;
150        Ok(Self::from_tx(tx).with_prefix_sets(loaded_prefix_sets))
151    }
152
153    fn incremental_root(
154        tx: &'a TX,
155        range: RangeInclusive<BlockNumber>,
156    ) -> Result<B256, StateRootError> {
157        debug!(target: "trie::loader", ?range, "incremental state root");
158        Self::incremental_root_calculator(tx, range)?.root()
159    }
160
161    fn incremental_root_with_updates(
162        tx: &'a TX,
163        range: RangeInclusive<BlockNumber>,
164    ) -> Result<(B256, TrieUpdates), StateRootError> {
165        debug!(target: "trie::loader", ?range, "incremental state root");
166        Self::incremental_root_calculator(tx, range)?.root_with_updates()
167    }
168
169    fn incremental_root_with_progress(
170        tx: &'a TX,
171        range: RangeInclusive<BlockNumber>,
172    ) -> Result<StateRootProgress, StateRootError> {
173        debug!(target: "trie::loader", ?range, "incremental state root with progress");
174        Self::incremental_root_calculator(tx, range)?.root_with_progress()
175    }
176
177    fn overlay_root(
178        tx: &'a TX,
179        post_state: &HashedPostStateSorted,
180    ) -> Result<B256, StateRootError> {
181        let prefix_sets = post_state.construct_prefix_sets().freeze();
182        StateRoot::new(
183            DatabaseTrieCursorFactory::new(tx),
184            HashedPostStateCursorFactory::new(DatabaseHashedCursorFactory::new(tx), post_state),
185        )
186        .with_prefix_sets(prefix_sets)
187        .root()
188    }
189
190    fn overlay_root_with_updates(
191        tx: &'a TX,
192        post_state: &HashedPostStateSorted,
193    ) -> Result<(B256, TrieUpdates), StateRootError> {
194        let prefix_sets = post_state.construct_prefix_sets().freeze();
195        StateRoot::new(
196            DatabaseTrieCursorFactory::new(tx),
197            HashedPostStateCursorFactory::new(DatabaseHashedCursorFactory::new(tx), post_state),
198        )
199        .with_prefix_sets(prefix_sets)
200        .root_with_updates()
201    }
202
203    fn overlay_root_from_nodes(tx: &'a TX, input: TrieInputSorted) -> Result<B256, StateRootError> {
204        StateRoot::new(
205            InMemoryTrieCursorFactory::new(
206                DatabaseTrieCursorFactory::new(tx),
207                input.nodes.as_ref(),
208            ),
209            HashedPostStateCursorFactory::new(
210                DatabaseHashedCursorFactory::new(tx),
211                input.state.as_ref(),
212            ),
213        )
214        .with_prefix_sets(input.prefix_sets.freeze())
215        .root()
216    }
217
218    fn overlay_root_from_nodes_with_updates(
219        tx: &'a TX,
220        input: TrieInputSorted,
221    ) -> Result<(B256, TrieUpdates), StateRootError> {
222        StateRoot::new(
223            InMemoryTrieCursorFactory::new(
224                DatabaseTrieCursorFactory::new(tx),
225                input.nodes.as_ref(),
226            ),
227            HashedPostStateCursorFactory::new(
228                DatabaseHashedCursorFactory::new(tx),
229                input.state.as_ref(),
230            ),
231        )
232        .with_prefix_sets(input.prefix_sets.freeze())
233        .root_with_updates()
234    }
235}
236
237impl<TX: DbTx> DatabaseHashedPostState<TX> for HashedPostStateSorted {
238    /// Builds a sorted hashed post-state from reverts.
239    ///
240    /// Reads MDBX data directly into Vecs, using `HashSet`s only to track seen keys.
241    /// This avoids intermediate `HashMap` allocations since MDBX data is already sorted.
242    ///
243    /// - Reads the first occurrence of each changed account/storage slot in the range.
244    /// - Hashes keys and returns them already ordered for trie iteration.
245    #[instrument(target = "trie::db", skip(tx), fields(range))]
246    fn from_reverts<KH: KeyHasher>(
247        tx: &TX,
248        range: impl RangeBounds<BlockNumber>,
249    ) -> Result<Self, DatabaseError> {
250        // Read accounts directly into Vec with HashSet to track seen keys.
251        // Only keep the first (oldest) occurrence of each account.
252        let mut accounts = Vec::new();
253        let mut seen_accounts = HashSet::new();
254        let account_range = (range.start_bound(), range.end_bound());
255        let mut account_changesets_cursor = tx.cursor_read::<tables::AccountChangeSets>()?;
256
257        for entry in account_changesets_cursor.walk_range(account_range)? {
258            let (_, AccountBeforeTx { address, info }) = entry?;
259            if seen_accounts.insert(address) {
260                accounts.push((KH::hash_key(address), info));
261            }
262        }
263        accounts.sort_unstable_by_key(|(hash, _)| *hash);
264
265        // Read storages directly into B256Map<Vec<_>> with HashSet to track seen keys.
266        // Only keep the first (oldest) occurrence of each (address, slot) pair.
267        let storage_range: BlockNumberAddressRange = range.into();
268        let mut storages = B256Map::<Vec<_>>::default();
269        let mut seen_storage_keys = HashSet::new();
270        let mut storage_changesets_cursor = tx.cursor_read::<tables::StorageChangeSets>()?;
271
272        for entry in storage_changesets_cursor.walk_range(storage_range)? {
273            let (BlockNumberAddress((_, address)), storage) = entry?;
274            if seen_storage_keys.insert((address, storage.key)) {
275                let hashed_address = KH::hash_key(address);
276                storages
277                    .entry(hashed_address)
278                    .or_default()
279                    .push((KH::hash_key(storage.key), storage.value));
280            }
281        }
282
283        // Sort storage slots and convert to HashedStorageSorted
284        let hashed_storages = storages
285            .into_iter()
286            .map(|(address, mut slots)| {
287                slots.sort_unstable_by_key(|(slot, _)| *slot);
288                (address, HashedStorageSorted { storage_slots: slots, wiped: false })
289            })
290            .collect();
291
292        Ok(Self::new(accounts, hashed_storages))
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use alloy_primitives::{hex, map::HashMap, Address, B256, U256};
300    use reth_db::test_utils::create_test_rw_db;
301    use reth_db_api::{
302        database::Database,
303        models::{AccountBeforeTx, BlockNumberAddress},
304        tables,
305        transaction::DbTxMut,
306    };
307    use reth_primitives_traits::{Account, StorageEntry};
308    use reth_trie::{HashedPostState, HashedStorage, KeccakKeyHasher};
309    use revm::state::AccountInfo;
310    use revm_database::BundleState;
311
312    /// Overlay root calculation works with sorted state.
313    #[test]
314    fn overlay_root_with_sorted_state() {
315        let db = create_test_rw_db();
316        let tx = db.tx().expect("failed to create transaction");
317
318        let mut hashed_state = HashedPostState::default();
319        hashed_state.accounts.insert(
320            B256::from(U256::from(1)),
321            Some(Account { nonce: 1, balance: U256::from(10), bytecode_hash: None }),
322        );
323        hashed_state.accounts.insert(B256::from(U256::from(2)), None);
324        hashed_state.storages.insert(
325            B256::from(U256::from(1)),
326            HashedStorage::from_iter(false, [(B256::from(U256::from(3)), U256::from(30))]),
327        );
328
329        let sorted = hashed_state.into_sorted();
330        let overlay_root = StateRoot::overlay_root(&tx, &sorted).unwrap();
331
332        // Just verify it produces a valid root
333        assert!(!overlay_root.is_zero());
334    }
335
336    /// Builds hashed state from a bundle and checks the known state root.
337    #[test]
338    fn from_bundle_state_with_rayon() {
339        let address1 = Address::with_last_byte(1);
340        let address2 = Address::with_last_byte(2);
341        let slot1 = U256::from(1015);
342        let slot2 = U256::from(2015);
343
344        let account1 = AccountInfo { nonce: 1, ..Default::default() };
345        let account2 = AccountInfo { nonce: 2, ..Default::default() };
346
347        let bundle_state = BundleState::builder(2..=2)
348            .state_present_account_info(address1, account1)
349            .state_present_account_info(address2, account2)
350            .state_storage(address1, HashMap::from_iter([(slot1, (U256::ZERO, U256::from(10)))]))
351            .state_storage(address2, HashMap::from_iter([(slot2, (U256::ZERO, U256::from(20)))]))
352            .build();
353        assert_eq!(bundle_state.reverts.len(), 1);
354
355        let post_state = HashedPostState::from_bundle_state::<KeccakKeyHasher>(&bundle_state.state);
356        assert_eq!(post_state.accounts.len(), 2);
357        assert_eq!(post_state.storages.len(), 2);
358
359        let db = create_test_rw_db();
360        let tx = db.tx().expect("failed to create transaction");
361        assert_eq!(
362            StateRoot::overlay_root(&tx, &post_state.into_sorted()).unwrap(),
363            hex!("b464525710cafcf5d4044ac85b72c08b1e76231b8d91f288fe438cc41d8eaafd")
364        );
365    }
366
367    /// Verifies `from_reverts` keeps first occurrence per key and preserves ordering guarantees.
368    #[test]
369    fn from_reverts_keeps_first_occurrence_and_ordering() {
370        let db = create_test_rw_db();
371        let tx = db.tx_mut().expect("failed to create rw tx");
372
373        let address1 = Address::with_last_byte(1);
374        let address2 = Address::with_last_byte(2);
375        let slot1 = B256::from(U256::from(11));
376        let slot2 = B256::from(U256::from(22));
377
378        // Account changesets: only first occurrence per address should be kept.
379        tx.put::<tables::AccountChangeSets>(
380            1,
381            AccountBeforeTx {
382                address: address1,
383                info: Some(Account { nonce: 1, ..Default::default() }),
384            },
385        )
386        .unwrap();
387        tx.put::<tables::AccountChangeSets>(
388            2,
389            AccountBeforeTx {
390                address: address1,
391                info: Some(Account { nonce: 2, ..Default::default() }),
392            },
393        )
394        .unwrap();
395        tx.put::<tables::AccountChangeSets>(3, AccountBeforeTx { address: address2, info: None })
396            .unwrap();
397
398        // Storage changesets: only first occurrence per slot should be kept, and slots sorted.
399        tx.put::<tables::StorageChangeSets>(
400            BlockNumberAddress((1, address1)),
401            StorageEntry { key: slot2, value: U256::from(200) },
402        )
403        .unwrap();
404        tx.put::<tables::StorageChangeSets>(
405            BlockNumberAddress((2, address1)),
406            StorageEntry { key: slot1, value: U256::from(100) },
407        )
408        .unwrap();
409        tx.put::<tables::StorageChangeSets>(
410            BlockNumberAddress((3, address1)),
411            StorageEntry { key: slot1, value: U256::from(999) }, // should be ignored
412        )
413        .unwrap();
414
415        tx.commit().unwrap();
416        let tx = db.tx().expect("failed to create ro tx");
417
418        let sorted = HashedPostStateSorted::from_reverts::<KeccakKeyHasher>(&tx, 1..=3).unwrap();
419
420        // Verify first occurrences were kept (nonce 1, not 2)
421        assert_eq!(sorted.accounts.len(), 2);
422        let hashed_addr1 = KeccakKeyHasher::hash_key(address1);
423        let account1 = sorted.accounts.iter().find(|(addr, _)| *addr == hashed_addr1).unwrap();
424        assert_eq!(account1.1.unwrap().nonce, 1);
425
426        // Ordering guarantees - accounts sorted by hashed address
427        assert!(sorted.accounts.windows(2).all(|w| w[0].0 <= w[1].0));
428
429        // Ordering guarantees - storage slots sorted by hashed slot
430        for storage in sorted.storages.values() {
431            assert!(storage.storage_slots.windows(2).all(|w| w[0].0 <= w[1].0));
432        }
433    }
434
435    /// Empty block range returns empty state.
436    #[test]
437    fn from_reverts_empty_range() {
438        let db = create_test_rw_db();
439
440        // Insert data outside the query range
441        db.update(|tx| {
442            tx.put::<tables::AccountChangeSets>(
443                100,
444                AccountBeforeTx {
445                    address: Address::with_last_byte(1),
446                    info: Some(Account { nonce: 1, ..Default::default() }),
447                },
448            )
449            .unwrap();
450        })
451        .unwrap();
452
453        let tx = db.tx().unwrap();
454
455        // Query a range with no data
456        let sorted = HashedPostStateSorted::from_reverts::<KeccakKeyHasher>(&tx, 1..=10).unwrap();
457        assert!(sorted.accounts.is_empty());
458        assert!(sorted.storages.is_empty());
459    }
460}