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}