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