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