Skip to main content

reth_trie_sparse/arena/
mod.rs

1mod branch_child_idx;
2mod cursor;
3mod node_arena;
4mod nodes;
5
6use branch_child_idx::{BranchChildIdx, BranchChildIter};
7use cursor::{branch_idx, ArenaCursor, NextResult, SeekResult};
8use node_arena::{BranchChild, Index, NodeArena};
9use nodes::{ArenaSparseNode, ArenaSparseNodeBranch, ArenaSparseNodeState};
10
11use crate::{
12    LeafLookup, LeafLookupError, LeafUpdate, SparseTrie, SparseTrieUpdates, TrieNodeEpoch,
13};
14use alloc::{borrow::Cow, boxed::Box, vec::Vec};
15use alloy_primitives::{keccak256, map::B256Map, B256};
16use alloy_trie::TrieMask;
17use core::{cmp::Reverse, mem};
18use rayon::iter::{IntoParallelIterator, IntoParallelRefMutIterator, ParallelIterator};
19use reth_execution_errors::{SparseTrieErrorKind, SparseTrieResult};
20use reth_trie_common::{
21    BranchNodeMasks, BranchNodeRef, ExtensionNodeRef, LeafNodeRef, Nibbles, ProofTrieNodeV2,
22    ProofV2TargetParent, RlpNode, TrieNodeV2, EMPTY_ROOT_HASH,
23};
24use smallvec::SmallVec;
25use tracing::{instrument, trace};
26
27const TRACE_TARGET: &str = "trie::arena";
28
29/// The maximum path length (in nibbles) for nodes that live in the upper trie. Nodes at this
30/// depth or deeper belong to lower subtries.
31const UPPER_TRIE_MAX_DEPTH: usize = 2;
32
33/// Returns the branch data for a branch or extension node.
34fn branch_data(arena: &NodeArena, idx: Index) -> &ArenaSparseNodeBranch {
35    arena[branch_idx(arena, idx).expect("node must be a branch or extension")].branch_ref()
36}
37
38/// Returns the mutable branch data for a branch or extension node.
39fn branch_data_mut(arena: &mut NodeArena, idx: Index) -> &mut ArenaSparseNodeBranch {
40    let idx = branch_idx(arena, idx).expect("node must be a branch or extension");
41    arena[idx].branch_mut()
42}
43
44/// Returns the cached or dirty state for a node, following extension and subtrie roots.
45fn node_state_ref(arena: &NodeArena, idx: Index) -> Option<&ArenaSparseNodeState> {
46    match &arena[idx] {
47        ArenaSparseNode::Extension { child, .. } => node_state_ref(arena, *child),
48        ArenaSparseNode::Subtrie(subtrie) => node_state_ref(&subtrie.arena, subtrie.root),
49        node => node.state_ref(),
50    }
51}
52
53/// Returns mutable cached or dirty state for a branch, extension, leaf, or empty root node.
54fn node_state_mut(arena: &mut NodeArena, idx: Index) -> &mut ArenaSparseNodeState {
55    let idx = arena[idx].extension_child().unwrap_or(idx);
56    arena[idx].state_mut()
57}
58
59/// Returns whether a node should contribute a bit in its parent's tree mask.
60fn node_tree_mask_bit(arena: &NodeArena, idx: Index) -> bool {
61    match &arena[idx] {
62        ArenaSparseNode::Branch(branch) => !branch.branch_masks.is_empty(),
63        ArenaSparseNode::Extension { child, .. } => {
64            !arena[*child].branch_ref().branch_masks.is_empty()
65        }
66        ArenaSparseNode::Subtrie(subtrie) => node_tree_mask_bit(&subtrie.arena, subtrie.root),
67        _ => false,
68    }
69}
70
71/// Moves a node into another arena, keeping an extension with its branch child.
72fn move_node(dst: &mut NodeArena, src: &mut NodeArena, idx: Index) -> Index {
73    let node = src.drain_node(idx);
74    let new_idx = dst.insert(node);
75    if let Some(child_idx) = dst[new_idx].extension_child() {
76        let child = src.drain_node(child_idx);
77        let new_child_idx = dst.insert(child);
78        let ArenaSparseNode::Extension { child, .. } = &mut dst[new_idx] else { unreachable!() };
79        *child = new_child_idx;
80    }
81    new_idx
82}
83
84/// Compacts an arena by BFS-copying all reachable nodes into a fresh arena, dropping
85/// unreachable (pruned) slots. Parents are stored before children for cache-friendly top-down
86/// traversal.
87fn compact_arena(arena: &mut NodeArena, root: &mut Index) {
88    let mut new_arena = NodeArena::with_capacity(arena.len());
89    let mut marks = new_arena.adopt_blinded(arena);
90    let root_node = arena.drain_node(*root);
91    let new_root = new_arena.insert(root_node);
92
93    // Nodes append consecutively without removals, so the destination vector is also the BFS
94    // queue. Each visited node's children still reference the source arena until rewritten.
95    let mut next = 0;
96    while next < new_arena.len() {
97        let new_idx = Index::new(next);
98        next += 1;
99        if let ArenaSparseNode::Extension { child, .. } = new_arena[new_idx] {
100            let child_node = arena.drain_node(child);
101            let new_child_idx = new_arena.insert(child_node);
102            let ArenaSparseNode::Extension { child, .. } = &mut new_arena[new_idx] else {
103                unreachable!()
104            };
105            *child = new_child_idx;
106            continue;
107        }
108
109        let old_children: SmallVec<[(usize, Index); 16]> = match &new_arena[new_idx] {
110            ArenaSparseNode::Branch(b) => b
111                .children
112                .iter()
113                .enumerate()
114                .filter_map(|(pos, child)| match child.revealed_index() {
115                    Some(old_child_idx) => Some((pos, old_child_idx)),
116                    // A blinded child needs no rewrite, its slot survived the adoption.
117                    None => {
118                        marks.mark(*child);
119                        None
120                    }
121                })
122                .collect(),
123            _ => continue,
124        };
125
126        for (child_pos, old_child_idx) in old_children {
127            let child_node = arena.drain_node(old_child_idx);
128            let new_child_idx = new_arena.insert(child_node);
129            debug_assert_eq!(
130                new_child_idx.get() + 1,
131                new_arena.len(),
132                "compaction must append, the destination doubles as the BFS queue",
133            );
134            new_arena[new_idx].branch_mut().children[child_pos] =
135                BranchChild::revealed(new_child_idx);
136        }
137    }
138
139    debug_assert!(
140        arena.iter().next().is_none(),
141        "compact_arena: {} orphaned nodes remaining after BFS drain",
142        arena.iter().count(),
143    );
144
145    new_arena.sweep_blinded(marks);
146    *arena = new_arena;
147    *root = new_root;
148}
149
150/// Reusable traversal state and optional accumulators shared by
151/// [`ArenaSparseSubtrie`] and [`ArenaParallelSparseTrie`].
152#[derive(Debug, Default, Clone)]
153struct ArenaTrieBuffers {
154    /// Reusable cursor for trie traversals.
155    cursor: ArenaCursor,
156    /// Trie updates built up directly during hashing and structural changes. `Some` when
157    /// tracking updates, `None` otherwise. Initialized alongside `updates` in `set_updates`.
158    updates: Option<SparseTrieUpdates>,
159    /// Reusable buffer for RLP encoding.
160    rlp_buf: Vec<u8>,
161    /// Reusable buffer for child `RlpNode`s during hashing.
162    rlp_node_buf: Vec<RlpNode>,
163}
164
165impl ArenaTrieBuffers {
166    fn clear(&mut self) {
167        if let Some(updates) = self.updates.as_mut() {
168            updates.clear();
169        }
170        self.rlp_buf.clear();
171        self.rlp_node_buf.clear();
172    }
173}
174
175/// A subtrie within the arena-based parallel sparse trie.
176///
177/// Each subtrie owns its own arena, allowing parallel mutations across subtries.
178#[derive(Debug, Clone)]
179struct ArenaSparseSubtrie {
180    /// The arena allocating nodes within this subtrie.
181    arena: NodeArena,
182    /// The root node of this subtrie.
183    root: Index,
184    /// The absolute path of this subtrie's root in the full trie.
185    path: Nibbles,
186    /// Reusable buffers for traversal, RLP encoding, and update actions.
187    buffers: ArenaTrieBuffers,
188    /// Reusable buffer for collecting required proofs during leaf updates.
189    /// Each entry is `(index, proof)` where `index` is the position of the target in the
190    /// `sorted_updates` slice passed to [`Self::update_leaves`].
191    required_proofs: Vec<(usize, ArenaRequiredProof)>,
192    /// Total number of revealed leaves in this subtrie.
193    num_leaves: u64,
194    /// Number of dirty (modified since last hash) leaves in this subtrie.
195    num_dirty_leaves: u64,
196}
197
198impl ArenaSparseSubtrie {
199    /// Creates a new subtrie with a pre-allocated root slot containing
200    /// [`ArenaSparseNode::EmptyRoot`]. The caller must overwrite `subtrie.arena[subtrie.root]`
201    /// before use.
202    fn new(record_updates: bool) -> Box<Self> {
203        let mut arena = NodeArena::new();
204        let root =
205            arena.insert(ArenaSparseNode::EmptyRoot { state: ArenaSparseNodeState::Revealed });
206        let buffers = ArenaTrieBuffers {
207            updates: record_updates.then(SparseTrieUpdates::default),
208            ..Default::default()
209        };
210        Box::new(Self {
211            arena,
212            root,
213            path: Nibbles::default(),
214            buffers,
215            required_proofs: Vec::new(),
216            num_leaves: 0,
217            num_dirty_leaves: 0,
218        })
219    }
220
221    /// Asserts that `num_leaves` and `num_dirty_leaves` match the actual counts in the arena.
222    #[cfg(all(debug_assertions, not(feature = "fuzz")))]
223    fn debug_assert_counters(&self) {
224        let (actual_leaves, actual_dirty) =
225            ArenaParallelSparseTrie::count_leaves_and_dirty(&self.arena, self.root);
226        debug_assert_eq!(
227            self.num_leaves, actual_leaves,
228            "subtrie {:?} num_leaves mismatch: stored {} vs actual {}",
229            self.path, self.num_leaves, actual_leaves,
230        );
231        debug_assert_eq!(
232            self.num_dirty_leaves, actual_dirty,
233            "subtrie {:?} num_dirty_leaves mismatch: stored {} vs actual {}",
234            self.path, self.num_dirty_leaves, actual_dirty,
235        );
236    }
237
238    /// Collapses nodes last modified before `prune_before` into hash stubs while copying retained
239    /// nodes into a compacted arena.
240    ///
241    /// Expects that all nodes have computed hashes (i.e. `prune` is called after hashing).
242    fn prune(&mut self, prune_before: TrieNodeEpoch) -> usize {
243        // Only branches can have pruneable children.
244        if branch_idx(&self.arena, self.root).is_none() {
245            return 0;
246        }
247
248        debug_assert_eq!(self.num_dirty_leaves, 0, "prune must run after hashing");
249
250        if prune_before == TrieNodeEpoch::UNMODIFIED {
251            return 0;
252        }
253
254        let old_count = self.arena.len();
255        // Reserve an upper bound on the retained nodes and hand the excess back after the copy,
256        // so that copying never reallocates but discarded nodes still release their capacity.
257        let mut new_arena = NodeArena::with_capacity(old_count);
258        let mut marks = new_arena.adopt_blinded(&mut self.arena);
259        let mut new_num_leaves = 0u64;
260
261        // The subtrie root is retained by the owning upper trie.
262        let new_root = move_node(&mut new_arena, &mut self.arena, self.root);
263        let mut stack = Vec::new();
264        if let Some(frame) =
265            prepare_retained_node(&new_arena, new_root, self.path, &mut new_num_leaves)
266        {
267            stack.push(frame);
268        }
269
270        while let Some(frame) = stack.last_mut() {
271            let Some((child_pos, nibble, child)) = frame.next_child(&new_arena) else {
272                stack.pop();
273                continue;
274            };
275
276            let Some(old_child_idx) = child.revealed_index() else {
277                // A blinded child needs no rewrite, its slot survived the adoption.
278                marks.mark(child);
279                continue;
280            };
281
282            let parent_new_idx = frame.new_idx;
283            let mut child_path = frame.branch_logical_path;
284            child_path.push(nibble);
285
286            let child_epoch = node_state_ref(&self.arena, old_child_idx)
287                .and_then(ArenaSparseNodeState::cached_epoch)
288                .expect("prune must run after hashing");
289
290            if child_epoch.should_prune(prune_before) {
291                let node = &self.arena[old_child_idx];
292                let rlp_node = node_state_ref(&self.arena, old_child_idx)
293                    .and_then(ArenaSparseNodeState::cached_rlp_node)
294                    .cloned()
295                    .expect("prune must run after hashing");
296                trace!(
297                    target: TRACE_TARGET,
298                    path = ?child_path,
299                    variant = %AsRef::<str>::as_ref(node),
300                    cached_rlp_node = ?rlp_node,
301                    "pruning node",
302                );
303                let new_child = new_arena.insert_blinded(rlp_node);
304                marks.mark(new_child);
305                branch_data_mut(&mut new_arena, parent_new_idx).children[child_pos] = new_child;
306                let node = self.arena.drain_node(old_child_idx);
307                if let Some(child) = node.extension_child() {
308                    self.arena.drain_node(child);
309                }
310            } else {
311                let new_child_idx = move_node(&mut new_arena, &mut self.arena, old_child_idx);
312                if let Some(frame) = prepare_retained_node(
313                    &new_arena,
314                    new_child_idx,
315                    child_path,
316                    &mut new_num_leaves,
317                ) {
318                    stack.push(frame);
319                }
320                branch_data_mut(&mut new_arena, parent_new_idx).children[child_pos] =
321                    BranchChild::revealed(new_child_idx);
322            }
323        }
324
325        new_arena.sweep_blinded(marks);
326        new_arena.shrink_nodes_to_fit();
327        let pruned = old_count - new_arena.len();
328        self.num_leaves = new_num_leaves;
329        self.num_dirty_leaves = 0;
330        self.arena = new_arena;
331        self.root = new_root;
332
333        #[cfg(all(debug_assertions, not(feature = "fuzz")))]
334        self.debug_assert_counters();
335        return pruned;
336
337        struct CopyFrame {
338            new_idx: Index,
339            branch_logical_path: Nibbles,
340            state_mask: TrieMask,
341            remaining_child_mask: TrieMask,
342        }
343
344        impl CopyFrame {
345            fn next_child(&mut self, new_arena: &NodeArena) -> Option<(usize, u8, BranchChild)> {
346                let b = branch_data(new_arena, self.new_idx);
347
348                let nibble = self.remaining_child_mask.first_set_bit_index()?;
349                self.remaining_child_mask.unset_bit(nibble);
350                let child_idx = BranchChildIdx::new(self.state_mask, nibble)
351                    .expect("remaining_child_mask must be a subset of state_mask");
352
353                Some((child_idx.get(), nibble, b.children[child_idx]))
354            }
355        }
356
357        /// Prepares a retained node for copying, returning a stack frame when the node has children
358        /// to walk.
359        fn prepare_retained_node(
360            new_arena: &NodeArena,
361            new_idx: Index,
362            node_path: Nibbles,
363            new_num_leaves: &mut u64,
364        ) -> Option<CopyFrame> {
365            if branch_idx(new_arena, new_idx).is_none() {
366                if matches!(&new_arena[new_idx], ArenaSparseNode::Leaf { .. }) {
367                    *new_num_leaves += 1;
368                }
369                return None;
370            }
371            let b = branch_data(new_arena, new_idx);
372
373            let mut branch_logical_path = node_path;
374            if let Some(key) = new_arena[new_idx].extension_key() {
375                branch_logical_path.extend(key);
376            }
377
378            Some(CopyFrame {
379                new_idx,
380                branch_logical_path,
381                state_mask: b.state_mask,
382                remaining_child_mask: b.state_mask,
383            })
384        }
385    }
386
387    /// Applies leaf updates within this subtrie. Uses the same walk-down-with-cursor pattern as
388    /// [`Self::reveal_nodes`], but checks accessibility for [`LeafUpdate::Touched`] entries.
389    ///
390    /// `sorted_updates` must be sorted lexicographically by their nibbles path (index 1).
391    ///
392    /// Any required proofs are appended to `self.required_proofs` and should be drained by the
393    /// caller after this method returns.
394    #[instrument(
395        level = "trace",
396        target = TRACE_TARGET,
397        skip_all,
398        fields(
399            subtrie = ?self.path,
400            num_updates = sorted_updates.len(),
401        ),
402    )]
403    fn update_leaves(&mut self, sorted_updates: &[(B256, Nibbles, LeafUpdate)]) {
404        if sorted_updates.is_empty() {
405            return;
406        }
407        trace!(target: TRACE_TARGET, "Subtrie update_leaves");
408
409        debug_assert!(
410            !matches!(self.arena[self.root], ArenaSparseNode::EmptyRoot { .. }),
411            "subtrie root must not be EmptyRoot at start of update_leaves"
412        );
413
414        self.buffers.cursor.reset(&self.arena, self.root, self.path);
415
416        for (idx, &(key, ref full_path, ref update)) in sorted_updates.iter().enumerate() {
417            let find_result = self.buffers.cursor.seek(&mut self.arena, full_path);
418
419            // If the path hits a blinded node, request a proof regardless of update type.
420            if matches!(find_result, SeekResult::Blinded) {
421                let logical_len = self.buffers.cursor.head_logical_branch_path_len(&self.arena);
422                self.required_proofs.push((
423                    idx,
424                    ArenaRequiredProof { key, parent: ProofV2TargetParent::new(logical_len) },
425                ));
426                continue;
427            }
428
429            match update {
430                LeafUpdate::Changed(value) if !value.is_empty() => {
431                    // Upsert: insert or update a leaf with the given value.
432                    let (_result, deltas) = ArenaParallelSparseTrie::upsert_leaf(
433                        &mut self.arena,
434                        &mut self.buffers.cursor,
435                        &mut self.root,
436                        full_path,
437                        value,
438                        find_result,
439                    );
440                    self.num_leaves = (self.num_leaves as i64 + deltas.num_leaves_delta) as u64;
441                    self.num_dirty_leaves =
442                        (self.num_dirty_leaves as i64 + deltas.num_dirty_leaves_delta) as u64;
443                }
444                LeafUpdate::Changed(_) => {
445                    let (result, deltas) = ArenaParallelSparseTrie::remove_leaf(
446                        &mut self.arena,
447                        &mut self.buffers.cursor,
448                        &mut self.root,
449                        key,
450                        full_path,
451                        find_result,
452                        &mut self.buffers.updates,
453                    );
454                    self.num_leaves = (self.num_leaves as i64 + deltas.num_leaves_delta) as u64;
455                    self.num_dirty_leaves =
456                        (self.num_dirty_leaves as i64 + deltas.num_dirty_leaves_delta) as u64;
457
458                    // The removed leaf is already revealed, so only the blinded sibling needs a
459                    // proof. The caller retries the update once that sibling is revealed.
460                    if let RemoveLeafResult::NeedsProof { proof_key, parent, .. } = result {
461                        self.required_proofs
462                            .push((idx, ArenaRequiredProof { key: proof_key, parent }));
463                    }
464                }
465                LeafUpdate::Touched => {}
466            }
467        }
468
469        // Drain remaining cursor entries, propagating dirty state.
470        self.buffers.cursor.drain(&mut self.arena);
471
472        #[cfg(all(debug_assertions, not(feature = "fuzz")))]
473        self.debug_assert_counters();
474    }
475
476    /// Reveals nodes inside this subtrie. Uses [`ArenaCursor::seek`] to locate the ancestor
477    /// node, then replaces blinded children with the proof nodes.
478    fn reveal_nodes(&mut self, nodes: &mut [ProofTrieNodeV2]) -> SparseTrieResult<()> {
479        if nodes.is_empty() {
480            return Ok(());
481        }
482        trace!(target: TRACE_TARGET, path = ?self.path, num_nodes = nodes.len(), "Subtrie reveal_nodes");
483
484        debug_assert!(
485            !matches!(self.arena[self.root], ArenaSparseNode::EmptyRoot { .. }),
486            "subtrie root must not be EmptyRoot in reveal_nodes"
487        );
488
489        self.buffers.cursor.reset(&self.arena, self.root, self.path);
490
491        for node in nodes.iter_mut() {
492            let find_result = self.buffers.cursor.seek(&mut self.arena, &node.path);
493            if ArenaParallelSparseTrie::reveal_node(
494                &mut self.arena,
495                &self.buffers.cursor,
496                node,
497                find_result,
498            )
499            .is_some_and(|child_idx| matches!(self.arena[child_idx], ArenaSparseNode::Leaf { .. }))
500            {
501                self.num_leaves += 1;
502            }
503        }
504
505        // Drain remaining cursor entries, propagating dirty state.
506        self.buffers.cursor.drain(&mut self.arena);
507
508        #[cfg(all(debug_assertions, not(feature = "fuzz")))]
509        self.debug_assert_counters();
510
511        Ok(())
512    }
513
514    /// Computes and caches `RlpNode` for all dirty nodes via iterative post-order DFS.
515    /// After this call every node reachable from `self.root` will be in `Cached` state.
516    ///
517    /// Trie updates are written directly to `self.buffers.updates` (if `Some`).
518    fn update_cached_rlp(&mut self, new_epoch: TrieNodeEpoch) {
519        ArenaParallelSparseTrie::update_cached_rlp(
520            &mut self.arena,
521            self.root,
522            self.path,
523            &mut self.buffers,
524            new_epoch,
525        );
526        self.num_dirty_leaves = 0;
527        #[cfg(all(debug_assertions, not(feature = "fuzz")))]
528        self.debug_assert_counters();
529    }
530}
531
532/// Tracks the net change in leaf counters caused by a trie mutation (upsert or removal).
533/// Returned alongside [`UpsertLeafResult`] / [`RemoveLeafResult`] so the caller can maintain
534/// aggregate counters on [`ArenaSparseSubtrie`] without scanning the arena.
535#[derive(Debug, Default)]
536struct SubtrieCounterDeltas {
537    num_leaves_delta: i64,
538    num_dirty_leaves_delta: i64,
539}
540
541/// Result of `upsert_leaf` indicating whether a new child was created that the caller
542/// may need to wrap as a subtrie (in the upper trie).
543#[derive(Debug)]
544enum UpsertLeafResult {
545    /// A leaf was updated in place (no structural change).
546    Updated,
547    /// A new leaf was created (e.g. EmptyRoot→Leaf, or root-level split).
548    NewLeaf,
549    /// A new child (branch or leaf) was created or inserted. The child is the cursor head
550    /// and its parent is the cursor's parent.
551    NewChild,
552}
553
554/// Result of `remove_leaf` indicating whether a proof is needed to complete a branch
555/// collapse.
556#[derive(Debug)]
557enum RemoveLeafResult {
558    /// No proof needed — the removal (and any collapse) completed fully.
559    Removed,
560    /// No leaf was found at the given path (no-op).
561    NotFound,
562    /// The branch collapse requires revealing a blinded sibling. The caller must request a
563    /// proof for the given key below the revealed logical parent branch.
564    NeedsProof { key: B256, proof_key: B256, parent: ProofV2TargetParent },
565}
566
567/// A proof request generated during leaf updates when a blinded node is encountered.
568#[derive(Debug, Clone)]
569struct ArenaRequiredProof {
570    /// The key requiring a proof.
571    key: B256,
572    /// The revealed logical parent branch.
573    parent: ProofV2TargetParent,
574}
575
576/// An arena-based parallel sparse trie.
577///
578/// Configuration for controlling when parallelism is enabled in [`ArenaParallelSparseTrie`]
579/// operations.
580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581pub struct ArenaParallelismThresholds {
582    /// Minimum number of dirty leaves in a subtrie before it is eligible for parallel hash
583    /// computation. Subtries with fewer dirty leaves than this are hashed serially during
584    /// [`ArenaParallelSparseTrie::update_subtrie_hashes`].
585    pub min_dirty_leaves: u64,
586    /// Minimum number of nodes to reveal in a subtrie before it is eligible for parallel
587    /// reveal. Subtries with fewer nodes to reveal than this are revealed inline during the
588    /// upper trie walk.
589    pub min_revealed_nodes: usize,
590    /// Minimum number of leaf updates targeting a subtrie before it is eligible for parallel
591    /// update. Subtries with fewer updates than this are updated inline during the upper trie
592    /// walk.
593    pub min_updates: usize,
594    /// Minimum number of revealed leaves in a subtrie before it is eligible for parallel
595    /// pruning. Subtries with fewer leaves than this are pruned inline during the upper trie
596    /// walk.
597    pub min_leaves_for_prune: u64,
598}
599
600impl Default for ArenaParallelismThresholds {
601    fn default() -> Self {
602        Self {
603            min_dirty_leaves: 64,
604            min_revealed_nodes: 16,
605            min_updates: 128,
606            min_leaves_for_prune: 128,
607        }
608    }
609}
610
611/// An arena-based sparse trie whose subtries can be mutated in parallel.
612///
613/// ## Structure
614///
615/// Uses arena allocation (`NodeArena`) for node storage with direct index-based child
616/// pointers, avoiding the per-node hashing overhead of a `HashMap`-based trie. The trie is split
617/// into two tiers:
618///
619/// - **Upper trie** (`upper_arena`): Contains nodes whose path is shorter than
620///   `UPPER_TRIE_MAX_DEPTH` nibbles. These are the root and its immediate children.
621/// - **Lower subtries** (`ArenaSparseSubtrie`): Each child of an upper-trie branch at the depth
622///   boundary becomes the root of its own subtrie, stored as an `ArenaSparseNode::Subtrie` child in
623///   the upper arena. Each subtrie owns its own arena, enabling lock-free parallel mutation.
624///
625/// Node placement is determined by path length (not counting a branch's short key):
626///
627/// - Paths with **< `UPPER_TRIE_MAX_DEPTH`** nibbles live in `upper_arena`.
628/// - Paths with **≥ `UPPER_TRIE_MAX_DEPTH`** nibbles live in a subtrie.
629///
630/// ## Node Revealing
631///
632/// Nodes are lazily revealed from proof data via [`SparseTrie::reveal_nodes`]. Each node is
633/// placed into the upper arena or delegated to its subtrie based on path depth. Unrevealed
634/// children are stored as blinded `BranchChild`s referencing their RLP encoding.
635/// When multiple subtries have pending reveals, they are processed in parallel using rayon
636/// (controlled by [`ArenaParallelismThresholds::min_revealed_nodes`]).
637///
638/// ## Leaf Operations
639///
640/// Leaf updates and removals are applied via [`SparseTrie::update_leaves`]. The method walks
641/// the upper trie to route each update to the correct subtrie, then processes subtries in
642/// parallel when the update count exceeds [`ArenaParallelismThresholds::min_updates`].
643///
644/// After updates, structural changes (branch collapse, subtrie unwrapping) are handled by
645/// propagating dirty state back up through the upper trie.
646///
647/// ## Root Hash Calculation
648///
649/// Root hash computation follows a bottom-up approach:
650///
651/// 1. **[`SparseTrie::update_subtrie_hashes`]**: Takes dirty subtries from the upper arena and
652///    hashes them in parallel (when dirty leaf count meets
653///    [`ArenaParallelismThresholds::min_dirty_leaves`]), then walks the upper trie to restore
654///    hashed subtries and inline-hash any remaining dirty nodes.
655/// 2. **[`SparseTrie::root`]**: Calls `update_subtrie_hashes`, then RLP-encodes the full upper trie
656///    depth-first to produce the root hash.
657///
658/// Each node tracks its state via `ArenaSparseNodeState` (`Revealed`, `Cached`, or `Dirty`)
659/// so only modified subtrees are recomputed.
660///
661/// ## Pruning
662///
663/// [`SparseTrie::prune`] replaces nodes older than its epoch cutoff with
664/// blinded `BranchChild` entries using their cached RLP, then compacts the
665/// arenas. Subtries are pruned in parallel when their leaf count exceeds
666/// [`ArenaParallelismThresholds::min_leaves_for_prune`].
667#[derive(Debug, Clone)]
668pub struct ArenaParallelSparseTrie {
669    /// The arena allocating nodes in the upper trie.
670    upper_arena: NodeArena,
671    /// The root node of the upper trie.
672    root: Index,
673    /// Reusable buffers for traversal, RLP encoding, and update actions.
674    buffers: ArenaTrieBuffers,
675    /// Thresholds controlling when parallelism is enabled for different operations.
676    parallelism_thresholds: ArenaParallelismThresholds,
677}
678
679impl ArenaParallelSparseTrie {
680    /// Sets the thresholds that control when parallelism is used during operations.
681    pub const fn with_parallelism_thresholds(
682        mut self,
683        thresholds: ArenaParallelismThresholds,
684    ) -> Self {
685        self.parallelism_thresholds = thresholds;
686        self
687    }
688
689    /// Returns `true` if a node at the given path length should be placed in a subtrie rather
690    /// than the upper arena.
691    const fn should_be_subtrie(path_len: usize) -> bool {
692        path_len == UPPER_TRIE_MAX_DEPTH
693    }
694
695    /// If the child at the cursor head should be a subtrie based on its depth, wraps it
696    /// in [`ArenaSparseNode::Subtrie`].
697    ///
698    /// The child must be the cursor head and its parent the cursor's parent.
699    fn maybe_wrap_in_subtrie(&mut self, child_idx: Index, child_path: &Nibbles) {
700        if !Self::should_be_subtrie(child_path.len()) {
701            return;
702        }
703
704        // Only branch and leaf nodes can become subtrie roots.
705        if !matches!(
706            self.upper_arena[child_idx],
707            ArenaSparseNode::Branch(_) |
708                ArenaSparseNode::Extension { .. } |
709                ArenaSparseNode::Leaf { .. }
710        ) {
711            return;
712        }
713
714        trace!(target: TRACE_TARGET, ?child_path, "Wrapping child into subtrie");
715        let mut subtrie = ArenaSparseSubtrie::new(self.buffers.updates.is_some());
716        subtrie.path = *child_path;
717        let mut root_node =
718            mem::replace(&mut self.upper_arena[child_idx], ArenaSparseNode::TakenSubtrie);
719
720        // Migrate any children from the upper arena into the subtrie arena.
721        Self::migrate_children(&mut subtrie.arena, &mut self.upper_arena, &mut root_node);
722
723        subtrie.arena[subtrie.root] = root_node;
724        let (leaves, dirty) = Self::count_leaves_and_dirty(&subtrie.arena, subtrie.root);
725        subtrie.num_leaves = leaves;
726        subtrie.num_dirty_leaves = dirty;
727        #[cfg(all(debug_assertions, not(feature = "fuzz")))]
728        subtrie.debug_assert_counters();
729        self.upper_arena[child_idx] = ArenaSparseNode::Subtrie(subtrie);
730    }
731
732    /// If the cursor head is a branch, wraps any revealed children that sit at
733    /// the subtrie boundary depth (`UPPER_TRIE_MAX_DEPTH`). This is needed after
734    /// structural changes like root-level splits or subtrie unwraps that can place
735    /// non-subtrie nodes at the boundary depth.
736    fn maybe_wrap_branch_children(&mut self, cursor: &ArenaCursor) {
737        let head_idx = cursor.head().expect("cursor is non-empty").index;
738        let head_path = cursor.head_path();
739
740        let Some(branch_idx) = branch_idx(&self.upper_arena, head_idx) else { return };
741        let short_key = self.upper_arena[head_idx].extension_key().copied();
742        let b = self.upper_arena[branch_idx].branch_ref();
743        let children: SmallVec<[_; 4]> = b
744            .child_iter()
745            .filter_map(|(nibble, child)| Some((nibble, child.revealed_index()?)))
746            .collect();
747
748        for (nibble, child_idx) in children {
749            let mut child_path = head_path;
750            if let Some(key) = short_key {
751                child_path.extend(&key);
752            }
753            child_path.push_unchecked(nibble);
754            self.maybe_wrap_in_subtrie(child_idx, &child_path);
755        }
756    }
757
758    /// Checks whether the subtrie at the cursor head has become empty after updates.
759    /// If the subtrie's root is [`ArenaSparseNode::EmptyRoot`] (all leaves were removed), the
760    /// child slot is removed from the parent branch entirely, the subtrie is recycled, and
761    /// if the parent is left with a single revealed child, it is collapsed via
762    /// `collapse_branch`.
763    ///
764    /// The subtrie must be the cursor head and its parent the cursor's parent.
765    /// Pops the subtrie entry (propagating leaf count deltas) before returning.
766    #[instrument(
767        level = "trace",
768        target = TRACE_TARGET,
769        skip_all,
770        fields(subtrie_path = ?cursor.head_path()),
771    )]
772    fn maybe_unwrap_subtrie(&mut self, cursor: &mut ArenaCursor) {
773        let subtrie_idx = cursor.head().expect("cursor is non-empty").index;
774
775        let ArenaSparseNode::Subtrie(subtrie) = &self.upper_arena[subtrie_idx] else {
776            return;
777        };
778
779        if !matches!(subtrie.arena[subtrie.root], ArenaSparseNode::EmptyRoot { .. }) {
780            return;
781        }
782
783        let child_nibble =
784            cursor.head_last_nibble().expect("subtrie path must have at least one nibble");
785        let parent_idx = cursor.parent().expect("cursor has parent").index;
786
787        // Pop the subtrie entry before mutating, so collapse_branch sees the parent as
788        // the cursor head.
789        cursor.pop(&mut self.upper_arena);
790
791        self.recycle_subtrie_from_idx(subtrie_idx);
792
793        trace!(target: TRACE_TARGET, "Unwrapping empty subtrie, removing child slot");
794        let parent_branch = branch_data_mut(&mut self.upper_arena, parent_idx);
795        let child_idx = BranchChildIdx::new(parent_branch.state_mask, child_nibble)
796            .expect("child nibble not found in parent state_mask");
797
798        parent_branch.children.remove(child_idx.get());
799        parent_branch.unset_child_bit(child_nibble);
800        // The branch structure changed (child removed), so any cached RLP is stale.
801        parent_branch.state = parent_branch.state.to_dirty();
802
803        self.maybe_collapse_or_remove_branch(cursor);
804    }
805
806    /// Merges buffered updates from a [`ArenaSparseNode::Subtrie`] and drops it.
807    ///
808    /// # Panics
809    ///
810    /// Panics if `node` is not a `Subtrie`.
811    fn recycle_subtrie(&mut self, node: ArenaSparseNode) {
812        let ArenaSparseNode::Subtrie(mut subtrie) = node else {
813            unreachable!("recycle_subtrie called on non-Subtrie node")
814        };
815        Self::merge_subtrie_updates(&mut self.buffers.updates, &mut subtrie.buffers.updates);
816    }
817
818    /// Removes a [`ArenaSparseNode::Subtrie`] from the upper arena at `idx` and recycles it.
819    fn recycle_subtrie_from_idx(&mut self, idx: Index) {
820        let node = self.upper_arena.remove(idx).expect("subtrie exists in arena");
821        self.recycle_subtrie(node);
822    }
823
824    /// Puts a subtrie that was taken for parallel processing back into its upper arena slot.
825    ///
826    /// The slot must still hold the [`ArenaSparseNode::TakenSubtrie`] placeholder: indices carry
827    /// no generation, so a slot that was removed and handed out again would silently be
828    /// overwritten.
829    fn restore_taken_subtrie(&mut self, idx: Index, subtrie: Box<ArenaSparseSubtrie>) {
830        debug_assert!(
831            matches!(self.upper_arena[idx], ArenaSparseNode::TakenSubtrie),
832            "taken subtrie slot {idx:?} was removed or reused before restoration",
833        );
834        self.upper_arena[idx] = ArenaSparseNode::Subtrie(subtrie);
835    }
836
837    /// Handles cascading structural changes on the branch at the cursor head after a child
838    /// has been removed.
839    ///
840    /// Depending on the remaining child count:
841    /// - **0 children**: the branch becomes `EmptyRoot` (if root) or is removed from its parent,
842    ///   cascading upward.
843    /// - **1 child**: collapses the branch into its sole child, unless that child is a
844    ///   `TakenSubtrie` (deferred) or blinded. If the remaining child is an empty subtrie, it is
845    ///   also removed, reducing to the 0-children case.
846    /// - **2+ children**: nothing to do.
847    fn maybe_collapse_or_remove_branch(&mut self, cursor: &mut ArenaCursor) {
848        loop {
849            let branch_node_idx = cursor.head().expect("cursor is non-empty").index;
850            let branch_path = cursor.head_path();
851
852            // Read-only phase: extract the count and remaining-child info we need before
853            // mutating. All values here are Copy so the borrow is released.
854            let Some(branch_child_idx) = branch_idx(&self.upper_arena, branch_node_idx) else {
855                return;
856            };
857            let count = self.upper_arena[branch_child_idx].branch_ref().state_mask.count_bits();
858
859            if count >= 2 {
860                return;
861            }
862
863            if count == 0 {
864                if branch_node_idx == self.root {
865                    self.upper_arena[branch_node_idx] =
866                        ArenaSparseNode::EmptyRoot { state: ArenaSparseNodeState::Dirty };
867                    if branch_child_idx != branch_node_idx {
868                        self.upper_arena.remove(branch_child_idx);
869                    }
870                    return;
871                }
872                // Remove the empty branch from its parent.
873                let branch_nibble = branch_path.last().expect("non-root branch");
874                cursor.pop(&mut self.upper_arena);
875                self.upper_arena.remove(branch_node_idx);
876                if branch_child_idx != branch_node_idx {
877                    self.upper_arena.remove(branch_child_idx);
878                }
879                let parent_idx = cursor.head().expect("cursor is non-empty").index;
880                let parent_branch = branch_data_mut(&mut self.upper_arena, parent_idx);
881                let child_idx = BranchChildIdx::new(parent_branch.state_mask, branch_nibble)
882                    .expect("child nibble not found in parent state_mask");
883                parent_branch.children.remove(child_idx.get());
884                parent_branch.unset_child_bit(branch_nibble);
885                parent_branch.state = parent_branch.state.to_dirty();
886                continue; // re-check the parent
887            }
888
889            // count == 1 — determine what kind of child remains.
890            let (remaining_nibble, remaining_child_idx) = {
891                let b = self.upper_arena[branch_child_idx].branch_ref();
892                let nibble = b.state_mask.iter().next().expect("branch has at least one child");
893                (nibble, b.children[0].revealed_index())
894            };
895
896            let Some(child_idx) = remaining_child_idx else {
897                debug_assert!(false, "single remaining child is blinded — should have been caught by check_subtrie_collapse_needs_proof");
898                return;
899            };
900
901            if matches!(self.upper_arena[child_idx], ArenaSparseNode::TakenSubtrie) {
902                // Subtrie hasn't been restored yet; collapse is deferred to the
903                // post-restore phase.
904                return;
905            }
906
907            // Check if the remaining child is an empty subtrie that should also be removed.
908            let is_empty_subtrie = matches!(
909                &self.upper_arena[child_idx],
910                ArenaSparseNode::Subtrie(s) if matches!(s.arena[s.root], ArenaSparseNode::EmptyRoot { .. })
911            );
912
913            if is_empty_subtrie {
914                self.recycle_subtrie_from_idx(child_idx);
915                let branch = branch_data_mut(&mut self.upper_arena, branch_node_idx);
916                branch.children.remove(0);
917                branch.unset_child_bit(remaining_nibble);
918                branch.state = branch.state.to_dirty();
919                continue; // now count == 0, will be handled next iteration
920            }
921
922            // Normal collapse: the remaining child is a Leaf, Branch, or non-empty Subtrie.
923            Self::collapse_branch(
924                &mut self.upper_arena,
925                cursor,
926                &mut self.root,
927                &mut self.buffers.updates,
928            );
929
930            // After collapse, the remaining child (now at cursor head) may be a
931            // Subtrie whose path was shortened by the collapsed branch's prefix. Since
932            // should_be_subtrie requires path_len == UPPER_TRIE_MAX_DEPTH and the collapse
933            // made the path shorter, the subtrie is no longer eligible — unwrap it.
934            let child_idx = cursor.head().expect("cursor is non-empty").index;
935            if let ArenaSparseNode::Subtrie(_) = &self.upper_arena[child_idx] {
936                let ArenaSparseNode::Subtrie(mut subtrie) =
937                    mem::replace(&mut self.upper_arena[child_idx], ArenaSparseNode::TakenSubtrie)
938                else {
939                    unreachable!()
940                };
941                Self::migrate_nodes(
942                    &mut self.upper_arena,
943                    &mut subtrie.arena,
944                    subtrie.root,
945                    Some(child_idx),
946                );
947                Self::merge_subtrie_updates(
948                    &mut self.buffers.updates,
949                    &mut subtrie.buffers.updates,
950                );
951
952                // The migrated subtrie root may be a branch whose children now live in
953                // the upper arena at or beyond the subtrie boundary depth. Re-wrap any
954                // such children as subtries.
955                self.maybe_wrap_branch_children(cursor);
956            }
957            return;
958        }
959    }
960
961    /// Merges updates from a subtrie's buffer into the parent's buffer.
962    /// Both `dst` and `src` must be `Some` when updates are being tracked.
963    ///
964    /// Source removals cancel destination insertions (and vice versa) so that
965    /// updates accumulated across multiple `root()` calls within a single block
966    /// stay consistent.
967    fn merge_subtrie_updates(
968        dst: &mut Option<SparseTrieUpdates>,
969        src: &mut Option<SparseTrieUpdates>,
970    ) {
971        if let Some(dst_updates) = dst.as_mut() {
972            let src_updates = src.as_mut().expect("updates are enabled");
973
974            // Source insertions cancel destination removals.
975            for path in src_updates.updated_nodes.keys() {
976                dst_updates.removed_nodes.remove(path);
977            }
978            dst_updates.updated_nodes.extend(src_updates.updated_nodes.drain());
979
980            // Source removals cancel destination insertions.
981            for path in &src_updates.removed_nodes {
982                dst_updates.updated_nodes.remove(path);
983            }
984            dst_updates.removed_nodes.extend(src_updates.removed_nodes.drain());
985        }
986    }
987
988    /// Right-pads a nibble path with zeros and packs it into a [`B256`].
989    fn nibbles_to_padded_b256(path: &Nibbles) -> B256 {
990        let mut bytes = [0u8; 32];
991        path.pack_to(&mut bytes);
992        B256::from(bytes)
993    }
994
995    /// Returns the [`BranchNodeMasks`] for a branch based on the status of its children.
996    fn get_branch_masks(arena: &NodeArena, branch: &ArenaSparseNodeBranch) -> BranchNodeMasks {
997        let mut masks = BranchNodeMasks::default();
998
999        for (nibble, child) in branch.child_iter() {
1000            let (hash_bit, tree_bit) = match child.revealed_index() {
1001                Some(child_idx) => {
1002                    let child = &arena[child_idx];
1003                    (child.hash_mask_bit(), node_tree_mask_bit(arena, child_idx))
1004                }
1005                None => (
1006                    branch.branch_masks.hash_mask.is_bit_set(nibble),
1007                    branch.branch_masks.tree_mask.is_bit_set(nibble),
1008                ),
1009            };
1010
1011            masks.set_child_bits(nibble, hash_bit, tree_bit);
1012        }
1013
1014        masks
1015    }
1016
1017    /// Computes and caches `RlpNode` for all dirty nodes reachable from `root` in `arena`.
1018    ///
1019    /// Uses the cursor's stack to walk dirty branches depth-first. For each branch,
1020    /// children are iterated left-to-right:
1021    /// - Blinded, cached, leaf, and `EmptyRoot` children have their `RlpNode` pushed directly onto
1022    ///   `rlp_node_buf`.
1023    /// - Dirty branch children are pushed onto `stack` and processed recursively first.
1024    ///
1025    /// When a dirty branch child finishes and is popped, the parent resumes iteration after
1026    /// the child's nibble. Once all children of a branch are processed, the branch is encoded
1027    /// via `BranchNodeRef` using the last N entries on `rlp_node_buf`, then replaced with a
1028    /// single result `RlpNode`.
1029    #[instrument(level = "trace", target = TRACE_TARGET, skip_all, fields(base_path = ?base_path), ret)]
1030    fn update_cached_rlp(
1031        arena: &mut NodeArena,
1032        root: Index,
1033        base_path: Nibbles,
1034        buffers: &mut ArenaTrieBuffers,
1035        new_epoch: TrieNodeEpoch,
1036    ) -> RlpNode {
1037        let cursor = &mut buffers.cursor;
1038        let rlp_buf = &mut buffers.rlp_buf;
1039        let rlp_node_buf = &mut buffers.rlp_node_buf;
1040        let updates = &mut buffers.updates;
1041
1042        rlp_node_buf.clear();
1043
1044        // Step 1: Handle trivial roots that don't need the stack-based walk.
1045        // Empty roots and leaves are encoded in place. Already-cached branches need no work.
1046        // Only dirty branches enter the main loop below.
1047        match &arena[root] {
1048            ArenaSparseNode::EmptyRoot { state } => {
1049                let node_epoch = match state {
1050                    ArenaSparseNodeState::Cached { epoch, .. } => *epoch,
1051                    ArenaSparseNodeState::Revealed => TrieNodeEpoch::UNMODIFIED,
1052                    ArenaSparseNodeState::Dirty => new_epoch,
1053                };
1054                let rlp_node = RlpNode::word_rlp(&EMPTY_ROOT_HASH);
1055                *arena[root].state_mut() =
1056                    ArenaSparseNodeState::Cached { rlp_node: rlp_node.clone(), epoch: node_epoch };
1057                return rlp_node
1058            }
1059            ArenaSparseNode::Leaf { .. } => {
1060                Self::encode_leaf(arena, root, rlp_buf, rlp_node_buf, new_epoch);
1061                return rlp_node_buf.pop().expect("encode_leaf must push an RlpNode");
1062            }
1063            ArenaSparseNode::Branch(_) | ArenaSparseNode::Extension { .. } => {
1064                if let Some(ArenaSparseNodeState::Cached { rlp_node, .. }) =
1065                    node_state_ref(arena, root)
1066                {
1067                    let rlp_node = rlp_node.clone();
1068                    return rlp_node;
1069                }
1070            }
1071            ArenaSparseNode::Subtrie(_) | ArenaSparseNode::TakenSubtrie | ArenaSparseNode::Free => {
1072                unreachable!("Subtrie/TakenSubtrie/Free cannot be a subtrie's root");
1073            }
1074        }
1075
1076        cursor.reset(arena, root, base_path);
1077
1078        // Step 2: Walk dirty branches depth-first using `cursor.next`. Only dirty branches
1079        // are descended into; all other children (leaves, cached branches, blinded, subtries)
1080        // are encoded when their parent branch is popped.
1081        loop {
1082            let result = cursor.next(&mut *arena, |_, node| {
1083                matches!(node, ArenaSparseNode::Branch(b) if !matches!(b.state, ArenaSparseNodeState::Cached { .. })) ||
1084                    matches!(node, ArenaSparseNode::Extension { .. })
1085            });
1086
1087            match result {
1088                NextResult::Done => break,
1089                NextResult::NonBranch => {
1090                    unreachable!("should_descend only returns true for dirty branches")
1091                }
1092                NextResult::Branch => {}
1093            };
1094
1095            let head_idx = cursor.head().expect("cursor is non-empty").index;
1096            // Extensions are visited even when their backing branch is already cached.
1097            // Preserve its epoch, which may include changes to children that were removed.
1098            if matches!(node_state_ref(arena, head_idx), Some(ArenaSparseNodeState::Cached { .. }))
1099            {
1100                continue;
1101            }
1102            let head_path = cursor.head_path();
1103
1104            // The branch at `head_idx` is exhausted. All its dirty child branches
1105            // have already been encoded and cached. Collect all children's RLP nodes
1106            // and encode the branch.
1107            trace!(
1108                target: TRACE_TARGET,
1109                branch_path = ?head_path,
1110                branch_short_key = ?arena[head_idx].extension_key(),
1111                state_mask = ?branch_data(arena, head_idx).state_mask,
1112                "Calculating branch RlpNode",
1113            );
1114
1115            rlp_node_buf.clear();
1116            let mut node_epoch = TrieNodeEpoch::UNMODIFIED;
1117            let state_mask = branch_data(arena, head_idx).state_mask;
1118            for (dense_idx, _nibble) in BranchChildIter::new(state_mask) {
1119                let child = branch_data(arena, head_idx).children[dense_idx];
1120                match child.revealed_index() {
1121                    None => {
1122                        rlp_node_buf.push(arena.blinded(child).clone());
1123                    }
1124                    Some(child_idx) => {
1125                        match &arena[child_idx] {
1126                            ArenaSparseNode::Leaf { .. } => {
1127                                Self::encode_leaf(
1128                                    arena,
1129                                    child_idx,
1130                                    rlp_buf,
1131                                    rlp_node_buf,
1132                                    new_epoch,
1133                                );
1134                            }
1135                            ArenaSparseNode::Branch(child_b) => {
1136                                let ArenaSparseNodeState::Cached { rlp_node, .. } = &child_b.state
1137                                else {
1138                                    panic!("child branch must be cached after DFS");
1139                                };
1140                                let rlp_node = rlp_node.clone();
1141                                rlp_node_buf.push(rlp_node);
1142                            }
1143                            ArenaSparseNode::Extension { child, .. } => {
1144                                let Some(ArenaSparseNodeState::Cached { rlp_node, .. }) =
1145                                    arena[*child].state_ref()
1146                                else {
1147                                    panic!("extension branch child must be cached after DFS");
1148                                };
1149                                rlp_node_buf.push(rlp_node.clone());
1150                            }
1151                            ArenaSparseNode::Subtrie(subtrie) => {
1152                                let Some(ArenaSparseNodeState::Cached { rlp_node, .. }) =
1153                                    node_state_ref(&subtrie.arena, subtrie.root)
1154                                else {
1155                                    panic!("subtrie root must be cached");
1156                                };
1157                                rlp_node_buf.push(rlp_node.clone());
1158                            }
1159                            ArenaSparseNode::TakenSubtrie |
1160                            ArenaSparseNode::EmptyRoot { .. } |
1161                            ArenaSparseNode::Free => {
1162                                unreachable!("Unexpected child {:?}", arena[child_idx]);
1163                            }
1164                        }
1165                        let Some(ArenaSparseNodeState::Cached { epoch: child_epoch, .. }) =
1166                            node_state_ref(arena, child_idx)
1167                        else {
1168                            panic!("revealed child must be cached after encoding");
1169                        };
1170                        node_epoch = node_epoch.max(*child_epoch);
1171                    }
1172                }
1173            }
1174
1175            // Encode the branch, optionally wrapping in its parent extension.
1176            let b = branch_data(arena, head_idx);
1177            let short_key = arena[head_idx].extension_key().copied().unwrap_or_default();
1178            let state_mask = b.state_mask;
1179            let prev_branch_masks = b.branch_masks;
1180            let new_branch_masks = Self::get_branch_masks(arena, b);
1181            let was_dirty = matches!(b.state, ArenaSparseNodeState::Dirty);
1182            if was_dirty {
1183                node_epoch = node_epoch.max(new_epoch);
1184            }
1185
1186            rlp_buf.clear();
1187            let rlp_node = BranchNodeRef::new(rlp_node_buf, state_mask).rlp(rlp_buf);
1188
1189            let rlp_node = if short_key.is_empty() {
1190                rlp_node
1191            } else {
1192                rlp_buf.clear();
1193                ExtensionNodeRef::new(&short_key, &rlp_node).rlp(rlp_buf)
1194            };
1195
1196            trace!(
1197                target: TRACE_TARGET,
1198                path = ?head_path,
1199                short_key = ?arena[head_idx].short_key(),
1200                children = ?state_mask.iter().zip(rlp_node_buf.iter()).collect::<Vec<_>>(),
1201                rlp_node = ?rlp_node,
1202                "Calculated branch RlpNode",
1203            );
1204
1205            let branch = branch_data_mut(arena, head_idx);
1206            branch.state = ArenaSparseNodeState::Cached { rlp_node, epoch: node_epoch };
1207            branch.branch_masks = new_branch_masks;
1208
1209            // Record trie updates for dirty branches only.
1210            // Skip the root node (empty logical path) as PST does.
1211            if let Some(trie_updates) = updates.as_mut().filter(|_| was_dirty) {
1212                let mut logical_path = head_path;
1213                logical_path.extend(&short_key);
1214
1215                if !logical_path.is_empty() {
1216                    if !prev_branch_masks.is_empty() && new_branch_masks.is_empty() {
1217                        trie_updates.updated_nodes.remove(&logical_path);
1218                        trie_updates.removed_nodes.insert(logical_path);
1219                    } else if !new_branch_masks.is_empty() {
1220                        let compact = branch_data(arena, head_idx).branch_node_compact(arena);
1221                        trie_updates.updated_nodes.insert(logical_path, compact);
1222                        trie_updates.removed_nodes.remove(&logical_path);
1223                    }
1224                }
1225            }
1226        }
1227
1228        let Some(ArenaSparseNodeState::Cached { rlp_node, .. }) = node_state_ref(arena, root)
1229        else {
1230            panic!("root must be cached after update_cached_rlp");
1231        };
1232        rlp_node.clone()
1233    }
1234
1235    /// Immutable traversal to find a leaf value at `full_path` starting from `root` in `arena`.
1236    /// `path_offset` is the number of nibbles already consumed from `full_path`.
1237    fn get_leaf_value_in_arena<'a>(
1238        arena: &'a NodeArena,
1239        mut current: Index,
1240        full_path: &Nibbles,
1241        mut path_offset: usize,
1242    ) -> Option<&'a Vec<u8>> {
1243        loop {
1244            match &arena[current] {
1245                ArenaSparseNode::EmptyRoot { .. } |
1246                ArenaSparseNode::TakenSubtrie |
1247                ArenaSparseNode::Free => return None,
1248                ArenaSparseNode::Leaf { key, value, .. } => {
1249                    let remaining = full_path.slice_unchecked(path_offset, full_path.len());
1250                    return (remaining == *key).then_some(value);
1251                }
1252                ArenaSparseNode::Extension { key, child } => {
1253                    let logical_end = path_offset + key.len();
1254                    if full_path.len() <= logical_end ||
1255                        full_path.slice_unchecked(path_offset, logical_end) != *key
1256                    {
1257                        return None;
1258                    }
1259                    current = *child;
1260                    path_offset = logical_end;
1261                }
1262                ArenaSparseNode::Branch(b) => {
1263                    if full_path.len() <= path_offset {
1264                        return None;
1265                    }
1266
1267                    let child_nibble = full_path.get_unchecked(path_offset);
1268                    let child_idx = BranchChildIdx::new(b.state_mask, child_nibble)?;
1269                    current = b.children[child_idx].revealed_index()?;
1270                    path_offset += 1;
1271                }
1272                ArenaSparseNode::Subtrie(subtrie) => {
1273                    return Self::get_leaf_value_in_arena(
1274                        &subtrie.arena,
1275                        subtrie.root,
1276                        full_path,
1277                        path_offset,
1278                    );
1279                }
1280            }
1281        }
1282    }
1283
1284    /// Immutable traversal from the given root in `arena`, following `full_path` to find a leaf.
1285    /// Returns whether the leaf exists or not, or an error if a blinded node is encountered or
1286    /// the value doesn't match.
1287    fn find_leaf_in_arena(
1288        arena: &NodeArena,
1289        mut current: Index,
1290        full_path: &Nibbles,
1291        mut path_offset: usize,
1292        expected_value: Option<&Vec<u8>>,
1293    ) -> Result<LeafLookup, LeafLookupError> {
1294        loop {
1295            match &arena[current] {
1296                ArenaSparseNode::EmptyRoot { .. } |
1297                ArenaSparseNode::TakenSubtrie |
1298                ArenaSparseNode::Free => {
1299                    return Ok(LeafLookup::NonExistent);
1300                }
1301                ArenaSparseNode::Leaf { key, value, .. } => {
1302                    let remaining = full_path.slice(path_offset..);
1303                    if remaining != *key {
1304                        return Ok(LeafLookup::NonExistent);
1305                    }
1306                    if let Some(expected) = expected_value &&
1307                        *expected != *value
1308                    {
1309                        return Err(LeafLookupError::ValueMismatch {
1310                            path: *full_path,
1311                            expected: Some(expected.clone()),
1312                            actual: value.clone(),
1313                        });
1314                    }
1315                    return Ok(LeafLookup::Exists);
1316                }
1317                ArenaSparseNode::Extension { key, child } => {
1318                    let logical_end = path_offset + key.len();
1319                    if full_path.len() <= logical_end ||
1320                        full_path.slice(path_offset..logical_end) != *key
1321                    {
1322                        return Ok(LeafLookup::NonExistent);
1323                    }
1324                    current = *child;
1325                    path_offset = logical_end;
1326                }
1327                ArenaSparseNode::Branch(b) => {
1328                    if full_path.len() <= path_offset {
1329                        return Ok(LeafLookup::NonExistent);
1330                    }
1331
1332                    let child_nibble = full_path.get_unchecked(path_offset);
1333                    let Some(child_idx) = BranchChildIdx::new(b.state_mask, child_nibble) else {
1334                        return Ok(LeafLookup::NonExistent);
1335                    };
1336
1337                    let child = b.children[child_idx];
1338                    match child.revealed_index() {
1339                        None => {
1340                            let rlp_node = arena.blinded(child);
1341                            let hash = rlp_node
1342                                .as_hash()
1343                                .unwrap_or_else(|| keccak256(rlp_node.as_slice()));
1344                            let mut blinded_path = full_path.slice(..path_offset);
1345                            blinded_path.push_unchecked(child_nibble);
1346                            return Err(LeafLookupError::BlindedNode { path: blinded_path, hash });
1347                        }
1348                        Some(child_idx) => {
1349                            current = child_idx;
1350                            path_offset += 1;
1351                        }
1352                    }
1353                }
1354                ArenaSparseNode::Subtrie(subtrie) => {
1355                    return Self::find_leaf_in_arena(
1356                        &subtrie.arena,
1357                        subtrie.root,
1358                        full_path,
1359                        path_offset,
1360                        expected_value,
1361                    );
1362                }
1363            }
1364        }
1365    }
1366
1367    /// Encodes a leaf node's RLP and pushes it onto `rlp_node_buf`.
1368    ///
1369    /// If the leaf is already cached, its existing `RlpNode` is reused.
1370    fn encode_leaf(
1371        arena: &mut NodeArena,
1372        idx: Index,
1373        rlp_buf: &mut Vec<u8>,
1374        rlp_node_buf: &mut Vec<RlpNode>,
1375        new_epoch: TrieNodeEpoch,
1376    ) {
1377        let (key, value, state) = match &arena[idx] {
1378            ArenaSparseNode::Leaf { key, value, state } => (key, value, state),
1379            _ => unreachable!("encode_leaf called on non-Leaf node"),
1380        };
1381
1382        let epoch = match state {
1383            ArenaSparseNodeState::Cached { rlp_node, .. } => {
1384                rlp_node_buf.push(rlp_node.clone());
1385                return;
1386            }
1387            ArenaSparseNodeState::Revealed => TrieNodeEpoch::UNMODIFIED,
1388            ArenaSparseNodeState::Dirty => new_epoch,
1389        };
1390
1391        rlp_buf.clear();
1392        let rlp_node = LeafNodeRef { key, value }.rlp(rlp_buf);
1393
1394        *arena[idx].state_mut() =
1395            ArenaSparseNodeState::Cached { rlp_node: rlp_node.clone(), epoch };
1396        rlp_node_buf.push(rlp_node);
1397    }
1398
1399    /// Creates a new leaf and a new branch that splits an existing child from the new leaf at
1400    /// a divergence point. Returns the index of the new branch.
1401    ///
1402    /// `new_leaf_path` is the full remaining path for the new leaf (relative to the split
1403    /// point's parent).
1404    ///
1405    /// The old child's key (leaf) or extension key is truncated to the suffix after the
1406    /// divergence nibble and its state is set to dirty.
1407    ///
1408    /// The top of `stack` must be the leaf or branch being split. The top of stack will be the
1409    /// newly created branch once this returns.
1410    /// Returns `true` if the existing node was not already dirty (i.e., the split newly dirtied
1411    /// it).
1412    fn split_and_insert_leaf(
1413        arena: &mut NodeArena,
1414        cursor: &mut ArenaCursor,
1415        root: &mut Index,
1416        new_leaf_path: Nibbles,
1417        value: &[u8],
1418    ) -> bool {
1419        let old_child_idx = cursor.head().expect("cursor must have head").index;
1420        let old_child_short_key = arena[old_child_idx].short_key().expect("top of stack is a leaf");
1421        let diverge_len = new_leaf_path.common_prefix_length(old_child_short_key);
1422
1423        trace!(
1424            target: TRACE_TARGET,
1425            path = ?cursor.head_path(),
1426            ?new_leaf_path,
1427            ?old_child_short_key,
1428            diverge_len,
1429            "Splitting node and inserting new leaf",
1430        );
1431
1432        let old_child_nibble = old_child_short_key.get_unchecked(diverge_len);
1433        let old_child_suffix = old_child_short_key.slice(diverge_len + 1..);
1434
1435        // Truncate the old child's key/extension key and mark it dirty.
1436        // Track whether the existing node was not already dirty (a leaf that becomes newly dirty).
1437        let old_child = mem::replace(&mut arena[old_child_idx], ArenaSparseNode::TakenSubtrie);
1438        let newly_dirtied_existing = match old_child {
1439            ArenaSparseNode::Leaf { value, state, .. } => {
1440                let was_clean = !matches!(state, ArenaSparseNodeState::Dirty);
1441                arena[old_child_idx] = ArenaSparseNode::Leaf {
1442                    key: old_child_suffix,
1443                    value,
1444                    state: ArenaSparseNodeState::Dirty,
1445                };
1446                was_clean
1447            }
1448            ArenaSparseNode::Extension { child, .. } => {
1449                if old_child_suffix.is_empty() {
1450                    let branch = arena.remove(child).expect("extension child exists");
1451                    arena[old_child_idx] = branch;
1452                    *arena[old_child_idx].state_mut() = ArenaSparseNodeState::Dirty;
1453                } else {
1454                    arena[old_child_idx] =
1455                        ArenaSparseNode::Extension { key: old_child_suffix, child };
1456                    *arena[child].state_mut() = ArenaSparseNodeState::Dirty;
1457                }
1458                // Branches don't contribute to num_dirty_leaves.
1459                false
1460            }
1461            _ => unreachable!("split_and_insert_leaf called on non-Leaf/Extension node"),
1462        };
1463
1464        let short_key = new_leaf_path.slice(..diverge_len);
1465        let new_leaf_nibble = new_leaf_path.get_unchecked(diverge_len);
1466        debug_assert_ne!(old_child_nibble, new_leaf_nibble);
1467
1468        let new_leaf_idx = arena.insert(ArenaSparseNode::Leaf {
1469            state: ArenaSparseNodeState::Dirty,
1470            key: new_leaf_path.slice(diverge_len + 1..),
1471            value: value.to_vec(),
1472        });
1473
1474        let (first_nibble, first_child, second_nibble, second_child) =
1475            if old_child_nibble < new_leaf_nibble {
1476                (old_child_nibble, old_child_idx, new_leaf_nibble, new_leaf_idx)
1477            } else {
1478                (new_leaf_nibble, new_leaf_idx, old_child_nibble, old_child_idx)
1479            };
1480
1481        let state_mask = TrieMask::from_nibble(first_nibble) | TrieMask::from_nibble(second_nibble);
1482        let mut children = SmallVec::new();
1483        children.push(BranchChild::revealed(first_child));
1484        children.push(BranchChild::revealed(second_child));
1485
1486        let branch_child_idx = arena.insert(ArenaSparseNode::Branch(ArenaSparseNodeBranch {
1487            state: ArenaSparseNodeState::Dirty,
1488            children,
1489            state_mask,
1490            branch_masks: BranchNodeMasks::default(),
1491        }));
1492        let new_branch_idx = if short_key.is_empty() {
1493            branch_child_idx
1494        } else {
1495            arena.insert(ArenaSparseNode::Extension { key: short_key, child: branch_child_idx })
1496        };
1497
1498        cursor.replace_head_index(arena, root, new_branch_idx);
1499        newly_dirtied_existing
1500    }
1501
1502    /// Performs a leaf upsert using a pre-computed [`SeekResult`] from
1503    /// [`ArenaCursor::seek`].
1504    ///
1505    /// Handles three cases based on `find_result`:
1506    /// 1. `RevealedLeaf` — the cursor head is a leaf; update in place or split into a branch.
1507    /// 2. Diverged — the path diverges within an extension key, split it.
1508    /// 3. `NoChild` — the target nibble has no child, insert a new leaf.
1509    ///
1510    /// The caller must handle [`SeekResult::Blinded`] and
1511    /// [`SeekResult::RevealedSubtrie`] before calling this function.
1512    /// The cursor must be non-empty when called.
1513    ///
1514    /// Returns an [`UpsertLeafResult`] and [`SubtrieCounterDeltas`] so the caller can maintain
1515    /// aggregate counters and decide whether to wrap the result as a subtrie.
1516    #[instrument(level = "trace", target = TRACE_TARGET, skip_all, fields(full_path = ?full_path))]
1517    fn upsert_leaf(
1518        arena: &mut NodeArena,
1519        cursor: &mut ArenaCursor,
1520        root: &mut Index,
1521        full_path: &Nibbles,
1522        value: &[u8],
1523        find_result: SeekResult,
1524    ) -> (UpsertLeafResult, SubtrieCounterDeltas) {
1525        trace!(target: TRACE_TARGET, ?find_result, "Upserting leaf");
1526        let head = cursor.head().expect("cursor is non-empty");
1527
1528        match find_result {
1529            SeekResult::Blinded => {
1530                unreachable!("Blinded case must be handled by caller")
1531            }
1532            SeekResult::EmptyRoot => {
1533                let head_idx = head.index;
1534                arena[head_idx] = ArenaSparseNode::Leaf {
1535                    state: ArenaSparseNodeState::Dirty,
1536                    key: full_path.slice(cursor.head_path_len()..),
1537                    value: value.to_vec(),
1538                };
1539                (
1540                    UpsertLeafResult::NewLeaf,
1541                    SubtrieCounterDeltas { num_leaves_delta: 1, num_dirty_leaves_delta: 1 },
1542                )
1543            }
1544            SeekResult::RevealedLeaf => {
1545                // RevealedLeaf guarantees the leaf's full path matches the target exactly.
1546                let head_idx = head.index;
1547                let was_clean =
1548                    if let ArenaSparseNode::Leaf { value: v, state, .. } = &mut arena[head_idx] {
1549                        v.clear();
1550                        v.extend_from_slice(value);
1551                        let was_clean = !matches!(state, ArenaSparseNodeState::Dirty);
1552                        *state = ArenaSparseNodeState::Dirty;
1553                        was_clean
1554                    } else {
1555                        unreachable!("RevealedLeaf but cursor head is not a leaf")
1556                    };
1557                (
1558                    UpsertLeafResult::Updated,
1559                    SubtrieCounterDeltas {
1560                        num_leaves_delta: 0,
1561                        num_dirty_leaves_delta: was_clean as i64,
1562                    },
1563                )
1564            }
1565            SeekResult::Diverged => {
1566                let full_path_from_head = full_path.slice(cursor.head_path_len()..);
1567
1568                let split_dirtied_existing =
1569                    Self::split_and_insert_leaf(arena, cursor, root, full_path_from_head, value);
1570
1571                let result = if cursor.depth() >= 1 {
1572                    UpsertLeafResult::NewChild
1573                } else {
1574                    UpsertLeafResult::NewLeaf
1575                };
1576                (
1577                    result,
1578                    SubtrieCounterDeltas {
1579                        num_leaves_delta: 1,
1580                        num_dirty_leaves_delta: 1 + split_dirtied_existing as i64,
1581                    },
1582                )
1583            }
1584            SeekResult::NoChild { child_nibble } => {
1585                let head_idx = head.index;
1586
1587                let leaf_key = full_path.slice(cursor.head_logical_branch_path_len(arena) + 1..);
1588                let new_leaf = arena.insert(ArenaSparseNode::Leaf {
1589                    state: ArenaSparseNodeState::Dirty,
1590                    key: leaf_key,
1591                    value: value.to_vec(),
1592                });
1593
1594                let branch = branch_data_mut(arena, head_idx);
1595                branch.set_child(child_nibble, BranchChild::revealed(new_leaf));
1596
1597                // Re-seek to position the cursor on the newly inserted leaf.
1598                cursor.seek(arena, full_path);
1599
1600                (
1601                    UpsertLeafResult::NewChild,
1602                    SubtrieCounterDeltas { num_leaves_delta: 1, num_dirty_leaves_delta: 1 },
1603                )
1604            }
1605            SeekResult::RevealedSubtrie => {
1606                unreachable!("RevealedSubtrie must be handled by caller")
1607            }
1608        }
1609    }
1610
1611    /// Removes a leaf node from the trie using a pre-computed [`SeekResult`] from
1612    /// [`ArenaCursor::seek`].
1613    ///
1614    /// Only the `RevealedLeaf` case performs a removal — the leaf must exist and its full path
1615    /// must match `full_path`. All other cases (`Diverged`, `NoChild`) are no-ops since the leaf
1616    /// doesn't exist at that path.
1617    ///
1618    /// When removing a leaf from a branch, if the branch is left with only one remaining child,
1619    /// the branch is collapsed: the remaining child absorbs the extension key and child's nibble
1620    /// as a prefix to its own key or extension key, and replaces the branch in the parent.
1621    /// If the remaining child is blinded, the collapse cannot proceed and a
1622    /// [`RemoveLeafResult::NeedsProof`] is returned so the caller can request a proof.
1623    ///
1624    /// The caller must handle [`SeekResult::Blinded`] and
1625    /// [`SeekResult::RevealedSubtrie`] before calling this function.
1626    fn remove_leaf(
1627        arena: &mut NodeArena,
1628        cursor: &mut ArenaCursor,
1629        root: &mut Index,
1630        key: B256,
1631        full_path: &Nibbles,
1632        find_result: SeekResult,
1633        updates: &mut Option<SparseTrieUpdates>,
1634    ) -> (RemoveLeafResult, SubtrieCounterDeltas) {
1635        match find_result {
1636            SeekResult::Blinded | SeekResult::RevealedSubtrie => {
1637                unreachable!("Blinded/RevealedSubtrie must be handled by caller")
1638            }
1639            SeekResult::EmptyRoot | SeekResult::Diverged | SeekResult::NoChild { .. } => {
1640                (RemoveLeafResult::NotFound, SubtrieCounterDeltas::default())
1641            }
1642            SeekResult::RevealedLeaf => {
1643                // RevealedLeaf guarantees the leaf's full path matches the target exactly.
1644                let head_idx = cursor.head().expect("cursor is non-empty").index;
1645                let head_path = cursor.head_path();
1646
1647                trace!(
1648                    target: TRACE_TARGET,
1649                    path = ?head_path,
1650                    ?full_path,
1651                    "Removing leaf",
1652                );
1653
1654                // Before mutating, check if removing this leaf would leave the parent
1655                // branch with a single blinded sibling (requiring a proof to collapse).
1656                if let Some(parent_entry) = cursor.parent() {
1657                    let parent_idx = parent_entry.index;
1658                    let child_nibble = head_path.last().expect("non-root leaf");
1659                    let parent_branch = branch_data(arena, parent_idx);
1660
1661                    if parent_branch.state_mask.count_bits() == 2 &&
1662                        parent_branch.sibling_child(child_nibble).is_blinded()
1663                    {
1664                        let sibling_nibble = parent_branch
1665                            .state_mask
1666                            .iter()
1667                            .find(|&n| n != child_nibble)
1668                            .expect("branch has two children");
1669                        let mut sibling_path = cursor.parent_logical_branch_path(arena);
1670                        sibling_path.push_unchecked(sibling_nibble);
1671                        trace!(target: TRACE_TARGET, ?full_path, ?sibling_path, "Removal would collapse branch onto blinded sibling, requesting proof");
1672                        return (
1673                            RemoveLeafResult::NeedsProof {
1674                                key,
1675                                proof_key: Self::nibbles_to_padded_b256(&sibling_path),
1676                                parent: ProofV2TargetParent::new(
1677                                    sibling_path
1678                                        .len()
1679                                        .checked_sub(1)
1680                                        .expect("sibling path has a child nibble"),
1681                                ),
1682                            },
1683                            SubtrieCounterDeltas::default(),
1684                        );
1685                    }
1686                }
1687
1688                // Check if the removed leaf was dirty before removing it.
1689                let removed_was_dirty =
1690                    matches!(arena[head_idx].state_ref(), Some(ArenaSparseNodeState::Dirty));
1691
1692                if cursor.depth() == 0 {
1693                    // The leaf is the root — replace with EmptyRoot and reset the cursor
1694                    // so subsequent iterations can call seek normally.
1695                    arena.remove(head_idx);
1696                    *root = arena
1697                        .insert(ArenaSparseNode::EmptyRoot { state: ArenaSparseNodeState::Dirty });
1698                    cursor.reset(arena, *root, head_path);
1699                    return (
1700                        RemoveLeafResult::Removed,
1701                        SubtrieCounterDeltas {
1702                            num_leaves_delta: -1,
1703                            num_dirty_leaves_delta: -(removed_was_dirty as i64),
1704                        },
1705                    );
1706                }
1707
1708                // Pop the leaf entry, propagating dirty state to the parent.
1709                cursor.pop(arena);
1710
1711                // The parent must be a branch. Remove the leaf from it.
1712                let parent_entry = cursor.head().expect("cursor is non-empty");
1713                let parent_idx = parent_entry.index;
1714                let child_nibble = head_path.last().expect("non-root leaf");
1715
1716                // Remove the leaf from the arena and from the parent's children.
1717                arena.remove(head_idx);
1718                let parent_branch = branch_data_mut(arena, parent_idx);
1719                parent_branch.remove_child(child_nibble);
1720
1721                // If the branch now has only one child, collapse it. The blinded sibling
1722                // case was already handled above before any mutations.
1723                let collapse_dirtied_leaf = if parent_branch.state_mask.count_bits() == 1 {
1724                    Self::collapse_branch(arena, cursor, root, updates)
1725                } else {
1726                    false
1727                };
1728                (
1729                    RemoveLeafResult::Removed,
1730                    SubtrieCounterDeltas {
1731                        num_leaves_delta: -1,
1732                        num_dirty_leaves_delta: (collapse_dirtied_leaf as i64) -
1733                            (removed_was_dirty as i64),
1734                    },
1735                )
1736            }
1737        }
1738    }
1739
1740    /// Checks whether a subtrie receiving only removals would cause its parent branch to collapse
1741    /// onto a single blinded sibling. If so, returns the proof needed to reveal that blinded
1742    /// sibling so the caller can request it and skip the subtrie's updates.
1743    ///
1744    /// Returns `Some(proof)` for the blinded sibling when the edge-case applies, `None` otherwise.
1745    fn check_subtrie_collapse_needs_proof(
1746        arena: &NodeArena,
1747        cursor: &ArenaCursor,
1748        subtrie_updates: &[(B256, Nibbles, LeafUpdate)],
1749    ) -> Option<ArenaRequiredProof> {
1750        let num_removals = subtrie_updates
1751            .iter()
1752            .filter(|(_, _, u)| matches!(u, LeafUpdate::Changed(v) if v.is_empty()))
1753            .count() as u64;
1754
1755        // Touched is a no-op that doesn't alter trie structure, so it must be
1756        // excluded when deciding whether "all updates are removals". This mirrors
1757        // the `all_removals` / `might_empty_subtrie` filter in `update_leaves`.
1758        // Without this, a batch of removals + Touched entries
1759        // would fail the `num_removals != num_changed` check, skip the proof
1760        // request for the blinded sibling, and later panic in
1761        // `maybe_collapse_or_remove_branch` when the subtrie empties inline.
1762        let num_changed = subtrie_updates.iter().filter(|(_, _, u)| u.is_changed()).count() as u64;
1763
1764        if num_removals == 0 || num_removals != num_changed {
1765            return None;
1766        }
1767
1768        // The subtrie is the cursor head; its parent is the cursor's parent.
1769        let subtrie_entry = cursor.head()?;
1770        let subtrie_num_leaves = match &arena[subtrie_entry.index] {
1771            ArenaSparseNode::Subtrie(s) => s.num_leaves,
1772            _ => return None,
1773        };
1774        if num_removals < subtrie_num_leaves {
1775            return None;
1776        }
1777
1778        let child_nibble =
1779            cursor.head_last_nibble().expect("subtrie path must have at least one nibble");
1780
1781        let parent_entry = cursor.parent()?;
1782        let parent_branch = branch_data(arena, parent_entry.index);
1783        if parent_branch.state_mask.count_bits() != 2 {
1784            return None;
1785        }
1786
1787        if !parent_branch.sibling_child(child_nibble).is_blinded() {
1788            return None;
1789        }
1790
1791        let sibling_nibble = parent_branch
1792            .state_mask
1793            .iter()
1794            .find(|&n| n != child_nibble)
1795            .expect("branch has two children");
1796        let mut sibling_path = cursor.parent_logical_branch_path(arena);
1797        sibling_path.push_unchecked(sibling_nibble);
1798
1799        Some(ArenaRequiredProof {
1800            key: Self::nibbles_to_padded_b256(&sibling_path),
1801            parent: ProofV2TargetParent::new(
1802                sibling_path.len().checked_sub(1).expect("sibling path has a child nibble"),
1803            ),
1804        })
1805    }
1806
1807    /// Collapses a branch node that has exactly one remaining revealed child. The branch's
1808    /// extension key, the remaining child's nibble, and the child's own key or extension key are
1809    /// concatenated to form the child's new key or extension key. The child then replaces the
1810    /// branch in the grandparent (or becomes the new root).
1811    ///
1812    /// The caller must verify that the remaining child is not blinded before calling this function.
1813    ///
1814    /// The branch being collapsed must be the current cursor head. The cursor head will be
1815    /// replaced with the remaining child which has taken its place.
1816    /// Returns `true` if the collapse dirtied a surviving leaf that was not already dirty.
1817    fn collapse_branch(
1818        arena: &mut NodeArena,
1819        cursor: &mut ArenaCursor,
1820        root: &mut Index,
1821        updates: &mut Option<SparseTrieUpdates>,
1822    ) -> bool {
1823        let branch_node_idx = cursor.head().expect("cursor is non-empty").index;
1824        let branch_child_idx = branch_idx(arena, branch_node_idx).expect("cursor head is a branch");
1825        let branch_path = cursor.head_path();
1826        let branch = arena[branch_child_idx].branch_ref();
1827        let remaining_nibble =
1828            branch.state_mask.iter().next().expect("branch has at least one child");
1829        let branch_short_key = arena[branch_node_idx].extension_key().copied().unwrap_or_default();
1830
1831        debug_assert_eq!(
1832            branch.state_mask.count_bits(),
1833            1,
1834            "collapse_branch requires exactly 1 child"
1835        );
1836        debug_assert!(
1837            !branch.children[0].is_blinded(),
1838            "collapse_branch called with a blinded remaining child"
1839        );
1840
1841        trace!(
1842            target: TRACE_TARGET,
1843            path = ?branch_path,
1844            short_key = ?branch_short_key,
1845            branch_masks = ?branch.branch_masks,
1846            ?remaining_nibble,
1847            "Collapsing single-child branch",
1848        );
1849
1850        // Record the collapsed branch's logical path for trie update tracking if it
1851        // was previously persisted in the DB trie.
1852        if let Some(trie_updates) = updates.as_mut() &&
1853            !branch.branch_masks.is_empty()
1854        {
1855            let logical_path = cursor.head_logical_branch_path(arena);
1856            if !logical_path.is_empty() {
1857                trie_updates.updated_nodes.remove(&logical_path);
1858                trie_updates.removed_nodes.insert(logical_path);
1859            }
1860        }
1861
1862        // Build the prefix: extension key + remaining child's nibble.
1863        let mut prefix = branch_short_key;
1864        prefix.push_unchecked(remaining_nibble);
1865
1866        let child_idx = branch.children[0].revealed_index().expect("remaining child is revealed");
1867
1868        // Prepend the prefix to the child's key or extension key and mark dirty.
1869        // Track whether a leaf was newly dirtied by this collapse.
1870        let child = mem::replace(&mut arena[child_idx], ArenaSparseNode::TakenSubtrie);
1871        let (replacement_idx, newly_dirtied_leaf) = match child {
1872            ArenaSparseNode::Leaf { key, value, state } => {
1873                let mut new_key = prefix;
1874                new_key.extend(&key);
1875                let was_clean = !matches!(state, ArenaSparseNodeState::Dirty);
1876                arena[child_idx] = ArenaSparseNode::Leaf {
1877                    key: new_key,
1878                    state: ArenaSparseNodeState::Dirty,
1879                    value,
1880                };
1881                (child_idx, was_clean)
1882            }
1883            ArenaSparseNode::Extension { key, child } => {
1884                let mut new_key = prefix;
1885                new_key.extend(&key);
1886                arena[child_idx] = ArenaSparseNode::Extension { key: new_key, child };
1887                *arena[child].state_mut() = ArenaSparseNodeState::Dirty;
1888                (child_idx, false)
1889            }
1890            ArenaSparseNode::Branch(branch) => {
1891                let new_branch_idx = arena.insert(ArenaSparseNode::Branch(branch));
1892                *arena[new_branch_idx].state_mut() = ArenaSparseNodeState::Dirty;
1893                arena[child_idx] =
1894                    ArenaSparseNode::Extension { key: prefix, child: new_branch_idx };
1895                (child_idx, false)
1896            }
1897            ArenaSparseNode::Subtrie(mut subtrie) => {
1898                subtrie.path = branch_path;
1899                let root = subtrie.root;
1900                let root_node =
1901                    mem::replace(&mut subtrie.arena[root], ArenaSparseNode::TakenSubtrie);
1902                match root_node {
1903                    ArenaSparseNode::Branch(branch) => {
1904                        let child = subtrie.arena.insert(ArenaSparseNode::Branch(branch));
1905                        subtrie.arena[root] = ArenaSparseNode::Extension { key: prefix, child };
1906                        *subtrie.arena[child].state_mut() = ArenaSparseNodeState::Dirty;
1907                    }
1908                    ArenaSparseNode::Extension { key, child } => {
1909                        let mut new_key = prefix;
1910                        new_key.extend(&key);
1911                        subtrie.arena[root] = ArenaSparseNode::Extension { key: new_key, child };
1912                        *subtrie.arena[child].state_mut() = ArenaSparseNodeState::Dirty;
1913                    }
1914                    ArenaSparseNode::Leaf { key, value, state } => {
1915                        let mut new_key = prefix;
1916                        new_key.extend(&key);
1917                        let was_clean = !matches!(state, ArenaSparseNodeState::Dirty);
1918                        subtrie.arena[root] = ArenaSparseNode::Leaf {
1919                            key: new_key,
1920                            value,
1921                            state: ArenaSparseNodeState::Dirty,
1922                        };
1923                        if was_clean {
1924                            subtrie.num_dirty_leaves += 1;
1925                        }
1926                    }
1927                    _ => {
1928                        unreachable!("subtrie root must be a Branch or Leaf during collapse_branch")
1929                    }
1930                }
1931                arena[child_idx] = ArenaSparseNode::Subtrie(subtrie);
1932                (child_idx, false)
1933            }
1934            _ => unreachable!("remaining child must be Leaf, Branch, Extension, or Subtrie"),
1935        };
1936
1937        // Replace the branch with the remaining child in the grandparent (or root).
1938        cursor.replace_head_index(arena, root, replacement_idx);
1939
1940        // Free the collapsed branch.
1941        arena.remove(branch_node_idx);
1942        if branch_node_idx != branch_child_idx {
1943            arena.remove(branch_child_idx);
1944        }
1945        newly_dirtied_leaf
1946    }
1947
1948    /// Counts the total leaves and dirty leaves in a subtree rooted at `idx`.
1949    fn count_leaves_and_dirty(arena: &NodeArena, idx: Index) -> (u64, u64) {
1950        match &arena[idx] {
1951            ArenaSparseNode::Leaf { state, .. } => {
1952                let dirty = matches!(state, ArenaSparseNodeState::Dirty) as u64;
1953                (1, dirty)
1954            }
1955            ArenaSparseNode::Branch(b) => {
1956                let mut leaves = 0u64;
1957                let mut dirty = 0u64;
1958                for c in &b.children {
1959                    if let Some(child_idx) = c.revealed_index() {
1960                        let (l, d) = Self::count_leaves_and_dirty(arena, child_idx);
1961                        leaves += l;
1962                        dirty += d;
1963                    }
1964                }
1965                (leaves, dirty)
1966            }
1967            ArenaSparseNode::Extension { child, .. } => Self::count_leaves_and_dirty(arena, *child),
1968            _ => (0, 0),
1969        }
1970    }
1971
1972    /// Asserts that every node in the upper arena satisfies the subtrie structure invariant:
1973    /// - Nodes at `UPPER_TRIE_MAX_DEPTH` path length must be `Subtrie` (or `TakenSubtrie`).
1974    /// - Nodes at other depths must NOT be `Subtrie`.
1975    ///
1976    /// Uses the cursor to DFS the upper arena, checking each visited node's path length.
1977    #[instrument(level = "trace", target = TRACE_TARGET, skip_all)]
1978    #[cfg(all(debug_assertions, not(feature = "fuzz")))]
1979    fn debug_assert_subtrie_structure(&mut self) {
1980        let mut cursor = mem::take(&mut self.buffers.cursor);
1981        cursor.reset(&self.upper_arena, self.root, Nibbles::default());
1982
1983        loop {
1984            let result = cursor.next(&mut self.upper_arena, |_, _| true);
1985            match result {
1986                NextResult::Done => break,
1987                NextResult::NonBranch | NextResult::Branch => {
1988                    let path_len = cursor.head_path_len();
1989                    let node = &self.upper_arena[cursor.head().expect("cursor is non-empty").index];
1990
1991                    if Self::should_be_subtrie(path_len) {
1992                        debug_assert!(
1993                            matches!(
1994                                node,
1995                                ArenaSparseNode::Subtrie(_) | ArenaSparseNode::TakenSubtrie
1996                            ),
1997                            "node at path_len={path_len} should be a Subtrie but is {node:?}",
1998                        );
1999                    } else {
2000                        debug_assert!(
2001                            !matches!(node, ArenaSparseNode::Subtrie(_)),
2002                            "node at path_len={path_len} should NOT be a Subtrie but is",
2003                        );
2004                    }
2005                }
2006            }
2007        }
2008
2009        self.buffers.cursor = cursor;
2010    }
2011
2012    /// Recursively migrates all nodes from `src` into `dst`, starting at `src_idx`.
2013    /// Branch children's `Revealed` indices are remapped to the new `dst` indices during
2014    /// the migration.
2015    ///
2016    /// If `dst_slot` is `Some(idx)`, the node at `src_idx` is placed into `dst[idx]`
2017    /// (overwriting); otherwise a new slot is allocated. Returns the `dst` index of the
2018    /// migrated node.
2019    fn migrate_nodes(
2020        dst: &mut NodeArena,
2021        src: &mut NodeArena,
2022        src_idx: Index,
2023        dst_slot: Option<Index>,
2024    ) -> Index {
2025        let mut node = src.remove(src_idx).expect("node exists in source arena");
2026
2027        // Recursively migrate children first so their new indices are known.
2028        Self::migrate_children(dst, src, &mut node);
2029
2030        if let Some(slot) = dst_slot {
2031            dst[slot] = node;
2032            slot
2033        } else {
2034            dst.insert(node)
2035        }
2036    }
2037
2038    /// Moves a node's children from `src` to `dst`: revealed children are migrated recursively
2039    /// with [`Self::migrate_nodes`], blinded children have their RLP moved into `dst`'s side
2040    /// table. `node` must already be detached from `src`.
2041    fn migrate_children(dst: &mut NodeArena, src: &mut NodeArena, node: &mut ArenaSparseNode) {
2042        match node {
2043            ArenaSparseNode::Extension { child, .. } => {
2044                *child = Self::migrate_nodes(dst, src, *child, None);
2045            }
2046            ArenaSparseNode::Branch(branch) => {
2047                for child in &mut branch.children {
2048                    *child = match child.revealed_index() {
2049                        Some(child_idx) => {
2050                            BranchChild::revealed(Self::migrate_nodes(dst, src, child_idx, None))
2051                        }
2052                        None => dst.insert_blinded(src.take_blinded(*child)),
2053                    };
2054                }
2055            }
2056            _ => {}
2057        }
2058    }
2059
2060    /// Removes a pruned node from the arena and blinds the parent's child slot with the node's
2061    /// cached RLP.
2062    fn remove_pruned_node(arena: &mut NodeArena, cursor: &mut ArenaCursor) -> ArenaSparseNode {
2063        // Entry paths are derived from the cursor's path, so read the head path before popping.
2064        let path = cursor.head_path();
2065        let idx = cursor.head().expect("cursor is non-empty").index;
2066        let rlp_node = node_state_ref(arena, idx)
2067            .and_then(ArenaSparseNodeState::cached_rlp_node)
2068            .cloned()
2069            .expect("prune must run after hashing");
2070        cursor.pop(arena);
2071        let node = arena.remove(idx).expect("node must exist to be pruned");
2072        if let Some(child) = node.extension_child() {
2073            arena.remove(child).expect("extension child exists");
2074        }
2075        trace!(
2076            target: TRACE_TARGET,
2077            ?path,
2078            variant = %AsRef::<str>::as_ref(&node),
2079            cached_rlp_node = ?rlp_node,
2080            "pruning node",
2081        );
2082
2083        let parent_idx = cursor.head().expect("pruned child has parent").index;
2084        let child_nibble = path.last().expect("non-root child");
2085        let blinded = arena.insert_blinded(rlp_node);
2086        let parent_branch = branch_data_mut(arena, parent_idx);
2087        let child_idx = BranchChildIdx::new(parent_branch.state_mask, child_nibble)
2088            .expect("child nibble not found in parent state_mask");
2089        parent_branch.children[child_idx] = blinded;
2090
2091        node
2092    }
2093
2094    /// Reveals a single proof node using a pre-computed [`SeekResult`] from
2095    /// [`ArenaCursor::seek`].
2096    ///
2097    /// If the result is `Blinded`, the blinded child is replaced with the proof node (converted to
2098    /// an arena node with `Cached` state). All other cases (already revealed, no child, diverged,
2099    /// leaf head) are no-ops — the proof node is skipped.
2100    ///
2101    /// Returns the `Index` of the revealed node in the arena, if any was revealed.
2102    #[instrument(level = "trace", target = TRACE_TARGET, skip_all)]
2103    fn reveal_node(
2104        arena: &mut NodeArena,
2105        cursor: &ArenaCursor,
2106        node: &mut ProofTrieNodeV2,
2107        find_result: SeekResult,
2108    ) -> Option<Index> {
2109        let SeekResult::Blinded = find_result else {
2110            // Already revealed, no child slot, or diverged — skip this proof node.
2111            return None;
2112        };
2113
2114        let head = cursor.head().expect("cursor is non-empty");
2115        let head_idx = head.index;
2116        let head_branch_logical_path = cursor.head_logical_branch_path(arena);
2117
2118        debug_assert_eq!(
2119            node.path.len(),
2120            head_branch_logical_path.len() + 1,
2121            "proof node path {:?} is not a direct child of branch at {:?} (expected depth {})",
2122            node.path,
2123            head_branch_logical_path,
2124            head_branch_logical_path.len() + 1,
2125        );
2126
2127        let child_nibble = node.path.get_unchecked(head_branch_logical_path.len());
2128        let head_branch = branch_data(arena, head_idx);
2129        let dense_child_idx = BranchChildIdx::new(head_branch.state_mask, child_nibble)
2130            .expect("Blinded result but child nibble not in state_mask");
2131
2132        let child = head_branch.children[dense_child_idx];
2133        if !child.is_blinded() {
2134            return None;
2135        }
2136        let cached_rlp = arena.blinded(child).clone();
2137
2138        trace!(
2139            target: TRACE_TARGET,
2140            path = ?node.path,
2141            rlp_node = ?cached_rlp,
2142            "Revealing node",
2143        );
2144
2145        let proof_node = mem::replace(node, ProofTrieNodeV2::empty());
2146        let child_idx = Self::insert_proof_node(arena, proof_node);
2147        *node_state_mut(arena, child_idx) =
2148            ArenaSparseNodeState::Cached { rlp_node: cached_rlp, epoch: TrieNodeEpoch::UNMODIFIED };
2149        branch_data_mut(arena, head_idx).children[dense_child_idx] =
2150            BranchChild::revealed(child_idx);
2151        arena.take_blinded(child);
2152
2153        Some(child_idx)
2154    }
2155
2156    /// Inserts a proof node, splitting a combined `BranchNodeV2` into an extension and its branch
2157    /// child when its short key is non-empty.
2158    fn insert_proof_node(arena: &mut NodeArena, proof_node: ProofTrieNodeV2) -> Index {
2159        let ProofTrieNodeV2 { path, node, masks } = proof_node;
2160        let TrieNodeV2::Branch(branch) = node else {
2161            let node =
2162                ArenaSparseNode::from_proof_node(arena, ProofTrieNodeV2 { path, node, masks });
2163            return arena.insert(node);
2164        };
2165
2166        let children = branch.stack[..branch.state_mask.count_bits() as usize]
2167            .iter()
2168            .map(|rlp| arena.insert_blinded(rlp.clone()))
2169            .collect();
2170        let child = arena.insert(ArenaSparseNode::Branch(ArenaSparseNodeBranch {
2171            state: ArenaSparseNodeState::Revealed,
2172            children,
2173            state_mask: branch.state_mask,
2174            branch_masks: masks.unwrap_or_default(),
2175        }));
2176        if branch.key.is_empty() {
2177            child
2178        } else {
2179            arena.insert(ArenaSparseNode::Extension { key: branch.key, child })
2180        }
2181    }
2182
2183    #[cfg(all(debug_assertions, not(feature = "fuzz")))]
2184    fn collect_reachable_nodes(
2185        arena: &NodeArena,
2186        idx: Index,
2187        reachable: &mut alloy_primitives::map::HashSet<Index>,
2188    ) {
2189        if !reachable.insert(idx) {
2190            return;
2191        }
2192        match &arena[idx] {
2193            ArenaSparseNode::Extension { child, .. } => {
2194                Self::collect_reachable_nodes(arena, *child, reachable);
2195            }
2196            ArenaSparseNode::Branch(b) => {
2197                for child in &b.children {
2198                    if let Some(child_idx) = child.revealed_index() {
2199                        Self::collect_reachable_nodes(arena, child_idx, reachable);
2200                    }
2201                }
2202            }
2203            _ => {}
2204        }
2205    }
2206
2207    #[cfg(all(debug_assertions, not(feature = "fuzz")))]
2208    fn assert_no_orphaned_nodes(arena: &NodeArena, root: Index, label: &str) {
2209        let mut reachable = alloy_primitives::map::HashSet::default();
2210        Self::collect_reachable_nodes(arena, root, &mut reachable);
2211        let all_indices: alloy_primitives::map::HashSet<Index> =
2212            arena.iter().map(|(idx, _)| idx).collect();
2213        let orphaned: Vec<_> = all_indices.difference(&reachable).collect();
2214        debug_assert!(
2215            orphaned.is_empty(),
2216            "{label} has {} orphaned node(s): {orphaned:?}",
2217            orphaned.len(),
2218        );
2219    }
2220}
2221
2222#[cfg(all(debug_assertions, not(feature = "fuzz")))]
2223impl Drop for ArenaParallelSparseTrie {
2224    fn drop(&mut self) {
2225        Self::assert_no_orphaned_nodes(&self.upper_arena, self.root, "upper arena");
2226
2227        for (_, node) in self.upper_arena.iter() {
2228            if let Some(subtrie) = node.as_subtrie() {
2229                Self::assert_no_orphaned_nodes(
2230                    &subtrie.arena,
2231                    subtrie.root,
2232                    &alloc::format!("subtrie {:?}", subtrie.path),
2233                );
2234            }
2235        }
2236    }
2237}
2238
2239impl Default for ArenaParallelSparseTrie {
2240    fn default() -> Self {
2241        let mut upper_arena = NodeArena::new();
2242        let root = upper_arena
2243            .insert(ArenaSparseNode::EmptyRoot { state: ArenaSparseNodeState::Revealed });
2244        Self {
2245            upper_arena,
2246            root,
2247            buffers: ArenaTrieBuffers::default(),
2248            parallelism_thresholds: ArenaParallelismThresholds::default(),
2249        }
2250    }
2251}
2252
2253impl ArenaParallelSparseTrie {
2254    /// Hashes a subtrie at `head_idx` and collects its update actions.
2255    fn update_upper_subtrie(&mut self, head_idx: Index, new_epoch: TrieNodeEpoch) {
2256        let ArenaSparseNode::Subtrie(subtrie) = &mut self.upper_arena[head_idx] else {
2257            unreachable!()
2258        };
2259
2260        if !node_state_ref(&subtrie.arena, subtrie.root)
2261            .is_some_and(|state| matches!(state, ArenaSparseNodeState::Cached { .. }))
2262        {
2263            subtrie.update_cached_rlp(new_epoch);
2264        }
2265
2266        Self::merge_subtrie_updates(&mut self.buffers.updates, &mut subtrie.buffers.updates);
2267    }
2268}
2269
2270impl SparseTrie for ArenaParallelSparseTrie {
2271    #[instrument(level = "trace", target = TRACE_TARGET, skip_all)]
2272    fn set_root(
2273        &mut self,
2274        root: TrieNodeV2,
2275        masks: Option<BranchNodeMasks>,
2276        retain_updates: bool,
2277    ) -> SparseTrieResult<()> {
2278        debug_assert!(
2279            matches!(self.upper_arena[self.root], ArenaSparseNode::EmptyRoot { .. }),
2280            "set_root called on a trie that already has revealed nodes"
2281        );
2282
2283        match root {
2284            TrieNodeV2::EmptyRoot => {
2285                trace!(target: TRACE_TARGET, "Setting empty root");
2286                self.upper_arena[self.root] =
2287                    ArenaSparseNode::EmptyRoot { state: ArenaSparseNodeState::Revealed };
2288            }
2289            TrieNodeV2::Leaf(leaf) => {
2290                trace!(target: TRACE_TARGET, key = ?leaf.key, "Setting leaf root");
2291                self.upper_arena[self.root] = ArenaSparseNode::Leaf {
2292                    state: ArenaSparseNodeState::Revealed,
2293                    key: leaf.key,
2294                    value: leaf.value,
2295                };
2296            }
2297            TrieNodeV2::Branch(branch) => {
2298                trace!(target: TRACE_TARGET, state_mask = ?branch.state_mask, num_children = branch.state_mask.count_bits(), "Setting branch root");
2299                let mut children = SmallVec::with_capacity(branch.state_mask.count_bits() as usize);
2300                for (stack_ptr, _nibble) in branch.state_mask.iter().enumerate() {
2301                    let child = self.upper_arena.insert_blinded(branch.stack[stack_ptr].clone());
2302                    children.push(child);
2303                }
2304
2305                let branch_node = ArenaSparseNode::Branch(ArenaSparseNodeBranch {
2306                    state: ArenaSparseNodeState::Revealed,
2307                    children,
2308                    state_mask: branch.state_mask,
2309                    branch_masks: masks.unwrap_or_default(),
2310                });
2311                if branch.key.is_empty() {
2312                    self.upper_arena[self.root] = branch_node;
2313                } else {
2314                    let child = self.upper_arena.insert(branch_node);
2315                    self.upper_arena[self.root] =
2316                        ArenaSparseNode::Extension { key: branch.key, child };
2317                }
2318            }
2319            TrieNodeV2::Extension(node) => {
2320                return Err(SparseTrieErrorKind::Reveal {
2321                    path: Nibbles::new(),
2322                    node: Box::new(node),
2323                }
2324                .into());
2325            }
2326        }
2327
2328        self.set_updates(retain_updates);
2329        Ok(())
2330    }
2331
2332    fn set_updates(&mut self, retain_updates: bool) {
2333        if retain_updates {
2334            self.buffers.updates.get_or_insert_with(SparseTrieUpdates::default).clear();
2335        } else {
2336            self.buffers.updates = None;
2337        }
2338    }
2339
2340    #[instrument(level = "trace", target = TRACE_TARGET, skip_all, fields(num_nodes = nodes.len()))]
2341    fn reveal_nodes(&mut self, nodes: &mut [ProofTrieNodeV2]) -> SparseTrieResult<()> {
2342        if nodes.is_empty() {
2343            return Ok(());
2344        }
2345
2346        if matches!(self.upper_arena[self.root], ArenaSparseNode::EmptyRoot { .. }) {
2347            trace!(target: TRACE_TARGET, "Skipping reveal_nodes on empty root");
2348            return Ok(());
2349        }
2350
2351        // Sort nodes lexicographically by path.
2352        nodes.sort_unstable_by_key(|n| n.path);
2353
2354        let threshold = self.parallelism_thresholds.min_revealed_nodes;
2355
2356        // Take the cursor out to avoid borrow conflicts with `self`.
2357        let mut cursor = mem::take(&mut self.buffers.cursor);
2358        cursor.reset(&self.upper_arena, self.root, Nibbles::default());
2359
2360        // Skip root node if present (set_root handles the root).
2361        let mut node_idx = if nodes[0].path.is_empty() { 1 } else { 0 };
2362
2363        // Walk the upper trie, revealing upper nodes inline and collecting subtrie work.
2364        // Subtries with enough nodes to reveal are taken for parallel processing; the rest
2365        // are revealed inline.
2366        let mut taken: Vec<(Index, Box<ArenaSparseSubtrie>, Vec<ProofTrieNodeV2>)> = Vec::new();
2367
2368        while node_idx < nodes.len() {
2369            let find_result = cursor.seek(&mut self.upper_arena, &nodes[node_idx].path);
2370
2371            match find_result {
2372                SeekResult::RevealedLeaf => {
2373                    trace!(target: TRACE_TARGET, path = ?nodes[node_idx].path, "Skipping reveal: leaf head");
2374                    node_idx += 1;
2375                }
2376                SeekResult::Blinded => {
2377                    // Save the proof node's path before reveal_node consumes it.
2378                    let child_path = nodes[node_idx].path;
2379                    let child_idx = Self::reveal_node(
2380                        &mut self.upper_arena,
2381                        &cursor,
2382                        &mut nodes[node_idx],
2383                        SeekResult::Blinded,
2384                    );
2385                    node_idx += 1;
2386
2387                    if let Some(child_idx) = child_idx {
2388                        self.maybe_wrap_in_subtrie(child_idx, &child_path);
2389                    }
2390                }
2391                SeekResult::RevealedSubtrie => {
2392                    let child_idx = cursor.head().expect("cursor is non-empty").index;
2393                    let prefix = cursor.head_path();
2394
2395                    let subtrie_start = node_idx;
2396                    while node_idx < nodes.len() && nodes[node_idx].path.starts_with(&prefix) {
2397                        node_idx += 1;
2398                    }
2399                    let num_subtrie_nodes = node_idx - subtrie_start;
2400
2401                    if num_subtrie_nodes >= threshold {
2402                        // Take subtrie for parallel reveal.
2403                        trace!(target: TRACE_TARGET, ?prefix, num_subtrie_nodes, "Taking subtrie for parallel reveal");
2404                        let ArenaSparseNode::Subtrie(subtrie) = mem::replace(
2405                            &mut self.upper_arena[child_idx],
2406                            ArenaSparseNode::TakenSubtrie,
2407                        ) else {
2408                            unreachable!("RevealedSubtrie must point to a Subtrie node")
2409                        };
2410                        let node_vec: Vec<ProofTrieNodeV2> = (subtrie_start..node_idx)
2411                            .map(|i| mem::replace(&mut nodes[i], ProofTrieNodeV2::empty()))
2412                            .collect();
2413                        taken.push((child_idx, subtrie, node_vec));
2414                    } else {
2415                        // Reveal inline.
2416                        trace!(target: TRACE_TARGET, ?prefix, num_subtrie_nodes, "Revealing subtrie inline");
2417                        let ArenaSparseNode::Subtrie(subtrie) = &mut self.upper_arena[child_idx]
2418                        else {
2419                            unreachable!("RevealedSubtrie must point to a Subtrie node")
2420                        };
2421                        let mut subtrie_nodes: Vec<ProofTrieNodeV2> = (subtrie_start..node_idx)
2422                            .map(|i| mem::replace(&mut nodes[i], ProofTrieNodeV2::empty()))
2423                            .collect();
2424                        subtrie.reveal_nodes(&mut subtrie_nodes)?;
2425                    }
2426                }
2427                _ => {
2428                    trace!(target: TRACE_TARGET, path = ?nodes[node_idx].path, ?find_result, "Skipping reveal: no blinded child");
2429                    node_idx += 1;
2430                }
2431            }
2432        }
2433
2434        // Drain remaining cursor entries from the upper-trie walk.
2435        cursor.drain(&mut self.upper_arena);
2436        self.buffers.cursor = cursor;
2437
2438        if taken.is_empty() {
2439            return Ok(());
2440        }
2441
2442        // Reveal taken subtries, in parallel if more than one.
2443        if taken.len() == 1 {
2444            let (_, subtrie, node_vec) = &mut taken[0];
2445            subtrie.reveal_nodes(node_vec)?;
2446        } else {
2447            let parent_span = tracing::Span::current();
2448            let results: Vec<SparseTrieResult<()>> = taken
2449                .par_iter_mut()
2450                .map(|(_, subtrie, node_vec)| {
2451                    let _guard = parent_span.enter();
2452                    subtrie.reveal_nodes(node_vec)
2453                })
2454                .collect();
2455
2456            if let Some(err) = results.into_iter().find(|r| r.is_err()) {
2457                // Restore before returning so we don't leave TakenSubtrie holes.
2458                for (idx, subtrie, _) in taken {
2459                    self.restore_taken_subtrie(idx, subtrie);
2460                }
2461                return err;
2462            }
2463        }
2464
2465        // Restore taken subtries into the upper arena.
2466        for (idx, subtrie, _) in taken {
2467            self.restore_taken_subtrie(idx, subtrie);
2468        }
2469
2470        #[cfg(all(debug_assertions, not(feature = "fuzz")))]
2471        self.debug_assert_subtrie_structure();
2472
2473        Ok(())
2474    }
2475
2476    #[instrument(level = "trace", target = TRACE_TARGET, skip_all, ret)]
2477    fn root(&mut self, new_epoch: TrieNodeEpoch) -> B256 {
2478        self.update_subtrie_hashes(new_epoch);
2479
2480        let rlp_node = Self::update_cached_rlp(
2481            &mut self.upper_arena,
2482            self.root,
2483            Nibbles::default(),
2484            &mut self.buffers,
2485            new_epoch,
2486        );
2487
2488        rlp_node.as_hash().expect("root RlpNode must be a hash")
2489    }
2490
2491    fn is_root_cached(&self) -> bool {
2492        matches!(
2493            node_state_ref(&self.upper_arena, self.root),
2494            Some(ArenaSparseNodeState::Cached { .. })
2495        )
2496    }
2497
2498    fn root_epoch(&self) -> Option<TrieNodeEpoch> {
2499        match node_state_ref(&self.upper_arena, self.root)? {
2500            ArenaSparseNodeState::Revealed => Some(TrieNodeEpoch::UNMODIFIED),
2501            ArenaSparseNodeState::Cached { epoch, .. } => Some(*epoch),
2502            ArenaSparseNodeState::Dirty => None,
2503        }
2504    }
2505
2506    #[instrument(level = "trace", target = TRACE_TARGET, skip_all)]
2507    fn update_subtrie_hashes(&mut self, new_epoch: TrieNodeEpoch) {
2508        trace!(target: TRACE_TARGET, "Updating subtrie hashes");
2509
2510        // Only descend if the root is a branch or extension; otherwise there are no subtries.
2511        if branch_idx(&self.upper_arena, self.root).is_none() {
2512            return;
2513        }
2514
2515        // Count total dirty leaves across all subtries to make one global parallelism decision.
2516        let mut total_dirty_leaves: u64 = 0;
2517        let mut taken: Vec<(Index, Box<ArenaSparseSubtrie>)> = Vec::new();
2518        for (idx, node) in self.upper_arena.iter_mut() {
2519            let ArenaSparseNode::Subtrie(s) = node else { continue };
2520            if s.num_dirty_leaves == 0 {
2521                continue;
2522            }
2523            total_dirty_leaves += s.num_dirty_leaves;
2524            let ArenaSparseNode::Subtrie(subtrie) =
2525                mem::replace(node, ArenaSparseNode::TakenSubtrie)
2526            else {
2527                unreachable!()
2528            };
2529            taken.push((idx, subtrie));
2530        }
2531
2532        // Hash taken subtries in parallel if total dirty leaves meet the threshold.
2533        if !taken.is_empty() {
2534            if taken.len() == 1 || total_dirty_leaves < self.parallelism_thresholds.min_dirty_leaves
2535            {
2536                for (_, subtrie) in &mut taken {
2537                    subtrie.update_cached_rlp(new_epoch);
2538                }
2539            } else {
2540                let parent_span = tracing::Span::current();
2541                taken = taken
2542                    .into_par_iter()
2543                    .map(|(idx, mut subtrie)| {
2544                        let _guard = parent_span.enter();
2545                        subtrie.update_cached_rlp(new_epoch);
2546                        (idx, subtrie)
2547                    })
2548                    .collect();
2549            }
2550        }
2551
2552        // If the root branch is already cached and nothing was taken for parallel
2553        // hashing, there are no dirty subtries to process.
2554        if taken.is_empty() && self.is_root_cached() {
2555            return;
2556        }
2557
2558        // Walk the upper trie depth-first, restoring hashed subtries and inline-hashing
2559        // any remaining dirty subtries. Only descend into dirty branches; clean subtrees
2560        // cannot contain dirty subtries since dirty state propagates upward.
2561        taken.sort_unstable_by_key(|(_, b)| Reverse(b.path));
2562
2563        self.buffers.cursor.reset(&self.upper_arena, self.root, Nibbles::default());
2564
2565        loop {
2566            let result = self.buffers.cursor.next(&mut self.upper_arena, |_, child| match child {
2567                ArenaSparseNode::Branch(_) | ArenaSparseNode::Subtrie(_) => !child.is_cached(),
2568                ArenaSparseNode::Extension { .. } | ArenaSparseNode::TakenSubtrie => true,
2569                _ => false,
2570            });
2571
2572            match result {
2573                NextResult::Done => break,
2574                NextResult::Branch => continue,
2575                NextResult::NonBranch => {}
2576            }
2577
2578            // Head is a subtrie or taken-subtrie — process it.
2579            let head_idx = self.buffers.cursor.head().expect("cursor is non-empty").index;
2580
2581            if matches!(&self.upper_arena[head_idx], ArenaSparseNode::TakenSubtrie) {
2582                let (_, subtrie) = taken.pop().expect("taken subtries must not be exhausted");
2583                debug_assert_eq!(
2584                    subtrie.path,
2585                    self.buffers.cursor.head_path(),
2586                    "taken subtrie path mismatch",
2587                );
2588                self.upper_arena[head_idx] = ArenaSparseNode::Subtrie(subtrie);
2589            }
2590
2591            self.update_upper_subtrie(head_idx, new_epoch);
2592        }
2593    }
2594
2595    fn get_leaf_value(&self, full_path: &Nibbles) -> Option<&Vec<u8>> {
2596        Self::get_leaf_value_in_arena(&self.upper_arena, self.root, full_path, 0)
2597    }
2598
2599    fn find_leaf(
2600        &self,
2601        full_path: &Nibbles,
2602        expected_value: Option<&Vec<u8>>,
2603    ) -> Result<LeafLookup, LeafLookupError> {
2604        Self::find_leaf_in_arena(&self.upper_arena, self.root, full_path, 0, expected_value)
2605    }
2606
2607    fn updates_ref(&self) -> Cow<'_, SparseTrieUpdates> {
2608        self.buffers
2609            .updates
2610            .as_ref()
2611            .map_or(Cow::Owned(SparseTrieUpdates::default()), Cow::Borrowed)
2612    }
2613
2614    fn take_updates(&mut self) -> SparseTrieUpdates {
2615        match self.buffers.updates.take() {
2616            Some(updates) => {
2617                self.buffers.updates = Some(SparseTrieUpdates::with_capacity(
2618                    updates.updated_nodes.len(),
2619                    updates.removed_nodes.len(),
2620                ));
2621                updates
2622            }
2623            None => SparseTrieUpdates::default(),
2624        }
2625    }
2626
2627    #[instrument(level = "trace", target = TRACE_TARGET, skip_all)]
2628    fn clear(&mut self) {
2629        self.upper_arena = NodeArena::new();
2630        self.root = self
2631            .upper_arena
2632            .insert(ArenaSparseNode::EmptyRoot { state: ArenaSparseNodeState::Revealed });
2633        self.buffers.clear();
2634    }
2635
2636    #[instrument(
2637        level = "trace",
2638        target = TRACE_TARGET,
2639        skip_all,
2640        fields(prune_before = prune_before.get()),
2641    )]
2642    fn prune(&mut self, prune_before: TrieNodeEpoch) -> usize {
2643        assert!(self.root_epoch().is_some(), "prune cannot run on a dirty trie");
2644
2645        // Only branches and extensions can have pruneable children.
2646        if branch_idx(&self.upper_arena, self.root).is_none() {
2647            return 0;
2648        }
2649
2650        let threshold = self.parallelism_thresholds.min_leaves_for_prune;
2651
2652        let mut cursor = mem::take(&mut self.buffers.cursor);
2653        cursor.reset(&self.upper_arena, self.root, Nibbles::default());
2654
2655        // Subtries taken for parallel pruning.
2656        let mut taken: Vec<(Index, Box<ArenaSparseSubtrie>)> = Vec::new();
2657
2658        let mut pruned = 0;
2659
2660        loop {
2661            let result = cursor.next(&mut self.upper_arena, |_, child| {
2662                matches!(
2663                    child,
2664                    ArenaSparseNode::Branch(_) |
2665                        ArenaSparseNode::Extension { .. } |
2666                        ArenaSparseNode::Subtrie(_) |
2667                        ArenaSparseNode::Leaf { .. }
2668                )
2669            });
2670
2671            if matches!(result, NextResult::Done) {
2672                break
2673            }
2674
2675            let head_idx = cursor.head().expect("cursor is non-empty").index;
2676
2677            match &self.upper_arena[head_idx] {
2678                ArenaSparseNode::Branch(_) |
2679                ArenaSparseNode::Extension { .. } |
2680                ArenaSparseNode::Leaf { .. } => {
2681                    // Don't prune the root.
2682                    if cursor.depth() == 0 {
2683                        continue;
2684                    }
2685
2686                    let node_epoch = node_state_ref(&self.upper_arena, head_idx)
2687                        .and_then(ArenaSparseNodeState::cached_epoch)
2688                        .expect("prune must run after hashing");
2689                    if !node_epoch.should_prune(prune_before) {
2690                        continue;
2691                    }
2692
2693                    let removed = Self::remove_pruned_node(&mut self.upper_arena, &mut cursor);
2694                    pruned += 1 + usize::from(removed.extension_child().is_some());
2695                }
2696                ArenaSparseNode::Subtrie(_) => {
2697                    let root_epoch = self.upper_arena[head_idx]
2698                        .state_ref()
2699                        .and_then(ArenaSparseNodeState::cached_epoch)
2700                        .expect("prune must run after hashing");
2701                    if root_epoch.should_prune(prune_before) {
2702                        let removed = Self::remove_pruned_node(&mut self.upper_arena, &mut cursor);
2703                        let ArenaSparseNode::Subtrie(s) = &removed else { unreachable!() };
2704                        pruned += s.arena.len();
2705                        self.recycle_subtrie(removed);
2706                        continue;
2707                    }
2708
2709                    let ArenaSparseNode::Subtrie(subtrie) = &self.upper_arena[head_idx] else {
2710                        unreachable!()
2711                    };
2712                    if subtrie.num_leaves >= threshold {
2713                        let ArenaSparseNode::Subtrie(subtrie) = mem::replace(
2714                            &mut self.upper_arena[head_idx],
2715                            ArenaSparseNode::TakenSubtrie,
2716                        ) else {
2717                            unreachable!()
2718                        };
2719                        taken.push((head_idx, subtrie));
2720                    } else {
2721                        let ArenaSparseNode::Subtrie(subtrie) = &mut self.upper_arena[head_idx]
2722                        else {
2723                            unreachable!()
2724                        };
2725                        pruned += subtrie.prune(prune_before);
2726                    }
2727                }
2728                _ => unreachable!("prune walk must visit a subtrie, leaf, branch, or extension"),
2729            }
2730        }
2731
2732        self.buffers.cursor = cursor;
2733
2734        if !taken.is_empty() {
2735            // Prune taken subtries, in parallel if more than one.
2736            if taken.len() == 1 {
2737                let (_, ref mut subtrie) = taken[0];
2738                pruned += subtrie.prune(prune_before);
2739            } else {
2740                let parent_span = tracing::Span::current();
2741                pruned += taken
2742                    .par_iter_mut()
2743                    .map(|(_, subtrie)| {
2744                        let _guard = parent_span.enter();
2745                        let _span = tracing::trace_span!(
2746                            target: TRACE_TARGET,
2747                            "subtrie_prune",
2748                            subtrie = ?subtrie.path,
2749                        )
2750                        .entered();
2751
2752                        subtrie.prune(prune_before)
2753                    })
2754                    .sum::<usize>();
2755            }
2756
2757            // Restore taken subtries into the upper arena.
2758            for (child_idx, subtrie) in taken {
2759                self.restore_taken_subtrie(child_idx, subtrie);
2760            }
2761        }
2762
2763        if pruned > 0 {
2764            compact_arena(&mut self.upper_arena, &mut self.root);
2765        }
2766
2767        pruned
2768    }
2769
2770    #[instrument(
2771        level = "trace",
2772        target = TRACE_TARGET,
2773        skip_all,
2774        fields(num_updates = updates.len()),
2775    )]
2776    fn update_leaves(
2777        &mut self,
2778        updates: &mut B256Map<LeafUpdate>,
2779        mut proof_required_fn: impl FnMut(B256, ProofV2TargetParent),
2780    ) -> SparseTrieResult<()> {
2781        if updates.is_empty() {
2782            return Ok(());
2783        }
2784
2785        // Drain and sort updates lexicographically by nibbles path.
2786        let mut sorted: Vec<_> =
2787            updates.drain().map(|(key, update)| (key, Nibbles::unpack(key), update)).collect();
2788        sorted.sort_unstable_by_key(|entry| entry.1);
2789
2790        let threshold = self.parallelism_thresholds.min_updates;
2791        let parallelize_distributed_updates = sorted.len() >= threshold.saturating_mul(4);
2792
2793        let mut cursor = mem::take(&mut self.buffers.cursor);
2794        cursor.reset(&self.upper_arena, self.root, Nibbles::default());
2795
2796        // Subtries taken for parallel processing: (arena_index, subtrie, update_range).
2797        let mut taken: Vec<(Index, Box<ArenaSparseSubtrie>, core::ops::Range<usize>)> = Vec::new();
2798
2799        let mut update_idx = 0;
2800        while update_idx < sorted.len() {
2801            let (key, ref full_path, ref update) = sorted[update_idx];
2802
2803            let find_result = cursor.seek(&mut self.upper_arena, full_path);
2804
2805            match find_result {
2806                // Blinded — request a proof regardless of update type.
2807                SeekResult::Blinded => {
2808                    let logical_len = cursor.head_logical_branch_path_len(&self.upper_arena);
2809                    let parent = ProofV2TargetParent::new(logical_len);
2810                    trace!(target: TRACE_TARGET, ?key, ?parent, "Update hit blinded node, requesting proof");
2811                    proof_required_fn(key, parent);
2812                    updates.insert(key, update.clone());
2813                }
2814                // Subtrie — forward all consecutive updates under this subtrie's prefix.
2815                SeekResult::RevealedSubtrie => {
2816                    let child_idx = cursor.head().expect("cursor is non-empty").index;
2817                    let subtrie_root_path = cursor.head_path();
2818
2819                    let subtrie_start = update_idx;
2820                    while update_idx < sorted.len() &&
2821                        sorted[update_idx].1.starts_with(&subtrie_root_path)
2822                    {
2823                        update_idx += 1;
2824                    }
2825
2826                    let subtrie_updates = &sorted[subtrie_start..update_idx];
2827
2828                    // Edge-case: if all updates are removals that could empty the
2829                    // subtrie and collapse the parent onto a blinded sibling, request
2830                    // a proof for the sibling and skip the subtrie's updates.
2831                    if let Some(proof) = Self::check_subtrie_collapse_needs_proof(
2832                        &self.upper_arena,
2833                        &cursor,
2834                        subtrie_updates,
2835                    ) {
2836                        trace!(target: TRACE_TARGET, proof_key = ?proof.key, proof_parent = ?proof.parent, "Subtrie collapse would need blinded sibling, requesting proof");
2837                        proof_required_fn(proof.key, proof.parent);
2838                        for &(key, _, ref update) in subtrie_updates {
2839                            updates.insert(key, update.clone());
2840                        }
2841                        // Pop the subtrie entry before continuing.
2842                        continue;
2843                    }
2844
2845                    let num_subtrie_updates = update_idx - subtrie_start;
2846
2847                    // If all updates are removals and could empty the subtrie,
2848                    // force inline processing so the upper-arena collapse logic
2849                    // can detect blinded siblings and request proofs.
2850                    let all_removals = subtrie_updates
2851                        .iter()
2852                        // Filter out Touched, as they don't affect the structure of the trie. So an
2853                        // update set with 2 removals and one Touched could still result in an empty
2854                        // sub trie.
2855                        .filter(|(_, _, u)| u.is_changed())
2856                        .all(|(_, _, u)| matches!(u, LeafUpdate::Changed(v) if v.is_empty()));
2857                    let subtrie_num_leaves = match &self.upper_arena[child_idx] {
2858                        ArenaSparseNode::Subtrie(s) => s.num_leaves,
2859                        _ => 0,
2860                    };
2861                    let might_empty_subtrie =
2862                        all_removals && num_subtrie_updates as u64 >= subtrie_num_leaves;
2863
2864                    if (num_subtrie_updates >= threshold || parallelize_distributed_updates) &&
2865                        !might_empty_subtrie
2866                    {
2867                        // Take subtrie for parallel update.
2868                        trace!(target: TRACE_TARGET, ?subtrie_root_path, num_subtrie_updates, "Taking subtrie for parallel update");
2869                        let ArenaSparseNode::Subtrie(subtrie) = mem::replace(
2870                            &mut self.upper_arena[child_idx],
2871                            ArenaSparseNode::TakenSubtrie,
2872                        ) else {
2873                            unreachable!()
2874                        };
2875                        taken.push((child_idx, subtrie, subtrie_start..update_idx));
2876                    } else {
2877                        // Update inline.
2878                        trace!(target: TRACE_TARGET, ?subtrie_root_path, num_subtrie_updates, "Updating subtrie inline");
2879                        let ArenaSparseNode::Subtrie(subtrie) = &mut self.upper_arena[child_idx]
2880                        else {
2881                            unreachable!()
2882                        };
2883
2884                        subtrie.update_leaves(subtrie_updates);
2885
2886                        for (target_idx, proof) in subtrie.required_proofs.drain(..) {
2887                            proof_required_fn(proof.key, proof.parent);
2888                            let (key, _, ref update) = subtrie_updates[target_idx];
2889                            updates.insert(key, update.clone());
2890                        }
2891
2892                        // Check if the subtrie's root became empty after updates.
2893                        self.maybe_unwrap_subtrie(&mut cursor);
2894                    }
2895
2896                    // Don't increment update_idx — already advanced past subtrie updates.
2897                    continue;
2898                }
2899                // EmptyRoot, leaf, diverged branch, or empty child slot — upsert directly.
2900                find_result @ (SeekResult::EmptyRoot |
2901                SeekResult::RevealedLeaf |
2902                SeekResult::Diverged |
2903                SeekResult::NoChild { .. }) => match update {
2904                    LeafUpdate::Changed(v) if !v.is_empty() => {
2905                        let (result, _deltas) = Self::upsert_leaf(
2906                            &mut self.upper_arena,
2907                            &mut cursor,
2908                            &mut self.root,
2909                            full_path,
2910                            v,
2911                            find_result,
2912                        );
2913                        match result {
2914                            UpsertLeafResult::NewChild => {
2915                                if Self::should_be_subtrie(cursor.head_path_len()) {
2916                                    // The new child itself sits at the subtrie
2917                                    // boundary — wrap it directly.
2918                                    let head_idx =
2919                                        cursor.head().expect("cursor is non-empty").index;
2920                                    self.maybe_wrap_in_subtrie(head_idx, &cursor.head_path());
2921                                } else {
2922                                    // The new child is above the boundary (e.g. a
2923                                    // split at depth 1 creates children at depth 2).
2924                                    // Wrap any of its children that land there.
2925                                    self.maybe_wrap_branch_children(&cursor);
2926                                }
2927                            }
2928                            UpsertLeafResult::NewLeaf => {
2929                                // A root-level split may create children at the
2930                                // subtrie boundary depth. Wrap them.
2931                                self.maybe_wrap_branch_children(&cursor);
2932                            }
2933                            UpsertLeafResult::Updated => {}
2934                        }
2935                    }
2936                    LeafUpdate::Changed(_) => {
2937                        let (result, _deltas) = Self::remove_leaf(
2938                            &mut self.upper_arena,
2939                            &mut cursor,
2940                            &mut self.root,
2941                            key,
2942                            full_path,
2943                            find_result,
2944                            &mut self.buffers.updates,
2945                        );
2946                        match result {
2947                            RemoveLeafResult::NeedsProof { key, proof_key, parent } => {
2948                                proof_required_fn(proof_key, parent);
2949                                let update =
2950                                    mem::replace(&mut sorted[update_idx].2, LeafUpdate::Touched);
2951                                updates.insert(key, update);
2952                            }
2953                            RemoveLeafResult::Removed => {
2954                                // remove_leaf may have called collapse_branch, which
2955                                // can leave structural invariants violated:
2956                                // 1. A branch with 0-1 children that needs further collapse or
2957                                //    removal.
2958                                // 2. A Subtrie at a depth shallower than UPPER_TRIE_MAX_DEPTH that
2959                                //    needs unwrapping.
2960                                // 3. A non-Subtrie node at UPPER_TRIE_MAX_DEPTH that needs
2961                                //    wrapping.
2962                                self.maybe_collapse_or_remove_branch(&mut cursor);
2963                                let head_idx = cursor
2964                                    .head()
2965                                    .expect("cursor always has root after collapse")
2966                                    .index;
2967                                self.maybe_wrap_in_subtrie(head_idx, &cursor.head_path());
2968                            }
2969                            RemoveLeafResult::NotFound => {}
2970                        }
2971                    }
2972                    LeafUpdate::Touched => {}
2973                },
2974            }
2975
2976            update_idx += 1;
2977        }
2978
2979        // Drain remaining cursor entries from the upper-trie walk.
2980        cursor.drain(&mut self.upper_arena);
2981        self.buffers.cursor = cursor;
2982
2983        if taken.is_empty() {
2984            #[cfg(all(debug_assertions, not(feature = "fuzz")))]
2985            self.debug_assert_subtrie_structure();
2986
2987            return Ok(());
2988        }
2989
2990        // Apply updates to taken subtries, in parallel if more than one.
2991        if taken.len() == 1 {
2992            let (_, ref mut subtrie, ref range) = taken[0];
2993            subtrie.update_leaves(&sorted[range.clone()]);
2994        } else {
2995            let parent_span = tracing::Span::current();
2996            taken.par_iter_mut().for_each(|(_, subtrie, range)| {
2997                let _guard = parent_span.enter();
2998                subtrie.update_leaves(&sorted[range.clone()]);
2999            });
3000        }
3001
3002        // Collect subtrie paths before consuming `taken`, then restore subtries and
3003        // process required proofs.
3004        let taken_paths: Vec<Nibbles> = taken.iter().map(|(_, s, _)| s.path).collect();
3005        for (child_idx, mut subtrie, range) in taken {
3006            let subtrie_updates = &sorted[range];
3007            for (target_idx, proof) in subtrie.required_proofs.drain(..) {
3008                proof_required_fn(proof.key, proof.parent);
3009                let (key, _, ref update) = subtrie_updates[target_idx];
3010                updates.insert(key, update.clone());
3011            }
3012
3013            // Restore the subtrie into the upper arena.
3014            self.restore_taken_subtrie(child_idx, subtrie);
3015        }
3016
3017        // Navigate to each taken subtrie via seek to propagate dirty state
3018        // through intermediate branches. Taken subtries are guaranteed not to
3019        // become EmptyRoot (the would-empty-subtrie check above forces those
3020        // inline), so we only need to handle sibling collapses that may have
3021        // occurred during inline processing while this subtrie was taken.
3022        {
3023            let mut cursor = mem::take(&mut self.buffers.cursor);
3024            cursor.reset(&self.upper_arena, self.root, Nibbles::default());
3025
3026            for path in &taken_paths {
3027                let find_result = cursor.seek(&mut self.upper_arena, path);
3028                match find_result {
3029                    SeekResult::RevealedSubtrie => {
3030                        debug_assert!(
3031                            {
3032                                let head_idx = cursor.head().expect("cursor is non-empty").index;
3033                                !matches!(
3034                                    &self.upper_arena[head_idx],
3035                                    ArenaSparseNode::Subtrie(s) if matches!(s.arena[s.root], ArenaSparseNode::EmptyRoot { .. })
3036                                )
3037                            },
3038                            "taken subtrie became EmptyRoot — should have been forced inline"
3039                        );
3040
3041                        cursor.pop(&mut self.upper_arena);
3042
3043                        // The parent branch (now at cursor top) may have had a sibling
3044                        // removed during inline processing while this subtrie was taken.
3045                        // Handle any necessary collapse or removal.
3046                        self.maybe_collapse_or_remove_branch(&mut cursor);
3047                    }
3048                    _ => {
3049                        // Subtrie was already unwrapped by a prior collapse; dirty state
3050                        // was propagated during that collapse. Nothing to do.
3051                    }
3052                }
3053            }
3054
3055            cursor.drain(&mut self.upper_arena);
3056            self.buffers.cursor = cursor;
3057        }
3058
3059        #[cfg(all(debug_assertions, not(feature = "fuzz")))]
3060        self.debug_assert_subtrie_structure();
3061
3062        Ok(())
3063    }
3064}
3065
3066#[cfg(test)]
3067mod tests {
3068    use super::*;
3069    use crate::{
3070        ArenaParallelSparseTrie, ArenaParallelismThresholds, LeafUpdate, SparseTrie, TrieNodeEpoch,
3071    };
3072    use alloy_primitives::{map::B256Map, B256, U256};
3073    use rand::{seq::SliceRandom, Rng, SeedableRng};
3074    use reth_trie::test_utils::TrieTestHarness;
3075    use reth_trie_common::ProofV2Target;
3076    use std::collections::BTreeMap;
3077    use tracing::{info, trace};
3078
3079    const fn epoch(value: u64) -> TrieNodeEpoch {
3080        TrieNodeEpoch::new(value)
3081    }
3082
3083    #[test]
3084    fn unsupported_root_preserves_update_retention_and_allows_valid_root() {
3085        let mut trie = ArenaParallelSparseTrie::default();
3086        let root = TrieNodeV2::Extension(reth_trie_common::ExtensionNode::new(
3087            Nibbles::from_nibbles([1]),
3088            RlpNode::word_rlp(&B256::ZERO),
3089        ));
3090        let error = trie.set_root(root, None, true).unwrap_err();
3091        assert!(
3092            matches!(error.kind(), SparseTrieErrorKind::Reveal { path, .. } if path.is_empty())
3093        );
3094        assert!(trie.buffers.updates.is_none());
3095
3096        trie.set_root(TrieNodeV2::EmptyRoot, None, true).unwrap();
3097        assert_eq!(trie.root(epoch(1)), EMPTY_ROOT_HASH);
3098        assert!(trie.buffers.updates.is_some());
3099    }
3100
3101    #[test]
3102    fn pruning_pops_cursor_before_reusing_node_slot() {
3103        use super::{
3104            ArenaCursor, ArenaSparseNode, ArenaSparseNodeBranch, ArenaSparseNodeState, BranchChild,
3105            NextResult, NodeArena,
3106        };
3107        use reth_trie_common::RlpNode;
3108
3109        let mut arena = NodeArena::new();
3110        let rlp = RlpNode::word_rlp(&B256::repeat_byte(1));
3111        let state = ArenaSparseNodeState::Cached { rlp_node: rlp.clone(), epoch: epoch(0) };
3112        let leaf =
3113            ArenaSparseNode::Leaf { state: state.clone(), key: Default::default(), value: vec![1] };
3114        let first = arena.insert(leaf.clone());
3115        let second = arena.insert(leaf);
3116        let root = arena.insert(ArenaSparseNode::Branch(ArenaSparseNodeBranch {
3117            state: state.clone(),
3118            children: [BranchChild::revealed(first), BranchChild::revealed(second)]
3119                .into_iter()
3120                .collect(),
3121            state_mask: alloy_trie::TrieMask::new(3),
3122            branch_masks: Default::default(),
3123        }));
3124        let mut cursor = ArenaCursor::default();
3125        cursor.reset(&arena, root, Default::default());
3126        assert!(matches!(cursor.next(&mut arena, |_, _| true), NextResult::NonBranch));
3127        assert_eq!(cursor.head().unwrap().index, first);
3128
3129        ArenaParallelSparseTrie::remove_pruned_node(&mut arena, &mut cursor);
3130        assert_eq!(cursor.head().unwrap().index, root);
3131        let reused = arena.insert(ArenaSparseNode::Leaf {
3132            state: ArenaSparseNodeState::Dirty,
3133            key: Default::default(),
3134            value: vec![2],
3135        });
3136        assert_eq!(reused, first);
3137
3138        assert!(matches!(cursor.next(&mut arena, |_, _| true), NextResult::NonBranch));
3139        assert_eq!(cursor.head().unwrap().index, second);
3140        assert!(matches!(cursor.next(&mut arena, |_, _| true), NextResult::Branch));
3141        assert_eq!(arena[root].state_ref(), Some(&state));
3142        assert_eq!(arena.blinded(arena[root].branch_ref().children[0]), &rlp);
3143        assert!(matches!(cursor.next(&mut arena, |_, _| true), NextResult::Done));
3144    }
3145
3146    /// Test harness for proptest-based arena sparse trie testing.
3147    ///
3148    /// Wraps [`TrieTestHarness`] and adds `ArenaParallelSparseTrie`-specific helpers for
3149    /// the reveal-update loop and asserting that sparse trie updates match `StorageRoot`.
3150    struct ArenaTrieTestHarness {
3151        /// The inner general-purpose harness.
3152        inner: TrieTestHarness,
3153    }
3154
3155    impl std::ops::Deref for ArenaTrieTestHarness {
3156        type Target = TrieTestHarness;
3157        fn deref(&self) -> &Self::Target {
3158            &self.inner
3159        }
3160    }
3161
3162    impl std::ops::DerefMut for ArenaTrieTestHarness {
3163        fn deref_mut(&mut self) -> &mut Self::Target {
3164            &mut self.inner
3165        }
3166    }
3167
3168    impl ArenaTrieTestHarness {
3169        /// Creates a new test harness from a map of hashed storage slots to values.
3170        fn new(storage: BTreeMap<B256, U256>) -> Self {
3171            Self { inner: TrieTestHarness::new(storage) }
3172        }
3173
3174        /// Computes the new storage root and trie updates after applying the given changes
3175        /// using both `StorageRoot` and the provided `ArenaParallelSparseTrie`, then asserts
3176        /// they match.
3177        fn assert_changes(
3178            &self,
3179            apst: &mut ArenaParallelSparseTrie,
3180            changes: BTreeMap<B256, U256>,
3181        ) {
3182            // Compute expected root and trie updates via StorageRoot.
3183            let (expected_root, mut expected_trie_updates) = if changes.is_empty() {
3184                (self.original_root(), Default::default())
3185            } else {
3186                self.get_root_with_updates(&changes)
3187            };
3188
3189            self.minimize_trie_updates(&mut expected_trie_updates);
3190
3191            // Build leaf updates for the APST: non-zero values are upserts (RLP-encoded),
3192            // zero values are deletions (empty vec).
3193            let mut leaf_updates: B256Map<LeafUpdate> = changes
3194                .iter()
3195                .map(|(&slot, &value)| {
3196                    let rlp_value = if value.is_zero() {
3197                        Vec::new()
3198                    } else {
3199                        alloy_rlp::encode_fixed_size(&value).to_vec()
3200                    };
3201                    (slot, LeafUpdate::Changed(rlp_value))
3202                })
3203                .collect();
3204
3205            // Reveal-update loop: call update_leaves, collect required proofs, fetch them,
3206            // reveal, and repeat until no more proofs are needed.
3207            loop {
3208                let mut targets: Vec<ProofV2Target> = Vec::new();
3209                apst.update_leaves(&mut leaf_updates, |key, parent| {
3210                    targets.push(ProofV2Target::new(key).with_parent(parent));
3211                })
3212                .expect("update_leaves should succeed");
3213
3214                if targets.is_empty() {
3215                    break;
3216                }
3217
3218                let (mut proof_nodes, _) = self.proof_v2(&mut targets);
3219                apst.reveal_nodes(&mut proof_nodes).expect("reveal_nodes should succeed");
3220            }
3221
3222            // Compute root and take updates from the APST.
3223            let actual_root = apst.root(epoch(0));
3224            let mut actual_updates = apst.take_updates();
3225
3226            // Minimize sparse updates inline (can't use TrieTestHarness::minimize_sparse_updates
3227            // due to the crate's SparseTrieUpdates being a different type than reth-trie's copy).
3228            actual_updates.updated_nodes.retain(|path, node| {
3229                self.storage_trie_updates().storage_nodes.get(path) != Some(node)
3230            });
3231            actual_updates
3232                .removed_nodes
3233                .retain(|path| self.storage_trie_updates().storage_nodes.contains_key(path));
3234
3235            let mut expected_updated_nodes =
3236                expected_trie_updates.storage_nodes.into_iter().collect::<Vec<_>>();
3237            let mut actual_updated_nodes =
3238                actual_updates.updated_nodes.into_iter().collect::<Vec<_>>();
3239            expected_updated_nodes.sort();
3240            actual_updated_nodes.sort();
3241            pretty_assertions::assert_eq!(
3242                expected_updated_nodes,
3243                actual_updated_nodes,
3244                "updated nodes mismatch"
3245            );
3246
3247            let mut expected_removed_nodes =
3248                expected_trie_updates.removed_nodes.into_iter().collect::<Vec<_>>();
3249            let mut actual_removed_nodes =
3250                actual_updates.removed_nodes.into_iter().collect::<Vec<_>>();
3251            expected_removed_nodes.sort();
3252            actual_removed_nodes.sort();
3253            pretty_assertions::assert_eq!(
3254                expected_removed_nodes,
3255                actual_removed_nodes,
3256                "removed nodes mismatch"
3257            );
3258            assert_eq!(expected_root, actual_root, "storage root mismatch");
3259        }
3260    }
3261
3262    use proptest::prelude::*;
3263    use proptest_arbitrary_interop::arb;
3264
3265    /// Builds a changeset by mixing `new_keys` (fresh insertions) with a fraction of
3266    /// existing keys from `base` (updates/deletions).
3267    ///
3268    /// `overlap_pct` controls how many existing keys are included, and `delete_pct`
3269    /// controls how many of those become deletions (zero values). The remaining
3270    /// overlap keys get random non-zero values.
3271    fn build_changeset(
3272        base: &BTreeMap<B256, U256>,
3273        new_keys: BTreeMap<B256, U256>,
3274        overlap_pct: f64,
3275        delete_pct: f64,
3276        rng: &mut rand::rngs::StdRng,
3277    ) -> BTreeMap<B256, U256> {
3278        let num_overlap = (base.len() as f64 * overlap_pct) as usize;
3279        let num_delete = (num_overlap as f64 * delete_pct) as usize;
3280
3281        let mut all_keys: Vec<B256> = base.keys().copied().collect();
3282        all_keys.shuffle(rng);
3283        let overlap_keys = &all_keys[..num_overlap];
3284
3285        let mut changeset = new_keys;
3286        for (i, &key) in overlap_keys.iter().enumerate() {
3287            let value =
3288                if i < num_delete { U256::ZERO } else { U256::from(rng.random::<u64>() | 1) };
3289            changeset.entry(key).or_insert(value);
3290        }
3291        changeset
3292    }
3293
3294    proptest! {
3295        #![proptest_config(ProptestConfig::with_cases(1000))]
3296        #[test]
3297        fn arena_trie_proptest(
3298            initial in proptest::collection::btree_map(arb::<B256>(), arb::<U256>(), 0..=100usize),
3299            changeset1_new_keys in proptest::collection::btree_map(arb::<B256>(), arb::<U256>(), 0..=30usize),
3300            changeset2_new_keys in proptest::collection::btree_map(arb::<B256>(), arb::<U256>(), 0..=30usize),
3301            overlap_pct in 0.0..=0.5f64,
3302            delete_pct in 0.0..=0.33f64, // percent of overlapping changeset which are deletes
3303            shuffle_seed in arb::<u64>(),
3304        ) {
3305            reth_tracing::init_test_tracing();
3306            info!(target: TRACE_TARGET, ?shuffle_seed, "PROPTEST START");
3307
3308            // Filter out zero-valued entries from the initial dataset (zeros mean "absent").
3309            let initial: BTreeMap<B256, U256> = initial.into_iter()
3310                .filter(|(_, v)| !v.is_zero())
3311                .collect();
3312
3313            let mut rng = rand::rngs::StdRng::seed_from_u64(shuffle_seed);
3314
3315            let changeset1 = build_changeset(&initial, changeset1_new_keys, overlap_pct, delete_pct, &mut rng);
3316            for (i, (k, v)) in changeset1.iter().enumerate() {
3317                trace!(target: TRACE_TARGET, ?i, ?k, ?v, "Changeset 1 entry");
3318            }
3319
3320            let mut harness = ArenaTrieTestHarness::new(initial);
3321
3322            // Initialize the APST from the harness root node.
3323            let root_node = harness.root_node();
3324            let mut apst = ArenaParallelSparseTrie::default().with_parallelism_thresholds(
3325                ArenaParallelismThresholds {
3326                    min_dirty_leaves: 3,
3327                    min_revealed_nodes: 3,
3328                    min_updates: 3,
3329                    min_leaves_for_prune: 3,
3330                },
3331            );
3332            apst.set_root(root_node.node, root_node.masks, true).expect("set_root should succeed");
3333
3334            harness.assert_changes(&mut apst, changeset1.clone());
3335
3336            // Update the harness base dataset to reflect the first changeset.
3337            harness.apply_changeset(changeset1);
3338
3339            // All nodes were cached at epoch 0, so this maximally prunes the trie before the
3340            // second update round.
3341            apst.prune(epoch(1));
3342
3343            let changeset2 = build_changeset(harness.storage(), changeset2_new_keys, overlap_pct, delete_pct, &mut rng);
3344            for (i, (k, v)) in changeset2.iter().enumerate() {
3345                trace!(target: TRACE_TARGET, ?i, ?k, ?v, "Changeset 2 entry");
3346            }
3347
3348            harness.assert_changes(&mut apst, changeset2);
3349        }
3350    }
3351}