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 = HashedStorage::from_iter(changeset.iter().map(|(&k, &v)| (k, v)));
123        let overlay = HashedPostStateSorted::new(
124            Vec::new(),
125            once((self.hashed_address(), hashed_storage.into_sorted())).collect(),
126        );
127        let overlay_cursor_factory =
128            HashedPostStateCursorFactory::new(self.hashed_cursor_factory.clone(), &overlay);
129
130        let (root, _, updates) = StorageRoot::new_hashed(
131            self.trie_cursor_factory.clone(),
132            overlay_cursor_factory,
133            self.hashed_address(),
134            prefix_set.freeze(),
135            #[cfg(feature = "metrics")]
136            crate::metrics::TrieRootMetrics::new(crate::TrieType::Storage),
137        )
138        .root_with_updates()
139        .expect("StorageRoot should succeed");
140
141        (root, updates)
142    }
143
144    /// Merges `changeset` into the base storage (zero values remove entries) and
145    /// rebuilds the harness from scratch with the resulting storage.
146    pub fn apply_changeset(&mut self, changeset: BTreeMap<B256, U256>) {
147        for (k, v) in changeset {
148            if v == U256::ZERO {
149                self.storage.remove(&k);
150            } else {
151                self.storage.insert(k, v);
152            }
153        }
154        self.rebuild();
155    }
156
157    /// Recomputes the storage root, trie updates, and cursor factories from `self.storage`.
158    fn rebuild(&mut self) {
159        self.hashed_cursor_factory = MockHashedCursorFactory::new(
160            BTreeMap::new(),
161            once((self.hashed_address(), self.storage.clone())).collect(),
162        );
163
164        let (root, _, updates) = StorageRoot::new_hashed(
165            MockTrieCursorFactory::new(
166                BTreeMap::new(),
167                once((self.hashed_address(), BTreeMap::new())).collect(),
168            ),
169            self.hashed_cursor_factory.clone(),
170            self.hashed_address(),
171            crate::prefix_set::PrefixSet::default(),
172            #[cfg(feature = "metrics")]
173            crate::metrics::TrieRootMetrics::new(crate::TrieType::Storage),
174        )
175        .root_with_updates()
176        .expect("StorageRoot should succeed");
177
178        self.trie_cursor_factory = MockTrieCursorFactory::new(
179            BTreeMap::new(),
180            once((
181                self.hashed_address(),
182                updates.storage_nodes.iter().map(|(k, v)| (*k, v.clone())).collect(),
183            ))
184            .collect(),
185        );
186
187        self.original_root = root;
188        self.storage_trie_updates = updates;
189    }
190
191    /// Returns the hashed address used for all storage trie operations.
192    pub const fn hashed_address(&self) -> B256 {
193        B256::ZERO
194    }
195
196    /// Returns a reference to the base storage dataset.
197    pub const fn storage(&self) -> &BTreeMap<B256, U256> {
198        &self.storage
199    }
200
201    /// Returns the expected storage root.
202    pub const fn original_root(&self) -> B256 {
203        self.original_root
204    }
205
206    /// Returns a reference to the storage trie updates.
207    pub const fn storage_trie_updates(&self) -> &StorageTrieUpdates {
208        &self.storage_trie_updates
209    }
210
211    /// Replaces the trie cursor factory with one backed by the given trie nodes.
212    pub fn set_trie_nodes(&mut self, trie_nodes: BTreeMap<Nibbles, BranchNodeCompact>) {
213        self.trie_cursor_factory = MockTrieCursorFactory::new(
214            BTreeMap::new(),
215            once((self.hashed_address(), trie_nodes)).collect(),
216        );
217    }
218
219    /// Returns a clone of the mock trie cursor factory.
220    pub fn trie_cursor_factory(&self) -> MockTrieCursorFactory {
221        self.trie_cursor_factory.clone()
222    }
223
224    /// Returns a clone of the mock hashed cursor factory.
225    pub fn hashed_cursor_factory(&self) -> MockHashedCursorFactory {
226        self.hashed_cursor_factory.clone()
227    }
228
229    /// Obtains the root node of the storage trie via [`StorageProofCalculator`].
230    pub fn root_node(&self) -> ProofTrieNodeV2 {
231        let trie_cursor = self
232            .trie_cursor_factory
233            .storage_trie_cursor(self.hashed_address())
234            .expect("storage trie cursor should succeed");
235        let hashed_cursor = self
236            .hashed_cursor_factory
237            .hashed_storage_cursor(self.hashed_address())
238            .expect("hashed storage cursor should succeed");
239
240        let mut proof_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
241        proof_calculator
242            .storage_root_node(self.hashed_address())
243            .expect("storage_root_node should succeed")
244    }
245
246    /// Generates storage proofs for the given targets using [`StorageProofCalculator`].
247    ///
248    /// Also computes and returns the root hash (if the proof contains a root node) by reusing
249    /// the calculator after the proof call.
250    pub fn proof_v2(&self, targets: &mut [ProofV2Target]) -> (Vec<ProofTrieNodeV2>, Option<B256>) {
251        let trie_cursor = self
252            .trie_cursor_factory
253            .storage_trie_cursor(self.hashed_address())
254            .expect("storage trie cursor should succeed");
255        let hashed_cursor = self
256            .hashed_cursor_factory
257            .hashed_storage_cursor(self.hashed_address())
258            .expect("hashed storage cursor should succeed");
259
260        let mut proof_calculator = StorageProofCalculator::new_storage(trie_cursor, hashed_cursor);
261        let proofs = proof_calculator
262            .storage_proof(self.hashed_address(), targets)
263            .expect("proof_v2 should succeed");
264        let root_hash =
265            proof_calculator.compute_root_hash(&proofs).expect("compute_root_hash should succeed");
266        (proofs, root_hash)
267    }
268
269    /// Removes all entries from `updates` that are redundant with the starting storage
270    /// trie updates.
271    ///
272    /// A storage node is redundant if it exists in the starting set with the same value.
273    /// A removed node is redundant if it was already absent from the starting set.
274    /// The `is_deleted` flag is cleared if it matches the starting value.
275    pub fn minimize_trie_updates(&self, updates: &mut StorageTrieUpdates) {
276        if updates.is_deleted == self.storage_trie_updates.is_deleted {
277            updates.is_deleted = false;
278        }
279
280        // StorageTrieUpdates::finalize can leave the same path in both storage_nodes
281        // and removed_nodes. Per into_sorted, updated nodes take precedence over
282        // removed ones. Record which paths had an update before minimization so we
283        // can drop their corresponding removals.
284        let paths_with_updates: HashSet<Nibbles> = updates.storage_nodes.keys().copied().collect();
285
286        updates
287            .storage_nodes
288            .retain(|path, node| self.storage_trie_updates.storage_nodes.get(path) != Some(node));
289
290        updates.removed_nodes.retain(|path| {
291            self.storage_trie_updates.storage_nodes.contains_key(path) &&
292                !paths_with_updates.contains(path)
293        });
294    }
295}