Skip to main content

reth_trie_sparse/arena/
cursor.rs

1use super::{
2    branch_child_idx::{BranchChildIdx, BranchChildIter},
3    ArenaSparseNode, ArenaSparseNodeState, BranchChild, Index, NodeArena,
4};
5use alloc::vec::Vec;
6use reth_trie_common::Nibbles;
7use tracing::{instrument, trace};
8
9const TRACE_TARGET: &str = "trie::arena::cursor";
10
11/// An entry on the cursor's traversal stack, tracking an ancestor node during trie walks.
12///
13/// The node's absolute path is not stored: it is the first `path_len` nibbles of the cursor's
14/// [`ArenaCursor::path`], which every entry on the stack is a prefix of.
15#[derive(Debug, Clone)]
16pub(super) struct ArenaCursorStackEntry {
17    /// The arena index of this node.
18    pub(super) index: Index,
19    /// The nibble length of the absolute path of this node (not including its `short_key`).
20    /// A [`Nibbles`] holds at most 64 nibbles, so this always fits in a `u8`.
21    path_len: u8,
22    /// The dense index at which to resume child iteration in [`ArenaCursor::next`].
23    /// Only meaningful when this entry's node is a branch. Ranges over `0..=16`, where 16 means
24    /// the last child of a full branch has been visited.
25    next_dense_idx: u8,
26}
27
28/// Result of [`ArenaCursor::seek`] describing the state at the deepest ancestor node.
29#[derive(Debug)]
30pub(super) enum SeekResult {
31    /// The stack head is an empty root node.
32    EmptyRoot,
33    /// The stack head is a leaf whose full path matches the target exactly.
34    RevealedLeaf,
35    /// The next child along the path is blinded (unrevealed).
36    Blinded,
37    /// The target path diverges from the stack head's `short_key` (branch or leaf).
38    Diverged,
39    /// The target nibble has no child in the branch's `state_mask`.
40    NoChild { child_nibble: u8 },
41    /// The target nibble has a revealed subtrie child (now pushed onto the stack).
42    RevealedSubtrie,
43}
44
45/// Result of [`ArenaCursor::next`] describing what the cursor did.
46#[derive(Debug)]
47pub(super) enum NextResult {
48    /// The head is a non-branch node (subtrie, taken-subtrie, leaf, etc.).
49    /// The caller should process it; the next call to [`ArenaCursor::next`] will pop it.
50    NonBranch,
51    /// The head branch has no more qualifying children. It is still on the stack;
52    /// the caller should process it. The next call to [`ArenaCursor::next`] will pop it.
53    Branch,
54    /// The stack is empty — the traversal is complete.
55    Done,
56}
57
58/// A cursor for depth-first traversal of an arena-based sparse trie.
59///
60/// Wraps a stack of [`ArenaCursorStackEntry`]s and provides methods for navigating
61/// the trie: pushing children, popping with dirty-state propagation, seeking
62/// to ancestors, and computing child paths.
63///
64/// The cursor borrows the arena on each method call rather than holding a
65/// reference, so the caller retains full ownership of the arena between calls.
66#[derive(Debug, Default, Clone)]
67pub(super) struct ArenaCursor {
68    stack: Vec<ArenaCursorStackEntry>,
69    /// A path that every stack entry's path is a prefix of: the entry with `path_len` nibbles
70    /// has the absolute path `path[..path_len]`.
71    ///
72    /// It holds the last seeked target, or the path of the deepest node a [`Self::next`] walk
73    /// descended to, and may therefore be longer than the head's own path. Popping never
74    /// shortens it, which is what keeps the ancestor test in [`Self::seek`] to a single
75    /// [`Nibbles::common_prefix_length`].
76    path: Nibbles,
77    /// Whether the head entry should be popped at the start of the next [`Self::next`] call.
78    /// Set when `next` returns [`NextResult::NonBranch`] or [`NextResult::Branch`].
79    needs_pop: bool,
80}
81
82impl ArenaCursor {
83    /// Returns the entry at the top of the stack, or `None` if empty.
84    pub(super) fn head(&self) -> Option<&ArenaCursorStackEntry> {
85        self.stack.last()
86    }
87
88    /// Returns the entry below the top of the stack (the parent of the head), or `None`.
89    pub(super) fn parent(&self) -> Option<&ArenaCursorStackEntry> {
90        let len = self.stack.len();
91        (len >= 2).then(|| &self.stack[len - 2])
92    }
93
94    /// Returns the depth of the head node (0 for the root).
95    ///
96    /// # Panics
97    ///
98    /// Panics if the stack is empty.
99    pub(super) const fn depth(&self) -> usize {
100        self.stack.len() - 1
101    }
102
103    /// Returns the absolute path of the node at the top of the stack.
104    pub(super) fn head_path(&self) -> Nibbles {
105        self.entry_path(self.head_entry())
106    }
107
108    /// Returns the nibble length of the absolute path of the node at the top of the stack.
109    /// Equivalent to `head_path().len()` but avoids constructing the path.
110    pub(super) fn head_path_len(&self) -> usize {
111        self.head_entry().path_len as usize
112    }
113
114    /// Returns the last nibble of the head node's absolute path, or `None` if that path is
115    /// empty.
116    pub(super) fn head_last_nibble(&self) -> Option<u8> {
117        self.head_path_len().checked_sub(1).map(|i| self.path.get_unchecked(i))
118    }
119
120    /// Replaces the root entry on the stack with a new one.
121    ///
122    /// The stack must contain exactly the root (depth 0) or be empty (freshly constructed).
123    #[instrument(level = "trace", target = TRACE_TARGET, skip(self, arena))]
124    pub(super) fn reset(&mut self, arena: &NodeArena, idx: Index, path: Nibbles) {
125        debug_assert!(
126            self.stack.len() <= 1 && !self.needs_pop,
127            "cursor must be drained before reset; stack has {} entries, needs_pop={}",
128            self.stack.len(),
129            self.needs_pop,
130        );
131        self.stack.clear();
132        self.needs_pop = false;
133        self.path = path;
134        self.push(arena, idx, path.len());
135    }
136
137    /// Pushes an entry onto the stack for the node at the given index, whose absolute path is
138    /// the first `path_len` nibbles of [`Self::path`].
139    fn push(&mut self, arena: &NodeArena, idx: Index, path_len: usize) {
140        debug_assert!(arena.contains_key(idx), "push called with invalid arena index");
141        debug_assert!(
142            path_len <= self.path.len(),
143            "pushed path length {path_len} exceeds cursor path {:?}",
144            self.path,
145        );
146        self.stack.push(ArenaCursorStackEntry {
147            index: idx,
148            path_len: path_len as u8,
149            next_dense_idx: 0,
150        });
151        trace!(
152            target: TRACE_TARGET,
153            ?idx,
154            path = ?self.path.slice_unchecked(0, path_len),
155            "Pushed stack entry",
156        );
157    }
158
159    /// Pops the top entry from the stack and propagates dirty state to the parent.
160    /// Returns the popped entry.
161    ///
162    /// The node must still be in the arena. Pop its entry before removing it, since a new
163    /// insertion can immediately reuse its index.
164    #[instrument(level = "trace", target = TRACE_TARGET, skip(self, arena))]
165    pub(super) fn pop(&mut self, arena: &mut NodeArena) -> ArenaCursorStackEntry {
166        let entry = self.stack.pop().expect("pop can't be called on empty stack");
167        self.needs_pop = false;
168        trace!(
169            target: TRACE_TARGET,
170            idx = ?entry.index,
171            path = ?self.entry_path(&entry),
172            "Popped stack entry",
173        );
174        let node = &arena[entry.index];
175
176        #[cfg(debug_assertions)]
177        if let ArenaSparseNode::Subtrie(s) = node {
178            let entry_path = self.path.slice_unchecked(0, entry.path_len as usize);
179            debug_assert_eq!(
180                s.path, entry_path,
181                "subtrie cached path {:?} does not match stack entry path {:?}",
182                s.path, entry_path,
183            );
184        }
185
186        if let Some(parent) = self.stack.last() {
187            let child_is_dirty = match node {
188                ArenaSparseNode::Branch(b) => matches!(b.state, ArenaSparseNodeState::Dirty),
189                ArenaSparseNode::Leaf { state, .. } => matches!(state, ArenaSparseNodeState::Dirty),
190                ArenaSparseNode::Subtrie(s) => {
191                    let root = &s.arena[s.root];
192                    matches!(root.state_ref(), Some(ArenaSparseNodeState::Dirty))
193                }
194                _ => false,
195            };
196            if child_is_dirty {
197                *arena[parent.index].state_mut() = ArenaSparseNodeState::Dirty;
198            }
199        }
200
201        entry
202    }
203
204    /// Drains the stack down to the root, propagating dirty state from each popped entry
205    /// to its parent. The root entry remains on the stack (there is no parent to propagate to).
206    #[instrument(level = "trace", target = TRACE_TARGET, skip_all)]
207    pub(super) fn drain(&mut self, arena: &mut NodeArena) {
208        trace!(target: TRACE_TARGET, "Draining stack");
209        self.needs_pop = false;
210        while self.stack.len() > 1 {
211            self.pop(arena);
212        }
213    }
214
215    /// Returns the logical path of the branch at the top of the stack.
216    /// The logical path is `head_path() + branch.short_key`.
217    pub(super) fn head_logical_branch_path(&self, arena: &NodeArena) -> Nibbles {
218        self.logical_branch_path(arena, self.head_entry())
219    }
220
221    /// Returns the length of the logical path of the branch at the top of the stack.
222    /// Equivalent to `head_logical_branch_path(arena).len()` but avoids constructing the path.
223    pub(super) fn head_logical_branch_path_len(&self, arena: &NodeArena) -> usize {
224        let head = self.head_entry();
225        head.path_len as usize + arena[head.index].branch_ref().short_key.len()
226    }
227
228    /// Returns the logical path of the parent branch entry (second from top of the stack).
229    /// Panics if the stack has fewer than 2 entries.
230    pub(super) fn parent_logical_branch_path(&self, arena: &NodeArena) -> Nibbles {
231        self.logical_branch_path(arena, self.parent().expect("cursor must have a parent"))
232    }
233
234    /// Replaces the arena index stored in the head entry with `new_idx`, and updates the
235    /// parent branch's children array to point to the new index. If the head is the root
236    /// (stack has one entry), `root` is updated instead.
237    pub(super) fn replace_head_index(
238        &mut self,
239        arena: &mut NodeArena,
240        root: &mut Index,
241        new_idx: Index,
242    ) {
243        let child_nibble = self.head_last_nibble();
244        let head = self.stack.last_mut().expect("cursor must have head");
245        let old_idx = head.index;
246        head.index = new_idx;
247
248        let Some(parent) = self.parent() else {
249            *root = new_idx;
250            return;
251        };
252
253        let child_nibble =
254            child_nibble.expect("if cursor has a parent then the head path can't be empty");
255
256        let parent_branch = arena[parent.index].branch_mut();
257        let child_idx = BranchChildIdx::new(parent_branch.state_mask, child_nibble)
258            .expect("child nibble not found in parent state_mask");
259
260        debug_assert_eq!(
261            parent_branch.children[child_idx].revealed_index(),
262            Some(old_idx),
263            "parent child at nibble {child_nibble} does not match old_idx",
264        );
265
266        parent_branch.children[child_idx] = BranchChild::revealed(new_idx);
267    }
268
269    /// Advances the DFS traversal to the next actionable node.
270    ///
271    /// If a previous call returned [`NextResult::NonBranch`] or [`NextResult::Branch`],
272    /// the head entry is automatically popped (with dirty-state propagation) before
273    /// descending further, unless the caller already popped it. Callers removing the head
274    /// node must call [`Self::pop`] before the removal.
275    ///
276    /// Returns [`NextResult::NonBranch`] when the head is a non-branch node the caller
277    /// should process, or [`NextResult::Branch`] when a branch has exhausted its
278    /// qualifying children. In both cases the node is still on the stack so the caller
279    /// can read it via [`Self::head`].
280    ///
281    /// Returns [`NextResult::Done`] when the stack is empty (traversal complete).
282    #[instrument(level = "trace", target = TRACE_TARGET, skip_all, ret)]
283    pub(super) fn next(
284        &mut self,
285        arena: &mut NodeArena,
286        should_descend: impl Fn(usize, &ArenaSparseNode) -> bool,
287    ) -> NextResult {
288        if self.needs_pop {
289            self.pop(arena);
290        }
291
292        loop {
293            let Some(head) = self.stack.last_mut() else {
294                return NextResult::Done;
295            };
296            let head_idx = head.index;
297            let head_path_len = head.path_len as usize;
298
299            let ArenaSparseNode::Branch(branch) = &arena[head_idx] else {
300                self.needs_pop = true;
301                return NextResult::NonBranch;
302            };
303
304            let state_mask = branch.state_mask;
305            let short_key = branch.short_key;
306            let start = head.next_dense_idx as usize;
307            let child_depth = self.stack.len();
308
309            let mut descended = false;
310            for (branch_child_idx, nibble) in BranchChildIter::new(state_mask) {
311                if branch_child_idx.get() < start {
312                    continue;
313                }
314
315                let Some(child_idx) =
316                    arena[head_idx].branch_ref().children[branch_child_idx].revealed_index()
317                else {
318                    continue;
319                };
320
321                if should_descend(child_depth, &arena[child_idx]) {
322                    // Record where to resume iteration when we return to this entry.
323                    self.stack.last_mut().expect("head exists").next_dense_idx =
324                        branch_child_idx.get() as u8 + 1;
325                    self.path.truncate(head_path_len);
326                    self.path.extend(&short_key);
327                    self.path.push_unchecked(nibble);
328                    self.push(arena, child_idx, self.path.len());
329                    descended = true;
330                    break;
331                }
332            }
333
334            if !descended {
335                self.needs_pop = true;
336                return NextResult::Branch;
337            }
338        }
339    }
340
341    /// Pops the stack until the head is an ancestor of `full_path`, then descends from that head
342    /// toward `full_path`, pushing revealed branch (and leaf) children onto the stack until the
343    /// deepest ancestor is reached.
344    ///
345    /// Returns a [`SeekResult`] describing the state at the stack head.
346    #[instrument(level = "trace", target = TRACE_TARGET, skip(self, arena), ret)]
347    pub(super) fn seek(&mut self, arena: &mut NodeArena, full_path: &Nibbles) -> SeekResult {
348        // Every entry's path is a prefix of `self.path`, so an entry is an ancestor of
349        // `full_path` exactly when its length fits within the common prefix of the previous
350        // target and this one.
351        let common = self.path.common_prefix_length(full_path);
352        while self.stack.len() > 1 && self.head_entry().path_len as usize > common {
353            self.pop(arena);
354        }
355
356        if self.head_entry().path_len as usize > common {
357            // The target is not below the walk's root. Callers only seek within the root's
358            // prefix, so this cannot happen for a revealed root; leaving `self.path` alone
359            // keeps the entry paths derivable.
360            debug_assert!(
361                false,
362                "seek target {full_path:?} is not below the walk's root {:?}",
363                self.head_path(),
364            );
365            return SeekResult::Diverged;
366        }
367        self.path = *full_path;
368
369        loop {
370            let head = self.head_entry();
371            let head_idx = head.index;
372            let head_path_len = head.path_len as usize;
373
374            let head_branch = match &arena[head_idx] {
375                ArenaSparseNode::EmptyRoot { .. } => {
376                    return SeekResult::EmptyRoot;
377                }
378                ArenaSparseNode::Leaf { key, .. } => {
379                    return if full_path.slice_unchecked(head_path_len, full_path.len()) == *key {
380                        SeekResult::RevealedLeaf
381                    } else {
382                        SeekResult::Diverged
383                    };
384                }
385                ArenaSparseNode::Branch(b) => b,
386                ArenaSparseNode::Subtrie(_) => {
387                    return SeekResult::RevealedSubtrie;
388                }
389                _ => unreachable!("unexpected node type on stack: {:?}", arena[head_idx]),
390            };
391
392            let short_key = &head_branch.short_key;
393            let logical_len = head_path_len + short_key.len();
394
395            // If full_path doesn't extend past the branch's logical path, the target is at or
396            // within the branch's short_key — treat as diverged. Most branches carry no
397            // short_key, in which case there is nothing left to compare.
398            if full_path.len() <= logical_len ||
399                (!short_key.is_empty() &&
400                    full_path.slice_unchecked(head_path_len, logical_len) != *short_key)
401            {
402                return SeekResult::Diverged;
403            }
404
405            let child_nibble = full_path.get_unchecked(logical_len);
406            let Some(branch_child_idx) = BranchChildIdx::new(head_branch.state_mask, child_nibble)
407            else {
408                return SeekResult::NoChild { child_nibble };
409            };
410
411            let Some(child_idx) = head_branch.children[branch_child_idx].revealed_index() else {
412                return SeekResult::Blinded;
413            };
414            self.push(arena, child_idx, logical_len + 1);
415        }
416    }
417
418    /// Returns the entry at the top of the stack, which must be non-empty.
419    fn head_entry(&self) -> &ArenaCursorStackEntry {
420        self.stack.last().expect("cursor is non-empty")
421    }
422
423    /// Returns the absolute path of a stack entry.
424    fn entry_path(&self, entry: &ArenaCursorStackEntry) -> Nibbles {
425        self.path.slice_unchecked(0, entry.path_len as usize)
426    }
427
428    /// Returns the logical path of a branch stack entry: `entry path + branch.short_key`.
429    fn logical_branch_path(&self, arena: &NodeArena, entry: &ArenaCursorStackEntry) -> Nibbles {
430        let mut path = self.entry_path(entry);
431        path.extend(&arena[entry.index].branch_ref().short_key);
432        path
433    }
434}