Skip to main content

reth_trie/
test_utils.rs

1use alloy_primitives::{Address, B256, U256};
2use alloy_rlp::encode_fixed_size;
3use reth_primitives_traits::Account;
4use reth_trie_common::triehash::KeccakHasher;
5
6/// Re-export of [triehash].
7pub use triehash;
8
9/// Compute the state root of a given set of accounts using [`triehash::sec_trie_root`].
10pub fn state_root<I, S>(accounts: I) -> B256
11where
12    I: IntoIterator<Item = (Address, (Account, S))>,
13    S: IntoIterator<Item = (B256, U256)>,
14{
15    let encoded_accounts = accounts.into_iter().map(|(address, (account, storage))| {
16        let storage_root = storage_root(storage);
17        let account = account.into_trie_account(storage_root);
18        (address, alloy_rlp::encode(account))
19    });
20    triehash::sec_trie_root::<KeccakHasher, _, _, _>(encoded_accounts)
21}
22
23/// Compute the storage root for a given account using [`triehash::sec_trie_root`].
24pub fn storage_root<I: IntoIterator<Item = (B256, U256)>>(storage: I) -> B256 {
25    let encoded_storage = storage.into_iter().map(|(k, v)| (k, encode_fixed_size(&v)));
26    triehash::sec_trie_root::<KeccakHasher, _, _, _>(encoded_storage)
27}
28
29/// Compute the state root of a given set of accounts with prehashed keys using
30/// [`triehash::trie_root`].
31pub fn state_root_prehashed<I, S>(accounts: I) -> B256
32where
33    I: IntoIterator<Item = (B256, (Account, S))>,
34    S: IntoIterator<Item = (B256, U256)>,
35{
36    let encoded_accounts = accounts.into_iter().map(|(address, (account, storage))| {
37        let storage_root = storage_root_prehashed(storage);
38        let account = account.into_trie_account(storage_root);
39        (address, alloy_rlp::encode(account))
40    });
41
42    triehash::trie_root::<KeccakHasher, _, _, _>(encoded_accounts)
43}
44
45/// Compute the storage root for a given account with prehashed slots using [`triehash::trie_root`].
46pub fn storage_root_prehashed<I: IntoIterator<Item = (B256, U256)>>(storage: I) -> B256 {
47    let encoded_storage = storage.into_iter().map(|(k, v)| (k, encode_fixed_size(&v)));
48    triehash::trie_root::<KeccakHasher, _, _, _>(encoded_storage)
49}
50
51// ---------------------------------------------------------------------------
52// Trie test harness
53// ---------------------------------------------------------------------------
54
55use crate::{
56    hashed_cursor::{
57        mock::MockHashedCursorFactory, HashedCursorFactory, HashedPostStateCursorFactory,
58    },
59    proof_v2::StorageProofCalculator,
60    trie_cursor::{mock::MockTrieCursorFactory, TrieCursorFactory},
61    StorageRoot,
62};
63use alloy_primitives::map::HashSet;
64use reth_trie_common::{
65    prefix_set::PrefixSetMut, updates::StorageTrieUpdates, BranchNodeCompact,
66    HashedPostStateSorted, HashedStorage, Nibbles, ProofTrieNodeV2, ProofV2Target,
67};
68use std::{collections::BTreeMap, iter::once};
69
70/// General-purpose test harness for storage trie tests.
71///
72/// Manages a base storage dataset, computes expected roots via [`StorageRoot`], and generates
73/// V2 proofs via [`StorageProofCalculator`] using mock cursors.
74#[derive(Debug)]
75pub struct TrieTestHarness {
76    /// The base storage dataset (hashed slot → value). Zero-valued entries are absent.
77    storage: BTreeMap<B256, U256>,
78    /// The expected storage root, calculated by [`StorageRoot`].
79    original_root: B256,
80    /// The starting storage trie updates, used for minimization.
81    storage_trie_updates: StorageTrieUpdates,
82    /// Mock factory for trie cursors.
83    trie_cursor_factory: MockTrieCursorFactory,
84    /// Mock factory for hashed cursors.
85    hashed_cursor_factory: MockHashedCursorFactory,
86}
87
88impl TrieTestHarness {
89    /// Creates a new test harness from a map of hashed storage slots to values.
90    pub fn new(storage: BTreeMap<B256, U256>) -> Self {
91        let mut harness = Self {
92            storage,
93            original_root: B256::ZERO,
94            storage_trie_updates: StorageTrieUpdates::default(),
95            trie_cursor_factory: MockTrieCursorFactory::new(
96                BTreeMap::new(),
97                once((B256::ZERO, BTreeMap::new())).collect(),
98            ),
99            hashed_cursor_factory: MockHashedCursorFactory::new(
100                BTreeMap::new(),
101                once((B256::ZERO, BTreeMap::new())).collect(),
102            ),
103        };
104        harness.rebuild();
105        harness
106    }
107
108    /// Computes the storage root and trie updates after applying the given changeset on top
109    /// of the current base storage.
110    ///
111    /// Builds a [`HashedPostStateCursorFactory`] overlay, derives a prefix set from the
112    /// changeset keys, and passes both into [`StorageRoot::new_hashed`].
113    pub fn get_root_with_updates(
114        &self,
115        changeset: &BTreeMap<B256, U256>,
116    ) -> (B256, StorageTrieUpdates) {
117        let mut prefix_set = PrefixSetMut::with_capacity(changeset.len());
118        for hashed_slot in changeset.keys() {
119            prefix_set.insert(Nibbles::unpack(hashed_slot));
120        }
121
122        let hashed_storage =
123            HashedStorage::from_iter(false, changeset.iter().map(|(&k, &v)| (k, v)));
124        let overlay = HashedPostStateSorted::new(
125            Vec::new(),
126            once((self.hashed_address(), hashed_storage.into_sorted())).collect(),
127        );
128        let overlay_cursor_factory =
129            HashedPostStateCursorFactory::new(self.hashed_cursor_factory.clone(), &overlay);
130
131        let (root, _, updates) = StorageRoot::new_hashed(
132            self.trie_cursor_factory.clone(),
133            overlay_cursor_factory,
134            self.hashed_address(),
135            prefix_set.freeze(),
136            #[cfg(feature = "metrics")]
137            crate::metrics::TrieRootMetrics::new(crate::TrieType::Storage),
138        )
139        .root_with_updates()
140        .expect("StorageRoot should succeed");
141
142        (root, updates)
143    }
144
145    /// Merges `changeset` into the base storage (zero values remove entries) and
146    /// rebuilds the harness from scratch with the resulting storage.
147    pub fn apply_changeset(&mut self, changeset: BTreeMap<B256, U256>) {
148        for (k, v) in changeset {
149            if v == U256::ZERO {
150                self.storage.remove(&k);
151            } else {
152                self.storage.insert(k, v);
153            }
154        }
155        self.rebuild();
156    }
157
158    /// Recomputes the storage root, trie updates, and cursor factories from `self.storage`.
159    fn rebuild(&mut self) {
160        self.hashed_cursor_factory = MockHashedCursorFactory::new(
161            BTreeMap::new(),
162            once((self.hashed_address(), self.storage.clone())).collect(),
163        );
164
165        let (root, _, updates) = StorageRoot::new_hashed(
166            MockTrieCursorFactory::new(
167                BTreeMap::new(),
168                once((self.hashed_address(), BTreeMap::new())).collect(),
169            ),
170            self.hashed_cursor_factory.clone(),
171            self.hashed_address(),
172            crate::prefix_set::PrefixSet::default(),
173            #[cfg(feature = "metrics")]
174            crate::metrics::TrieRootMetrics::new(crate::TrieType::Storage),
175        )
176        .root_with_updates()
177        .expect("StorageRoot should succeed");
178
179        self.trie_cursor_factory = MockTrieCursorFactory::new(
180            BTreeMap::new(),
181            once((
182                self.hashed_address(),
183                updates.storage_nodes.iter().map(|(k, v)| (*k, v.clone())).collect(),
184            ))
185            .collect(),
186        );
187
188        self.original_root = root;
189        self.storage_trie_updates = updates;
190    }
191
192    /// Returns the hashed address used for all storage trie operations.
193    pub const fn hashed_address(&self) -> B256 {
194        B256::ZERO
195    }
196
197    /// Returns a reference to the base storage dataset.
198    pub const fn storage(&self) -> &BTreeMap<B256, U256> {
199        &self.storage
200    }
201
202    /// Returns the expected storage root.
203    pub const fn original_root(&self) -> B256 {
204        self.original_root
205    }
206
207    /// Returns a reference to the storage trie updates.
208    pub const fn storage_trie_updates(&self) -> &StorageTrieUpdates {
209        &self.storage_trie_updates
210    }
211
212    /// Replaces the trie cursor factory with one backed by the given trie nodes.
213    pub fn set_trie_nodes(&mut self, trie_nodes: BTreeMap<Nibbles, BranchNodeCompact>) {
214        self.trie_cursor_factory = MockTrieCursorFactory::new(
215            BTreeMap::new(),
216            once((self.hashed_address(), trie_nodes)).collect(),
217        );
218    }
219
220    /// Returns a clone of the mock trie cursor factory.
221    pub fn trie_cursor_factory(&self) -> MockTrieCursorFactory {
222        self.trie_cursor_factory.clone()
223    }
224
225    /// Returns a clone of the mock hashed cursor factory.
226    pub fn hashed_cursor_factory(&self) -> MockHashedCursorFactory {
227        self.hashed_cursor_factory.clone()
228    }
229
230    /// Obtains the root node of the storage trie via [`StorageProofCalculator`].
231    pub fn root_node(&self) -> ProofTrieNodeV2 {
232        let trie_cursor = self
233            .trie_cursor_factory
234            .storage_trie_cursor(self.hashed_address())
235            .expect("storage trie cursor should succeed");
236        let hashed_cursor = self
237            .hashed_cursor_factory
238            .hashed_storage_cursor(self.hashed_address())
239            .expect("hashed storage cursor should succeed");
240
241        let mut proof_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
242        proof_calculator
243            .storage_root_node(self.hashed_address())
244            .expect("storage_root_node should succeed")
245    }
246
247    /// Generates storage proofs for the given targets using [`StorageProofCalculator`].
248    ///
249    /// Also computes and returns the root hash (if the proof contains a root node) by reusing
250    /// the calculator after the proof call.
251    pub fn proof_v2(&self, targets: &mut [ProofV2Target]) -> (Vec<ProofTrieNodeV2>, Option<B256>) {
252        let trie_cursor = self
253            .trie_cursor_factory
254            .storage_trie_cursor(self.hashed_address())
255            .expect("storage trie cursor should succeed");
256        let hashed_cursor = self
257            .hashed_cursor_factory
258            .hashed_storage_cursor(self.hashed_address())
259            .expect("hashed storage cursor should succeed");
260
261        let mut proof_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
262        let proofs = proof_calculator
263            .storage_proof(self.hashed_address(), targets)
264            .expect("proof_v2 should succeed");
265        let root_hash =
266            proof_calculator.compute_root_hash(&proofs).expect("compute_root_hash should succeed");
267        (proofs, root_hash)
268    }
269
270    /// Removes all entries from `updates` that are redundant with the starting storage
271    /// trie updates.
272    ///
273    /// A storage node is redundant if it exists in the starting set with the same value.
274    /// A removed node is redundant if it was already absent from the starting set.
275    /// The `is_deleted` flag is cleared if it matches the starting value.
276    pub fn minimize_trie_updates(&self, updates: &mut StorageTrieUpdates) {
277        if updates.is_deleted == self.storage_trie_updates.is_deleted {
278            updates.is_deleted = false;
279        }
280
281        // StorageTrieUpdates::finalize can leave the same path in both storage_nodes
282        // and removed_nodes. Per into_sorted, updated nodes take precedence over
283        // removed ones. Record which paths had an update before minimization so we
284        // can drop their corresponding removals.
285        let paths_with_updates: HashSet<Nibbles> = updates.storage_nodes.keys().copied().collect();
286
287        updates
288            .storage_nodes
289            .retain(|path, node| self.storage_trie_updates.storage_nodes.get(path) != Some(node));
290
291        updates.removed_nodes.retain(|path| {
292            self.storage_trie_updates.storage_nodes.contains_key(path) &&
293                !paths_with_updates.contains(path)
294        });
295    }
296}