Skip to main content

reth_trie_sparse/arena/
node_arena.rs

1use super::ArenaSparseNode;
2use alloc::vec::Vec;
3use core::{
4    fmt,
5    ops::{Index as IndexOps, IndexMut},
6};
7use reth_trie_common::RlpNode;
8
9/// Bits per word of a [`BlindedMarks`] bitmap.
10const WORD_BITS: usize = u64::BITS as usize;
11
12/// Arena of trie nodes addressed by an [`Index`].
13///
14/// Nodes live in a flat vector; [`NodeArena::remove`] leaves an [`ArenaSparseNode::Free`] tombstone
15/// behind and pushes the slot onto a LIFO free list. Unlike a slotmap there are no generation
16/// counters, so an index handed out again refers to the new occupant: an index must not be reused
17/// across a removal unless the caller knows nothing was inserted in between.
18#[derive(Debug, Clone, Default)]
19pub(super) struct NodeArena {
20    nodes: Vec<ArenaSparseNode>,
21    free: Vec<Index>,
22    blinded: Vec<RlpNode>,
23    blinded_free: Vec<u32>,
24}
25
26impl NodeArena {
27    /// Creates an empty arena.
28    pub(super) const fn new() -> Self {
29        Self { nodes: Vec::new(), free: Vec::new(), blinded: Vec::new(), blinded_free: Vec::new() }
30    }
31
32    /// Creates an empty arena with room for `capacity` nodes.
33    pub(super) fn with_capacity(capacity: usize) -> Self {
34        Self { nodes: Vec::with_capacity(capacity), ..Self::new() }
35    }
36
37    /// Inserts a node, reusing a free slot when one is available, and returns its index.
38    pub(super) fn insert(&mut self, node: ArenaSparseNode) -> Index {
39        if let Some(idx) = self.free.pop() {
40            self.nodes[idx.get()] = node;
41            return idx;
42        }
43        let idx = Index::new(self.nodes.len());
44        self.nodes.push(node);
45        idx
46    }
47
48    /// Removes the node at `idx` without recycling its slot.
49    ///
50    /// For an arena that is being emptied into a fresh one — pruning and compaction both do that
51    /// and then drop the source — maintaining the free list is wasted work. [`Self::len`] is no
52    /// longer accurate afterwards.
53    ///
54    /// # Panics
55    ///
56    /// Panics if the slot is out of bounds or free.
57    pub(super) fn drain_node(&mut self, idx: Index) -> ArenaSparseNode {
58        let node = core::mem::replace(&mut self.nodes[idx.get()], ArenaSparseNode::Free);
59        assert!(!matches!(node, ArenaSparseNode::Free), "drained a free arena slot");
60        node
61    }
62
63    /// Removes the node at `idx`, returning it if the slot was occupied.
64    pub(super) fn remove(&mut self, idx: Index) -> Option<ArenaSparseNode> {
65        let slot = self.nodes.get_mut(idx.get())?;
66        if matches!(slot, ArenaSparseNode::Free) {
67            return None;
68        }
69        let node = core::mem::replace(slot, ArenaSparseNode::Free);
70        self.free.push(idx);
71        Some(node)
72    }
73
74    /// Returns the node at `idx`, or `None` if the slot is out of bounds or free.
75    pub(super) fn get(&self, idx: Index) -> Option<&ArenaSparseNode> {
76        match self.nodes.get(idx.get()) {
77            Some(ArenaSparseNode::Free) | None => None,
78            Some(node) => Some(node),
79        }
80    }
81
82    /// Returns `true` if `idx` refers to an occupied slot, or `false` if it is out of bounds or
83    /// free.
84    pub(super) fn contains_key(&self, idx: Index) -> bool {
85        self.get(idx).is_some()
86    }
87
88    /// Returns the number of occupied slots.
89    pub(super) const fn len(&self) -> usize {
90        self.nodes.len() - self.free.len()
91    }
92
93    /// Iterates over the occupied slots.
94    pub(super) fn iter(&self) -> impl Iterator<Item = (Index, &ArenaSparseNode)> + '_ {
95        self.nodes
96            .iter()
97            .enumerate()
98            .filter(|(_, node)| !matches!(node, ArenaSparseNode::Free))
99            .map(|(idx, node)| (Index(idx as u32), node))
100    }
101
102    /// Iterates mutably over the occupied slots.
103    pub(super) fn iter_mut(&mut self) -> impl Iterator<Item = (Index, &mut ArenaSparseNode)> + '_ {
104        self.nodes
105            .iter_mut()
106            .enumerate()
107            .filter(|(_, node)| !matches!(node, ArenaSparseNode::Free))
108            .map(|(idx, node)| (Index(idx as u32), node))
109    }
110
111    /// Stores the RLP of an unrevealed child and returns a [`BranchChild`] referencing it.
112    pub(super) fn insert_blinded(&mut self, rlp: RlpNode) -> BranchChild {
113        let slot = if let Some(slot) = self.blinded_free.pop() {
114            self.blinded[slot as usize] = rlp;
115            slot
116        } else {
117            let slot = self.blinded.len();
118            assert!(slot < BranchChild::BLINDED as usize, "blinded slot overflows the tag bit");
119            self.blinded.push(rlp);
120            slot as u32
121        };
122        BranchChild::blinded(slot)
123    }
124
125    /// Returns the RLP of a blinded child.
126    ///
127    /// # Panics
128    ///
129    /// Panics if `child` is revealed.
130    pub(super) fn blinded(&self, child: BranchChild) -> &RlpNode {
131        &self.blinded[child.blinded_slot().expect("child is revealed") as usize]
132    }
133
134    /// Takes the RLP of a blinded child, releasing its slot.
135    ///
136    /// # Panics
137    ///
138    /// Panics if `child` is revealed.
139    pub(super) fn take_blinded(&mut self, child: BranchChild) -> RlpNode {
140        let slot = child.blinded_slot().expect("child is revealed");
141        self.blinded_free.push(slot);
142        core::mem::take(&mut self.blinded[slot as usize])
143    }
144
145    /// Releases the node vector's unused capacity.
146    pub(super) fn shrink_nodes_to_fit(&mut self) {
147        self.nodes.shrink_to_fit();
148    }
149
150    /// Takes over `src`'s side table of blinded children, so that blinded [`BranchChild`]s copied
151    /// out of `src` stay valid without their RLP being moved one slot at a time.
152    ///
153    /// The copy must record every child it carries over in the returned marks and finish with
154    /// [`Self::sweep_blinded`], which reclaims unreferenced slots and may rewrite the blinded
155    /// children stored in this arena. Blinded handles must not be retained across the sweep.
156    pub(super) fn adopt_blinded(&mut self, src: &mut Self) -> BlindedMarks {
157        debug_assert!(self.blinded.is_empty(), "adopting into a non-empty side table");
158        self.blinded = core::mem::take(&mut src.blinded);
159        self.blinded_free = core::mem::take(&mut src.blinded_free);
160        BlindedMarks { words: alloc::vec![0; self.blinded.len().div_ceil(WORD_BITS)] }
161    }
162
163    /// Reclaims unmarked slots in the side table adopted by [`Self::adopt_blinded`].
164    /// Sparse tables are compacted and their branch child references rewritten, so a live tail
165    /// slot cannot retain an arbitrarily large table. Other tables keep their slot indices.
166    pub(super) fn sweep_blinded(&mut self, marks: BlindedMarks) {
167        let words = marks.words;
168        let end = words
169            .iter()
170            .rposition(|word| *word != 0)
171            .map_or(0, |idx| idx * WORD_BITS + WORD_BITS - words[idx].leading_zeros() as usize);
172        debug_assert!(end <= self.blinded.len(), "marked slot outside the side table");
173        self.blinded.truncate(end);
174        self.blinded_free.clear();
175
176        let words = &words[..end.div_ceil(WORD_BITS)];
177        let live_count = words.iter().map(|word| word.count_ones() as usize).sum::<usize>();
178        if end > 0 && live_count <= end / 4 {
179            // A slot's new index is the number of marked slots before it. Store one prefix
180            // count per bitmap word instead of allocating a mapping for every old slot.
181            let mut offsets = Vec::with_capacity(words.len());
182            let mut next = 0;
183            for (idx, &word) in words.iter().enumerate() {
184                offsets.push(next as u32);
185                let mut marked = word;
186                while marked != 0 {
187                    let slot = idx * WORD_BITS + marked.trailing_zeros() as usize;
188                    self.blinded.swap(next, slot);
189                    next += 1;
190                    marked &= marked - 1;
191                }
192            }
193            self.blinded.truncate(next);
194            for (_, node) in self.iter_mut() {
195                if let ArenaSparseNode::Branch(branch) = node {
196                    for child in &mut branch.children {
197                        if let Some(slot) = child.blinded_slot() {
198                            let word = slot as usize / WORD_BITS;
199                            let bit = slot as usize % WORD_BITS;
200                            debug_assert_ne!(words[word] & (1 << bit), 0, "unmarked child");
201                            let preceding = words[word] & ((1 << bit) - 1);
202                            *child = BranchChild::blinded(offsets[word] + preceding.count_ones());
203                        }
204                    }
205                }
206            }
207            shrink_excess_capacity(&mut self.blinded);
208            shrink_excess_capacity(&mut self.blinded_free);
209            return;
210        }
211
212        // Push high to low so that `insert_blinded` hands the lowest slot out first and the tail
213        // of the table keeps draining.
214        let word_count = end.div_ceil(WORD_BITS);
215        for (idx, word) in words[..word_count].iter().enumerate().rev() {
216            // The last word covers only the slots below `end`.
217            let covered = if idx + 1 == word_count && end % WORD_BITS != 0 {
218                (1 << (end % WORD_BITS)) - 1
219            } else {
220                u64::MAX
221            };
222            let mut unmarked = !word & covered;
223            while unmarked != 0 {
224                let bit = u64::BITS - 1 - unmarked.leading_zeros();
225                unmarked ^= 1 << bit;
226                self.blinded_free.push(idx as u32 * u64::BITS + bit);
227            }
228        }
229
230        shrink_excess_capacity(&mut self.blinded);
231        shrink_excess_capacity(&mut self.blinded_free);
232    }
233}
234
235impl IndexOps<Index> for NodeArena {
236    type Output = ArenaSparseNode;
237
238    #[inline]
239    fn index(&self, idx: Index) -> &Self::Output {
240        let node = &self.nodes[idx.get()];
241        assert!(!matches!(node, ArenaSparseNode::Free), "indexed a free arena slot");
242        node
243    }
244}
245
246impl IndexMut<Index> for NodeArena {
247    #[inline]
248    fn index_mut(&mut self, idx: Index) -> &mut Self::Output {
249        let node = &mut self.nodes[idx.get()];
250        assert!(!matches!(node, ArenaSparseNode::Free), "indexed a free arena slot");
251        node
252    }
253}
254
255/// A reference to a node inside a [`NodeArena`].
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
257pub(super) struct Index(u32);
258
259impl Index {
260    /// Creates an index that fits below the blinded-child tag bit.
261    ///
262    /// # Panics
263    ///
264    /// Panics if `index` is at least 2^31.
265    pub(super) const fn new(index: usize) -> Self {
266        assert!(index < BranchChild::BLINDED as usize, "arena index overflows the blinded tag bit");
267        Self(index as u32)
268    }
269
270    /// Returns the index as a `usize`, suitable for indexing into the arena's backing vector.
271    pub(super) const fn get(self) -> usize {
272        self.0 as usize
273    }
274}
275
276/// The slots of a side table of blinded children that a copy into a fresh arena carried over.
277///
278/// See [`NodeArena::adopt_blinded`].
279#[must_use = "adopted blinded children must be marked and swept"]
280pub(super) struct BlindedMarks {
281    words: Vec<u64>,
282}
283
284impl BlindedMarks {
285    /// Records that `child`'s slot is still referenced.
286    ///
287    /// # Panics
288    ///
289    /// Panics if `child` is revealed.
290    pub(super) fn mark(&mut self, child: BranchChild) {
291        let slot = child.blinded_slot().expect("child is revealed") as usize;
292        let idx = slot / WORD_BITS;
293        if idx >= self.words.len() {
294            self.words.resize(idx + 1, 0);
295        }
296        self.words[idx] |= 1 << (slot % WORD_BITS);
297    }
298}
299
300/// A reference from a branch node to one of its children.
301///
302/// Packs the two cases into a single `u32`: a revealed child holds an [`Index`] into the arena's
303/// nodes, a blinded child a slot in the arena's side table of unrevealed RLP. The top bit
304/// discriminates, so both fit in four bytes and a branch's children stay dense and small.
305#[derive(Clone, Copy, PartialEq, Eq)]
306pub(super) struct BranchChild(u32);
307
308impl BranchChild {
309    /// Tag bit marking a child as blinded.
310    const BLINDED: u32 = 1 << 31;
311
312    /// Returns a child referencing the revealed node at `idx`.
313    ///
314    /// # Panics
315    ///
316    /// Panics if `idx` is at least 2^31.
317    pub(super) const fn revealed(idx: Index) -> Self {
318        assert!(idx.0 & Self::BLINDED == 0, "arena index overflows the blinded tag bit");
319        Self(idx.0)
320    }
321
322    /// Returns a child referencing the blinded RLP at `slot`.
323    ///
324    /// # Panics
325    ///
326    /// Panics if `slot` is at least 2^31.
327    const fn blinded(slot: u32) -> Self {
328        assert!(slot & Self::BLINDED == 0, "blinded slot overflows the tag bit");
329        Self(slot | Self::BLINDED)
330    }
331
332    /// Returns `true` if this child reference is blinded (not yet revealed in the arena).
333    pub(super) const fn is_blinded(self) -> bool {
334        self.0 & Self::BLINDED != 0
335    }
336
337    /// Returns the arena index of a revealed child, or `None` if it is blinded.
338    pub(super) const fn revealed_index(self) -> Option<Index> {
339        if self.is_blinded() {
340            None
341        } else {
342            Some(Index(self.0))
343        }
344    }
345
346    /// Returns the side-table slot of a blinded child, or `None` if it is revealed.
347    const fn blinded_slot(self) -> Option<u32> {
348        if self.is_blinded() {
349            Some(self.0 & !Self::BLINDED)
350        } else {
351            None
352        }
353    }
354}
355
356impl fmt::Debug for BranchChild {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        match self.blinded_slot() {
359            Some(slot) => write!(f, "Blinded({slot})"),
360            None => write!(f, "Revealed({})", self.0),
361        }
362    }
363}
364
365/// Reclaims capacity after a large contraction while leaving room for subsequent growth.
366fn shrink_excess_capacity<T>(vec: &mut Vec<T>) {
367    // Shrink at quarter-full to avoid reallocating on small prunes. Empty vectors release all
368    // capacity; non-empty vectors retain room for twice their current length.
369    if vec.len() <= vec.capacity() / 4 {
370        vec.shrink_to(vec.len() * 2);
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use crate::arena::{ArenaSparseNodeBranch, ArenaSparseNodeState};
378    use alloy_primitives::B256;
379
380    #[test]
381    fn removed_slots_are_recycled() {
382        let mut arena = NodeArena::new();
383        let first = arena.insert(ArenaSparseNode::TakenSubtrie);
384        let second = arena.insert(ArenaSparseNode::TakenSubtrie);
385        assert_eq!(arena.len(), 2);
386
387        assert!(arena.remove(first).is_some());
388        assert!(arena.get(first).is_none());
389        assert!(!arena.contains_key(first));
390        assert!(arena.remove(first).is_none());
391        assert_eq!(arena.len(), 1);
392
393        let third = arena.insert(ArenaSparseNode::TakenSubtrie);
394        assert_eq!(third, first, "the freed slot is handed out again");
395        assert_ne!(third, second);
396        assert_eq!(arena.len(), 2);
397        assert_eq!(arena.iter().count(), 2);
398    }
399
400    #[test]
401    fn contains_key_excludes_free_slots() {
402        let mut arena = NodeArena::new();
403        let removed = arena.insert(ArenaSparseNode::TakenSubtrie);
404        let drained = arena.insert(ArenaSparseNode::TakenSubtrie);
405        assert!(arena.contains_key(removed));
406        assert!(arena.contains_key(drained));
407        assert!(!arena.contains_key(Index::new(2)));
408
409        arena.remove(removed);
410        arena.drain_node(drained);
411        assert!(!arena.contains_key(removed));
412        assert!(!arena.contains_key(drained));
413    }
414
415    #[test]
416    #[should_panic(expected = "indexed a free arena slot")]
417    fn indexing_a_free_slot_panics() {
418        let mut arena = NodeArena::new();
419        let idx = arena.insert(ArenaSparseNode::TakenSubtrie);
420        arena.remove(idx);
421        let _ = &arena[idx];
422    }
423
424    #[test]
425    #[should_panic(expected = "indexed a free arena slot")]
426    fn mutably_indexing_a_free_slot_panics() {
427        let mut arena = NodeArena::new();
428        let idx = arena.insert(ArenaSparseNode::TakenSubtrie);
429        arena.remove(idx);
430        arena[idx] = ArenaSparseNode::TakenSubtrie;
431    }
432
433    #[test]
434    #[should_panic(expected = "drained a free arena slot")]
435    fn draining_a_free_slot_panics() {
436        let mut arena = NodeArena::new();
437        let idx = arena.insert(ArenaSparseNode::TakenSubtrie);
438        arena.drain_node(idx);
439        arena.drain_node(idx);
440    }
441
442    #[test]
443    fn branch_child_preserves_largest_index() {
444        let index = Index::new((1 << 31) - 1);
445        assert_eq!(BranchChild::revealed(index).revealed_index(), Some(index));
446        assert_eq!(BranchChild::blinded(index.0).blinded_slot(), Some(index.0));
447    }
448
449    #[test]
450    #[should_panic(expected = "arena index overflows the blinded tag bit")]
451    fn arena_index_rejects_blinded_tag_bit() {
452        Index::new(1 << 31);
453    }
454
455    #[test]
456    fn blinded_slots_are_recycled() {
457        let mut arena = NodeArena::new();
458        let rlp = RlpNode::word_rlp(&B256::repeat_byte(1));
459        let child = arena.insert_blinded(rlp.clone());
460        assert!(child.is_blinded());
461        assert_eq!(child.revealed_index(), None);
462        assert_eq!(arena.blinded(child), &rlp);
463
464        assert_eq!(arena.take_blinded(child), rlp);
465        let other = RlpNode::word_rlp(&B256::repeat_byte(2));
466        assert_eq!(arena.insert_blinded(other.clone()), child, "the freed slot is reused");
467        assert_eq!(arena.blinded(child), &other);
468    }
469
470    #[test]
471    fn sweeping_an_adopted_table_keeps_marked_slots() {
472        let mut src = NodeArena::new();
473        // More than one word of the mark bitmap.
474        let children: Vec<_> = (0..200u8)
475            .map(|byte| src.insert_blinded(RlpNode::word_rlp(&B256::repeat_byte(byte))))
476            .collect();
477
478        let mut dst = NodeArena::new();
479        let mut marks = dst.adopt_blinded(&mut src);
480        for child in children.iter().step_by(3) {
481            marks.mark(*child);
482        }
483        dst.sweep_blinded(marks);
484
485        let last_marked = (children.len() - 1) / 3 * 3;
486        assert_eq!(dst.blinded.len(), last_marked + 1, "the unmarked tail is dropped");
487
488        // Every unmarked slot below the tail is handed out again, lowest first.
489        let expected: Vec<u32> = (0..=last_marked as u32).filter(|slot| slot % 3 != 0).collect();
490        let handed_out: Vec<u32> = expected
491            .iter()
492            .map(|_| dst.insert_blinded(RlpNode::default()).blinded_slot().unwrap())
493            .collect();
494        assert_eq!(handed_out, expected);
495
496        for (byte, child) in (0..200u8).step_by(3).zip(children.iter().step_by(3)) {
497            assert_eq!(dst.blinded(*child), &RlpNode::word_rlp(&B256::repeat_byte(byte)));
498        }
499    }
500
501    #[test]
502    fn sweeping_releases_excess_side_table_capacity() {
503        let mut src = NodeArena::new();
504        let rlp = RlpNode::word_rlp(&B256::repeat_byte(1));
505        let children: Vec<_> = (0..4096).map(|_| src.insert_blinded(rlp.clone())).collect();
506        for child in &children[2048..] {
507            src.take_blinded(*child);
508        }
509
510        let mut dst = NodeArena::new();
511        let mut marks = dst.adopt_blinded(&mut src);
512        for child in children[..64].iter().step_by(2) {
513            marks.mark(*child);
514        }
515        dst.sweep_blinded(marks);
516
517        assert!(dst.blinded.capacity() <= 128, "release the discarded RLP capacity");
518        assert!(dst.blinded_free.capacity() <= 64, "release the discarded free-list capacity");
519        for child in children[..64].iter().step_by(2) {
520            assert_eq!(dst.blinded(*child), &rlp);
521        }
522        for slot in (1..63).step_by(2) {
523            assert_eq!(dst.insert_blinded(rlp.clone()).blinded_slot(), Some(slot));
524        }
525
526        let mut empty = NodeArena::new();
527        let marks = empty.adopt_blinded(&mut dst);
528        empty.sweep_blinded(marks);
529        assert_eq!(empty.blinded.capacity(), 0);
530        assert_eq!(empty.blinded_free.capacity(), 0);
531    }
532
533    #[test]
534    fn sweeping_keeps_capacity_for_small_contractions() {
535        let mut src = NodeArena::new();
536        let children: Vec<_> = (0..128).map(|_| src.insert_blinded(RlpNode::default())).collect();
537        let capacity = src.blinded.capacity();
538        let mut dst = NodeArena::new();
539        let mut marks = dst.adopt_blinded(&mut src);
540        for child in &children[..96] {
541            marks.mark(*child);
542        }
543        dst.sweep_blinded(marks);
544        assert_eq!(dst.blinded.capacity(), capacity);
545    }
546
547    #[test]
548    fn sweeping_compacts_a_live_replacement_at_the_tail() {
549        let mut src = NodeArena::new();
550        for _ in 0..65_536 {
551            src.insert_blinded(RlpNode::default());
552        }
553        let mut dst = NodeArena::new();
554        let mut marks = dst.adopt_blinded(&mut src);
555        let rlp = RlpNode::word_rlp(&B256::repeat_byte(1));
556        let replacement = dst.insert_blinded(rlp.clone());
557        assert_eq!(replacement.blinded_slot(), Some(65_536));
558        let root = dst.insert(branch([replacement]));
559        marks.mark(replacement);
560        dst.sweep_blinded(marks);
561
562        let child = dst[root].branch_ref().children[0];
563        assert_eq!(dst.blinded(child), &rlp);
564        assert_eq!(child.blinded_slot(), Some(0));
565        assert_eq!(dst.blinded.len(), 1);
566        assert!(dst.blinded.capacity() <= 2);
567        assert_eq!(dst.blinded_free.capacity(), 0);
568
569        // Subsequent sweeps visit only the compacted table, not the old high-water mark.
570        let mut next = NodeArena::new();
571        let mut marks = next.adopt_blinded(&mut dst);
572        assert_eq!(marks.words.len(), 1);
573        marks.mark(child);
574        next.insert(dst.drain_node(root));
575        next.sweep_blinded(marks);
576        assert_eq!(next.blinded(child), &rlp);
577        assert_eq!(next.blinded.len(), 1);
578    }
579
580    #[test]
581    fn sweeping_rewrites_sparse_slots_across_bitmap_words() {
582        let mut src = NodeArena::new();
583        let children: Vec<_> = (0..200u8)
584            .map(|byte| src.insert_blinded(RlpNode::word_rlp(&B256::repeat_byte(byte))))
585            .collect();
586        let mut dst = NodeArena::new();
587        let mut marks = dst.adopt_blinded(&mut src);
588        let kept = [199, 64, 0, 130, 63];
589        for slot in kept {
590            marks.mark(children[slot]);
591        }
592        let leaf = dst.insert(ArenaSparseNode::TakenSubtrie);
593        let root = dst.insert(branch(
594            kept.map(|slot| children[slot]).into_iter().chain([BranchChild::revealed(leaf)]),
595        ));
596        dst.sweep_blinded(marks);
597
598        assert_eq!(dst.blinded.len(), kept.len());
599        assert!(dst.blinded.capacity() <= kept.len() * 2);
600        assert_eq!(dst.blinded_free.capacity(), 0);
601        let branch = dst[root].branch_ref();
602        for (pos, slot) in kept.into_iter().enumerate() {
603            assert_eq!(
604                dst.blinded(branch.children[pos]),
605                &RlpNode::word_rlp(&B256::repeat_byte(slot as u8))
606            );
607        }
608        assert_eq!(branch.children[kept.len()].revealed_index(), Some(leaf));
609        assert!(dst.contains_key(leaf));
610        assert_eq!(dst.insert_blinded(RlpNode::default()).blinded_slot(), Some(5));
611    }
612
613    fn branch(children: impl IntoIterator<Item = BranchChild>) -> ArenaSparseNode {
614        let children: smallvec::SmallVec<_> = children.into_iter().collect();
615        ArenaSparseNode::Branch(ArenaSparseNodeBranch {
616            state_mask: alloy_trie::TrieMask::new((1 << children.len()) - 1),
617            children,
618            state: ArenaSparseNodeState::Revealed,
619            short_key: Default::default(),
620            branch_masks: Default::default(),
621        })
622    }
623}