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