Skip to main content

reth_trie_sparse/arena/
mod.rs

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