Skip to main content

reth_trie/hashed_cursor/
post_state.rs

1use super::{HashedCursor, HashedCursorFactory, HashedStorageCursor};
2use crate::forward_cursor::ForwardInMemoryCursor;
3use alloy_primitives::{B256, U256};
4use reth_primitives_traits::Account;
5use reth_storage_errors::db::DatabaseError;
6use reth_trie_common::HashedPostStateSorted;
7
8/// The hashed cursor factory for the post state.
9#[derive(Clone, Debug)]
10pub struct HashedPostStateCursorFactory<CF, T> {
11    cursor_factory: CF,
12    post_state: T,
13}
14
15impl<CF, T> HashedPostStateCursorFactory<CF, T> {
16    /// Create a new factory.
17    pub const fn new(cursor_factory: CF, post_state: T) -> Self {
18        Self { cursor_factory, post_state }
19    }
20}
21
22impl<'overlay, CF, T> HashedCursorFactory for HashedPostStateCursorFactory<CF, &'overlay T>
23where
24    CF: HashedCursorFactory,
25    T: AsRef<HashedPostStateSorted>,
26{
27    type AccountCursor<'cursor>
28        = HashedPostStateCursor<'overlay, CF::AccountCursor<'cursor>, Option<Account>>
29    where
30        Self: 'cursor;
31    type StorageCursor<'cursor>
32        = HashedPostStateCursor<'overlay, CF::StorageCursor<'cursor>, U256>
33    where
34        Self: 'cursor;
35
36    fn hashed_account_cursor(&self) -> Result<Self::AccountCursor<'_>, DatabaseError> {
37        let cursor = self.cursor_factory.hashed_account_cursor()?;
38        Ok(HashedPostStateCursor::new_account(cursor, self.post_state.as_ref()))
39    }
40
41    fn hashed_storage_cursor(
42        &self,
43        hashed_address: B256,
44    ) -> Result<Self::StorageCursor<'_>, DatabaseError> {
45        let post_state = self.post_state.as_ref();
46        let cursor = self.cursor_factory.hashed_storage_cursor(hashed_address)?;
47        Ok(HashedPostStateCursor::new_storage(cursor, post_state, hashed_address))
48    }
49}
50
51/// Trait for types that can be used with [`HashedPostStateCursor`] as a value.
52///
53/// This enables uniform handling of deletions across different wrapper types:
54/// - `Option<Account>`: `None` indicates deletion
55/// - `U256`: `U256::ZERO` indicates deletion (maps to `None`)
56///
57/// This design allows us to use `U256::ZERO`, rather than an Option, to indicate deletion for
58/// storage (which maps cleanly to how changesets are stored in the DB) while not requiring two
59/// different cursor implementations.
60pub trait HashedPostStateCursorValue: Clone {
61    /// The non-zero type returned by `into_option`.
62    /// For `Option<Account>`, this is `Account`.
63    /// For `U256`, this is `U256`.
64    type NonZero: Clone + std::fmt::Debug;
65
66    /// Returns `Some(&NonZero)` if the value is present, `None` if deleted.
67    fn into_option(self) -> Option<Self::NonZero>;
68}
69
70impl HashedPostStateCursorValue for Option<Account> {
71    type NonZero = Account;
72
73    fn into_option(self) -> Option<Self::NonZero> {
74        self
75    }
76}
77
78impl HashedPostStateCursorValue for U256 {
79    type NonZero = Self;
80
81    fn into_option(self) -> Option<Self::NonZero> {
82        (!self.is_zero()).then_some(self)
83    }
84}
85
86/// A cursor to iterate over state updates and corresponding database entries.
87/// It will always give precedence to the data from the post state updates.
88#[derive(Debug)]
89pub struct HashedPostStateCursor<'a, C, V>
90where
91    V: HashedPostStateCursorValue,
92{
93    /// The underlying cursor.
94    cursor: C,
95    /// Tracks whether the DB cursor is available, positioned, or exhausted.
96    db_cursor_state: DbCursorState<V::NonZero>,
97    /// Forward-only in-memory cursor over underlying V.
98    post_state_cursor: ForwardInMemoryCursor<'a, B256, V>,
99    /// The last hashed key that was returned by the cursor.
100    /// De facto, this is a current cursor position.
101    last_key: Option<B256>,
102    #[cfg(debug_assertions)]
103    /// Tracks whether `seek` has been called.
104    seeked: bool,
105    /// Reference to the full post state.
106    post_state: &'a HashedPostStateSorted,
107}
108
109#[derive(Debug)]
110enum DbCursorState<V> {
111    NeedsPosition,
112    Positioned((B256, V)),
113    Exhausted,
114}
115
116impl<V> DbCursorState<V> {
117    const fn entry(&self) -> Option<&(B256, V)> {
118        match self {
119            Self::Positioned(entry) => Some(entry),
120            Self::NeedsPosition | Self::Exhausted => None,
121        }
122    }
123
124    fn set_entry(&mut self, entry: Option<(B256, V)>) {
125        *self = match entry {
126            Some(entry) => Self::Positioned(entry),
127            None => Self::Exhausted,
128        };
129    }
130}
131
132impl<'a, C> HashedPostStateCursor<'a, C, Option<Account>>
133where
134    C: HashedCursor<Value = Account>,
135{
136    /// Create new account cursor which combines a DB cursor and the post state.
137    pub fn new_account(cursor: C, post_state: &'a HashedPostStateSorted) -> Self {
138        let post_state_cursor = ForwardInMemoryCursor::new(&post_state.accounts);
139        Self {
140            cursor,
141            db_cursor_state: DbCursorState::NeedsPosition,
142            post_state_cursor,
143            last_key: None,
144            #[cfg(debug_assertions)]
145            seeked: false,
146            post_state,
147        }
148    }
149}
150
151impl<'a, C> HashedPostStateCursor<'a, C, U256>
152where
153    C: HashedStorageCursor<Value = U256>,
154{
155    /// Create new storage cursor with full post state reference.
156    /// This allows the cursor to switch between storage tries when `set_hashed_address` is called.
157    pub fn new_storage(
158        cursor: C,
159        post_state: &'a HashedPostStateSorted,
160        hashed_address: B256,
161    ) -> Self {
162        let post_state_cursor = Self::get_storage_overlay(post_state, hashed_address);
163        Self {
164            cursor,
165            db_cursor_state: DbCursorState::NeedsPosition,
166            post_state_cursor,
167            last_key: None,
168            #[cfg(debug_assertions)]
169            seeked: false,
170            post_state,
171        }
172    }
173
174    /// Returns the storage overlay for `hashed_address`.
175    fn get_storage_overlay(
176        post_state: &'a HashedPostStateSorted,
177        hashed_address: B256,
178    ) -> ForwardInMemoryCursor<'a, B256, U256> {
179        let post_state_storage = post_state.storages.get(&hashed_address);
180        let storage_slots = post_state_storage.map(|u| u.storage_slots_ref()).unwrap_or(&[]);
181
182        ForwardInMemoryCursor::new(storage_slots)
183    }
184}
185
186impl<'a, C, V> HashedPostStateCursor<'a, C, V>
187where
188    C: HashedCursor<Value = V::NonZero>,
189    V: HashedPostStateCursorValue,
190{
191    const fn get_cursor_mut(&mut self) -> &mut C {
192        &mut self.cursor
193    }
194
195    /// Asserts that the next entry to be returned from the cursor is not previous to the last entry
196    /// returned.
197    fn set_last_key(&mut self, next_entry: &Option<(B256, V::NonZero)>) {
198        let next_key = next_entry.as_ref().map(|e| e.0);
199        debug_assert!(
200            self.last_key.is_none_or(|last| next_key.is_none_or(|next| next >= last)),
201            "Cannot return entry {:?} previous to the last returned entry at {:?}",
202            next_key,
203            self.last_key,
204        );
205        self.last_key = next_key;
206    }
207
208    /// Positions the DB cursor state using the underlying cursor when needed.
209    fn cursor_seek(&mut self, key: B256) -> Result<(), DatabaseError> {
210        // Only seek if:
211        // 1. We have a cursor entry and need to seek forward (entry.0 < key), OR
212        // 2. The DB cursor needs to be positioned.
213        let should_seek = match &self.db_cursor_state {
214            DbCursorState::NeedsPosition => true,
215            DbCursorState::Positioned((entry_key, _)) => entry_key < &key,
216            DbCursorState::Exhausted => false,
217        };
218
219        if should_seek {
220            let entry = self.get_cursor_mut().seek(key)?;
221            self.db_cursor_state.set_entry(entry);
222        }
223
224        Ok(())
225    }
226
227    /// Advances the DB cursor state to the subsequent entry using the underlying cursor.
228    fn cursor_next(&mut self) -> Result<(), DatabaseError> {
229        #[cfg(debug_assertions)]
230        {
231            debug_assert!(self.seeked);
232        }
233
234        // Exhausted DB state is stable; only advance when the DB cursor is positioned at an entry.
235        if matches!(self.db_cursor_state, DbCursorState::Positioned(_)) {
236            let entry = self.get_cursor_mut().next()?;
237            self.db_cursor_state.set_entry(entry);
238        }
239
240        Ok(())
241    }
242
243    /// Compares the current in-memory entry with the current entry of the cursor, and applies the
244    /// in-memory entry to the cursor entry as an overlay.
245    ///
246    /// This may consume and move forward the current entries when the overlay indicates a removed
247    /// node.
248    fn choose_next_entry(&mut self) -> Result<Option<(B256, V::NonZero)>, DatabaseError> {
249        loop {
250            let post_state_current =
251                self.post_state_cursor.current().cloned().map(|(k, v)| (k, v.into_option()));
252            let db_entry = self.db_cursor_state.entry();
253
254            match (post_state_current, db_entry) {
255                (Some((mem_key, None)), _)
256                    if db_entry.is_none_or(|(db_key, _)| &mem_key < db_key) =>
257                {
258                    // If overlay has a removed value but DB cursor is exhausted or ahead of the
259                    // in-memory cursor then move ahead in-memory, as there might be further
260                    // non-removed overlay values.
261                    self.post_state_cursor.first_after(&mem_key);
262                }
263                (Some((mem_key, None)), Some((db_key, _))) if &mem_key == db_key => {
264                    // If overlay has a removed value which is returned from DB then move both
265                    // cursors ahead to the next key.
266                    self.post_state_cursor.first_after(&mem_key);
267                    self.cursor_next()?;
268                }
269                (Some((mem_key, Some(value))), _)
270                    if db_entry.is_none_or(|(db_key, _)| &mem_key <= db_key) =>
271                {
272                    // If overlay returns a value prior to the DB's value, or the DB is exhausted,
273                    // then we return the overlay's value.
274                    return Ok(Some((mem_key, value)))
275                }
276                // All other cases:
277                // - mem_key > db_key
278                // - overlay is exhausted
279                // Return the db_entry. If DB is also exhausted then this returns None.
280                _ => return Ok(db_entry.cloned()),
281            }
282        }
283    }
284}
285
286impl<C, V> HashedCursor for HashedPostStateCursor<'_, C, V>
287where
288    C: HashedCursor<Value = V::NonZero>,
289    V: HashedPostStateCursorValue,
290{
291    type Value = V::NonZero;
292
293    /// Seek the next entry for a given hashed key.
294    ///
295    /// If the post state contains the exact match for the key, return it.
296    /// Otherwise, retrieve the next entries that are greater than or equal to the key from the
297    /// database and the post state. The two entries are compared and the lowest is returned.
298    ///
299    /// The returned account key is memoized and the cursor remains positioned at that key until
300    /// [`HashedCursor::seek`] or [`HashedCursor::next`] are called.
301    fn seek(&mut self, key: B256) -> Result<Option<(B256, Self::Value)>, DatabaseError> {
302        // Forward scans can reuse cursor positions; backward seeks and exhausted cursors cannot.
303        if self.last_key.is_none_or(|last| key < last) {
304            self.reset();
305        }
306        let post_state_entry =
307            self.post_state_cursor.seek(&key).cloned().map(|(k, v)| (k, v.into_option()));
308
309        if let Some((mem_key, Some(value))) = post_state_entry &&
310            mem_key == key
311        {
312            #[cfg(debug_assertions)]
313            {
314                self.seeked = true;
315            }
316
317            // An exact overlay hit is the first logical entry at or after `key`, so the DB cursor
318            // can stay lazy until a later operation needs it.
319            if matches!(&self.db_cursor_state, DbCursorState::Positioned((db_key, _)) if db_key < &key)
320            {
321                self.db_cursor_state = DbCursorState::NeedsPosition;
322            }
323
324            let entry = Some((key, value));
325            self.set_last_key(&entry);
326            return Ok(entry)
327        }
328
329        self.cursor_seek(key)?;
330
331        #[cfg(debug_assertions)]
332        {
333            self.seeked = true;
334        }
335
336        let entry = self.choose_next_entry()?;
337        self.set_last_key(&entry);
338        Ok(entry)
339    }
340
341    /// Retrieve the next entry from the cursor.
342    ///
343    /// If the cursor is positioned at the entry, return the entry with next greater key.
344    /// Returns [None] if the previous memoized or the next greater entries are missing.
345    ///
346    /// NOTE: This function will not return any entry unless [`HashedCursor::seek`] has been called.
347    fn next(&mut self) -> Result<Option<(B256, Self::Value)>, DatabaseError> {
348        #[cfg(debug_assertions)]
349        {
350            debug_assert!(self.seeked, "Cursor must be seek'd before next is called");
351        }
352
353        // A `last_key` of `None` indicates that the cursor is exhausted.
354        let Some(last_key) = self.last_key else {
355            return Ok(None);
356        };
357
358        // If either cursor is currently pointing to the last entry which was returned then consume
359        // that entry so that `choose_next_entry` is looking at the subsequent one.
360        if let Some((key, _)) = self.post_state_cursor.current() &&
361            key == &last_key
362        {
363            self.post_state_cursor.first_after(&last_key);
364        }
365
366        if matches!(self.db_cursor_state, DbCursorState::NeedsPosition) {
367            self.cursor_seek(last_key)?;
368        }
369
370        if let Some((key, _)) = self.db_cursor_state.entry() &&
371            key == &last_key
372        {
373            self.cursor_next()?;
374        }
375
376        let entry = self.choose_next_entry()?;
377        self.set_last_key(&entry);
378        Ok(entry)
379    }
380
381    fn reset(&mut self) {
382        let Self { cursor, db_cursor_state, post_state_cursor, last_key, .. } = self;
383
384        cursor.reset();
385        post_state_cursor.reset();
386
387        *db_cursor_state = DbCursorState::NeedsPosition;
388        *last_key = None;
389        #[cfg(debug_assertions)]
390        {
391            self.seeked = false;
392        }
393    }
394}
395
396/// The cursor to iterate over post state hashed values and corresponding database entries.
397/// It will always give precedence to the data from the post state.
398impl<C> HashedStorageCursor for HashedPostStateCursor<'_, C, U256>
399where
400    C: HashedStorageCursor<Value = U256>,
401{
402    /// Returns `true` if the account has no storage entries.
403    ///
404    /// This function should be called before attempting to call [`HashedCursor::seek`] or
405    /// [`HashedCursor::next`].
406    fn is_storage_empty(&mut self) -> Result<bool, DatabaseError> {
407        let is_empty = self.seek(B256::ZERO)?.is_none();
408        self.reset();
409        Ok(is_empty)
410    }
411
412    fn set_hashed_address(&mut self, hashed_address: B256) {
413        self.reset();
414        self.cursor.set_hashed_address(hashed_address);
415        let post_state_cursor =
416            HashedPostStateCursor::<C, U256>::get_storage_overlay(self.post_state, hashed_address);
417        self.post_state_cursor = post_state_cursor;
418        self.db_cursor_state = DbCursorState::NeedsPosition;
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425    use crate::hashed_cursor::mock::MockHashedCursor;
426    use parking_lot::Mutex;
427    use std::{collections::BTreeMap, sync::Arc};
428
429    fn key(byte: u8) -> B256 {
430        B256::repeat_byte(byte)
431    }
432
433    fn storage_post_state(storage_slots: Vec<(B256, U256)>) -> HashedPostStateSorted {
434        let storage_sorted = reth_trie_common::HashedStorageSorted { storage_slots };
435        let mut storages = alloy_primitives::map::B256Map::default();
436        storages.insert(B256::ZERO, storage_sorted);
437        HashedPostStateSorted::new(Vec::new(), storages)
438    }
439
440    #[test]
441    fn test_seek_overlay_exact_hit_does_not_touch_db_until_next() {
442        let db_nodes = vec![(key(0x02), U256::from(2)), (key(0x03), U256::from(3))];
443        let post_state_nodes = vec![(key(0x02), U256::from(42))];
444
445        let db_nodes_map: BTreeMap<B256, U256> = db_nodes.into_iter().collect();
446        let db_nodes_arc = Arc::new(db_nodes_map);
447        let visited_keys = Arc::new(Mutex::new(Vec::new()));
448        let mock_cursor = MockHashedCursor::new(db_nodes_arc, visited_keys.clone());
449
450        let post_state = storage_post_state(post_state_nodes);
451        let mut cursor = HashedPostStateCursor::new_storage(mock_cursor, &post_state, B256::ZERO);
452
453        let result = cursor.seek(key(0x02)).unwrap();
454        assert_eq!(result, Some((key(0x02), U256::from(42))));
455        assert!(visited_keys.lock().is_empty(), "exact overlay hit should not touch the DB cursor");
456
457        let result = cursor.next().unwrap();
458        assert_eq!(result, Some((key(0x03), U256::from(3))));
459        assert!(!visited_keys.lock().is_empty(), "next should lazily position the DB cursor");
460    }
461
462    #[test]
463    fn test_seek_overlay_exact_hit_repositions_stale_db_on_next() {
464        let db_nodes = vec![(key(0x01), U256::ONE), (key(0x03), U256::from(3))];
465        let post_state_nodes = vec![(key(0x02), U256::from(2))];
466
467        let db_nodes_map: BTreeMap<B256, U256> = db_nodes.into_iter().collect();
468        let db_nodes_arc = Arc::new(db_nodes_map);
469        let visited_keys = Arc::new(Mutex::new(Vec::new()));
470        let mock_cursor = MockHashedCursor::new(db_nodes_arc, visited_keys.clone());
471
472        let post_state = storage_post_state(post_state_nodes);
473        let mut cursor = HashedPostStateCursor::new_storage(mock_cursor, &post_state, B256::ZERO);
474
475        let result = cursor.seek(key(0x01)).unwrap();
476        assert_eq!(result, Some((key(0x01), U256::ONE)));
477        assert_eq!(visited_keys.lock().len(), 1);
478
479        let result = cursor.seek(key(0x02)).unwrap();
480        assert_eq!(result, Some((key(0x02), U256::from(2))));
481        assert_eq!(visited_keys.lock().len(), 1, "exact overlay hit should not seek the DB");
482
483        let result = cursor.next().unwrap();
484        assert_eq!(result, Some((key(0x03), U256::from(3))));
485    }
486
487    #[test]
488    fn test_seek_overlay_exact_deletion_still_seeks_db() {
489        let db_nodes = vec![(key(0x02), U256::from(2)), (key(0x03), U256::from(3))];
490        let post_state_nodes = vec![(key(0x02), U256::ZERO)];
491
492        let db_nodes_map: BTreeMap<B256, U256> = db_nodes.into_iter().collect();
493        let db_nodes_arc = Arc::new(db_nodes_map);
494        let visited_keys = Arc::new(Mutex::new(Vec::new()));
495        let mock_cursor = MockHashedCursor::new(db_nodes_arc, visited_keys.clone());
496
497        let post_state = storage_post_state(post_state_nodes);
498        let mut cursor = HashedPostStateCursor::new_storage(mock_cursor, &post_state, B256::ZERO);
499
500        let result = cursor.seek(key(0x02)).unwrap();
501        assert_eq!(result, Some((key(0x03), U256::from(3))));
502        assert!(!visited_keys.lock().is_empty(), "exact overlay deletion should consult the DB");
503    }
504
505    mod proptest_tests {
506        use super::*;
507        use itertools::Itertools;
508        use proptest::prelude::*;
509
510        /// Merge `db_nodes` with `post_state_nodes`, applying the post state overlay.
511        /// This properly handles deletions (ZERO values for U256, None for Account).
512        fn merge_with_overlay<V>(
513            db_nodes: Vec<(B256, V::NonZero)>,
514            post_state_nodes: Vec<(B256, V)>,
515        ) -> Vec<(B256, V::NonZero)>
516        where
517            V: HashedPostStateCursorValue,
518            V::NonZero: Clone,
519        {
520            db_nodes
521                .into_iter()
522                .merge_join_by(post_state_nodes, |db_entry, mem_entry| db_entry.0.cmp(&mem_entry.0))
523                .filter_map(|entry| match entry {
524                    // Only in db: keep it
525                    itertools::EitherOrBoth::Left((key, node)) => Some((key, node)),
526                    // Only in post state: keep if not a deletion
527                    itertools::EitherOrBoth::Right((key, wrapped)) => {
528                        wrapped.into_option().map(|val| (key, val))
529                    }
530                    // In both: post state takes precedence (keep if not a deletion)
531                    itertools::EitherOrBoth::Both(_, (key, wrapped)) => {
532                        wrapped.into_option().map(|val| (key, val))
533                    }
534                })
535                .collect()
536        }
537
538        /// Generate a strategy for U256 values
539        fn u256_strategy() -> impl Strategy<Value = U256> {
540            any::<u64>().prop_map(U256::from)
541        }
542
543        /// Generate a sorted vector of (B256, U256) entries
544        fn sorted_db_nodes_strategy() -> impl Strategy<Value = Vec<(B256, U256)>> {
545            prop::collection::vec((any::<u8>(), u256_strategy()), 0..20).prop_map(|entries| {
546                let mut result: Vec<(B256, U256)> = entries
547                    .into_iter()
548                    .map(|(byte, value)| (B256::repeat_byte(byte), value))
549                    .collect();
550                result.sort_by_key(|a| a.0);
551                result.dedup_by(|a, b| a.0 == b.0);
552                result
553            })
554        }
555
556        /// Generate a sorted vector of (B256, U256) entries (including deletions as ZERO)
557        fn sorted_post_state_nodes_strategy() -> impl Strategy<Value = Vec<(B256, U256)>> {
558            // Explicitly inject ZERO values to model post-state deletions.
559            prop::collection::vec((any::<u8>(), u256_strategy(), any::<bool>()), 0..20).prop_map(
560                |entries| {
561                    let mut result: Vec<(B256, U256)> = entries
562                        .into_iter()
563                        .map(|(byte, value, is_deletion)| {
564                            let effective_value = if is_deletion { U256::ZERO } else { value };
565                            (B256::repeat_byte(byte), effective_value)
566                        })
567                        .collect();
568                    result.sort_by_key(|a| a.0);
569                    result.dedup_by(|a, b| a.0 == b.0);
570                    result
571                },
572            )
573        }
574
575        proptest! {
576            #![proptest_config(ProptestConfig::with_cases(1000))]
577        /// Tests `HashedPostStateCursor` produces identical results to a pre-merged cursor
578        /// across 1000 random scenarios.
579        ///
580        /// For random DB entries and post-state changes, creates two cursors:
581        /// - Control: pre-merged data (expected behavior)
582        /// - Test: `HashedPostStateCursor` (lazy overlay)
583        ///
584        /// Executes random sequences of `next()` and `seek()` operations, asserting
585        /// both cursors return identical results.
586        #[test]
587        fn proptest_hashed_post_state_cursor(
588                db_nodes in sorted_db_nodes_strategy(),
589                post_state_nodes in sorted_post_state_nodes_strategy(),
590                op_choices in prop::collection::vec(any::<u8>(), 10..500),
591            ) {
592                reth_tracing::init_test_tracing();
593                use tracing::debug;
594
595                debug!("Starting proptest!");
596
597                // Create the expected results by merging the two sorted vectors,
598                // properly handling deletions (ZERO values in post_state_nodes)
599                let expected_combined = merge_with_overlay(db_nodes.clone(), post_state_nodes.clone());
600
601                // Collect all keys for operation generation
602                let all_keys: Vec<B256> = expected_combined.iter().map(|(k, _)| *k).collect();
603
604                // Create a control cursor using the combined result with a mock cursor
605                let control_db_map: BTreeMap<B256, U256> = expected_combined.into_iter().collect();
606                let control_db_arc = Arc::new(control_db_map);
607                let control_visited_keys = Arc::new(Mutex::new(Vec::new()));
608                let mut control_cursor = MockHashedCursor::new(control_db_arc, control_visited_keys);
609
610                // Create the HashedPostStateCursor being tested
611                let db_nodes_map: BTreeMap<B256, U256> = db_nodes.into_iter().collect();
612                let db_nodes_arc = Arc::new(db_nodes_map);
613                let visited_keys = Arc::new(Mutex::new(Vec::new()));
614                let mock_cursor = MockHashedCursor::new(db_nodes_arc, visited_keys);
615
616                // Create a HashedPostStateSorted with the storage data
617                let hashed_address = B256::ZERO;
618                let storage_sorted = reth_trie_common::HashedStorageSorted {
619                    storage_slots: post_state_nodes,
620                };
621                let mut storages = alloy_primitives::map::B256Map::default();
622                storages.insert(hashed_address, storage_sorted);
623                let post_state = HashedPostStateSorted::new(Vec::new(), storages);
624
625                let mut test_cursor = HashedPostStateCursor::new_storage(mock_cursor, &post_state, hashed_address);
626
627                // Test: seek to the beginning first
628                let control_first = control_cursor.seek(B256::ZERO).unwrap();
629                let test_first = test_cursor.seek(B256::ZERO).unwrap();
630                debug!(
631                    control=?control_first.as_ref().map(|(k, _)| k),
632                    test=?test_first.as_ref().map(|(k, _)| k),
633                    "Initial seek returned",
634                );
635                assert_eq!(control_first, test_first, "Initial seek mismatch");
636
637                // If both cursors returned None, nothing to test
638                if control_first.is_none() && test_first.is_none() {
639                    return Ok(());
640                }
641
642                // Track the last key returned from the cursor
643                let mut last_returned_key = control_first.as_ref().map(|(k, _)| *k);
644
645                // Execute a sequence of random operations
646                for choice in op_choices {
647                    let op_type = choice % 2; // Only 2 operation types: next and seek
648
649                    match op_type {
650                        0 => {
651                            // Next operation
652                            let control_result = control_cursor.next().unwrap();
653                            let test_result = test_cursor.next().unwrap();
654                            debug!(
655                                control=?control_result.as_ref().map(|(k, _)| k),
656                                test=?test_result.as_ref().map(|(k, _)| k),
657                                "Next returned",
658                            );
659                            assert_eq!(control_result, test_result, "Next operation mismatch");
660
661                            last_returned_key = control_result.as_ref().map(|(k, _)| *k);
662
663                            // Stop if both cursors are exhausted
664                            if control_result.is_none() && test_result.is_none() {
665                                break;
666                            }
667                        }
668                        _ => {
669                            // Seek operation - choose a key >= last_returned_key
670                            if all_keys.is_empty() {
671                                continue;
672                            }
673
674                            let valid_keys: Vec<_> = all_keys
675                                .iter()
676                                .filter(|k| last_returned_key.is_none_or(|last| **k >= last))
677                                .collect();
678
679                            if valid_keys.is_empty() {
680                                continue;
681                            }
682
683                            let key = *valid_keys[(choice as usize / 2) % valid_keys.len()];
684
685                            let control_result = control_cursor.seek(key).unwrap();
686                            let test_result = test_cursor.seek(key).unwrap();
687                            debug!(
688                                control=?control_result.as_ref().map(|(k, _)| k),
689                                test=?test_result.as_ref().map(|(k, _)| k),
690                                ?key,
691                                "Seek returned",
692                            );
693                            assert_eq!(control_result, test_result, "Seek operation mismatch for key {:?}", key);
694
695                            last_returned_key = control_result.as_ref().map(|(k, _)| *k);
696
697                            // Stop if both cursors are exhausted
698                            if control_result.is_none() && test_result.is_none() {
699                                break;
700                            }
701                        }
702                    }
703                }
704            }
705        }
706    }
707}