Compare commits

...

1 Commits

Author SHA1 Message Date
DaniPopes
06878253fe wip 2026-02-11 18:09:31 -05:00
8 changed files with 375 additions and 39 deletions

19
Cargo.lock generated
View File

@@ -1913,6 +1913,12 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "boxcar"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
[[package]]
name = "boyer-moore-magiclen"
version = "0.2.22"
@@ -5109,6 +5115,18 @@ dependencies = [
"memoffset",
]
[[package]]
name = "inturn"
version = "0.1.2"
source = "git+https://github.com/DaniPopes/inturn?branch=dani%2Fcopy-interner#dfbf54fe098a323d326af9a6ecc8fd052af84587"
dependencies = [
"boxcar",
"bumpalo",
"dashmap",
"hashbrown 0.14.5",
"thread_local",
]
[[package]]
name = "ipconfig"
version = "0.3.2"
@@ -10674,6 +10692,7 @@ dependencies = [
"arbitrary",
"assert_matches",
"auto_impl",
"inturn",
"itertools 0.14.0",
"metrics",
"pretty_assertions",

View File

@@ -519,6 +519,7 @@ fdlimit = "0.3.0"
fixed-map = { version = "0.9", default-features = false }
humantime = "2.1"
humantime-serde = "1.1"
inturn = "0.1"
itertools = { version = "0.14", default-features = false }
linked_hash_set = "0.1"
lz4 = "1.28.1"
@@ -757,3 +758,5 @@ ipnet = "2.11"
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
inturn = { git = "https://github.com/DaniPopes/inturn", branch = "dani/copy-interner" }

View File

@@ -59,6 +59,7 @@ RUN --mount=type=secret,id=DEPOT_TOKEN,env=SCCACHE_WEBDAV_TOKEN \
export RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"; \
fi && \
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin $BINARY --manifest-path $MANIFEST_PATH/Cargo.toml && \
pgrep -a sccache || echo "sccache server is NOT running (likely OOM-killed)" && \
sccache --show-stats
# Copy binary to a known location (ARG not resolved in COPY)

View File

@@ -25,6 +25,7 @@ alloy-rlp.workspace = true
# misc
auto_impl.workspace = true
inturn = { workspace = true, optional = true }
rayon = { workspace = true, optional = true }
smallvec.workspace = true
@@ -53,6 +54,7 @@ rand_08.workspace = true
[features]
default = ["std", "metrics"]
std = [
"dep:inturn",
"dep:rayon",
"alloy-primitives/std",
"alloy-rlp/std",

View File

@@ -0,0 +1,245 @@
//! Interned node storage for the parallel sparse trie.
use alloy_primitives::map::HashMap;
use core::ops;
pub use inturn;
use inturn::{CopyInterner, Symbol};
use reth_trie_common::Nibbles;
use std::sync::Arc;
use crate::SparseNode;
/// A shared path interner backed by [`CopyInterner<Nibbles>`].
///
/// This is a thread-safe, lock-free interner that deduplicates [`Nibbles`] paths across the entire
/// [`crate::ParallelSparseTrie`]. Each unique path is assigned a compact [`Symbol`] index.
/// The interner never frees symbols; it only grows. Node storage lifecycle is managed separately
/// by each [`SparseNodeInterner`] via its `HashMap<Symbol, SparseNode>`.
pub type SharedPathInterner = Arc<CopyInterner<Nibbles>>;
/// Creates a new shared path interner.
pub fn shared_path_interner() -> SharedPathInterner {
Arc::new(CopyInterner::new())
}
/// An interned node store for [`SparseNode`]s keyed by [`Nibbles`] path.
///
/// Paths are interned into compact [`Symbol`]s via a [`SharedPathInterner`] that is shared across
/// the entire [`crate::ParallelSparseTrie`] (upper subtrie + all 256 lower subtries).
/// Nodes are stored in a `HashMap<Symbol, SparseNode>` per subtrie.
pub struct SparseNodeInterner {
/// Shared path interner.
interner: SharedPathInterner,
/// Nodes stored by interned symbol.
nodes: HashMap<Symbol, SparseNode>,
}
impl core::fmt::Debug for SparseNodeInterner {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_map().entries(self.iter()).finish()
}
}
impl Clone for SparseNodeInterner {
fn clone(&self) -> Self {
Self { interner: Arc::clone(&self.interner), nodes: self.nodes.clone() }
}
}
impl PartialEq for SparseNodeInterner {
fn eq(&self, other: &Self) -> bool {
if self.nodes.len() != other.nodes.len() {
return false;
}
for (path, node) in self.iter() {
match other.get(path) {
Some(other_node) if node == other_node => {}
_ => return false,
}
}
true
}
}
impl Eq for SparseNodeInterner {}
impl SparseNodeInterner {
/// Creates an empty interner backed by the given shared path interner.
pub fn new(interner: SharedPathInterner) -> Self {
Self { interner, nodes: HashMap::default() }
}
/// Returns a reference to the shared path interner.
pub const fn interner(&self) -> &SharedPathInterner {
&self.interner
}
/// Returns the number of live entries.
pub fn len(&self) -> usize {
self.nodes.len()
}
/// Returns `true` if there are no live entries.
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
/// Reserves capacity for at least `additional` more entries.
pub fn reserve(&mut self, additional: usize) {
self.nodes.reserve(additional);
}
/// Shrinks the capacity of the underlying storage.
pub fn shrink_to(&mut self, min_capacity: usize) {
self.nodes.shrink_to(min_capacity);
}
/// Gets a reference to the node at the given path.
pub fn get(&self, path: &Nibbles) -> Option<&SparseNode> {
let sym = self.interner.intern(path);
self.nodes.get(&sym)
}
/// Gets a mutable reference to the node at the given path.
pub fn get_mut(&mut self, path: &Nibbles) -> Option<&mut SparseNode> {
let sym = self.interner.intern(path);
self.nodes.get_mut(&sym)
}
/// Returns `true` if the path has a live node.
pub fn contains_key(&self, path: &Nibbles) -> bool {
let sym = self.interner.intern(path);
self.nodes.contains_key(&sym)
}
/// Inserts a node at the given path, returning the previous node if any.
pub fn insert(&mut self, path: Nibbles, node: SparseNode) -> Option<SparseNode> {
let sym = self.interner.intern(&path);
self.nodes.insert(sym, node)
}
/// Removes the node at the given path, returning it if it existed.
pub fn remove(&mut self, path: &Nibbles) -> Option<SparseNode> {
let sym = self.interner.intern(path);
self.nodes.remove(&sym)
}
/// Provides entry API similar to [`HashMap::entry`].
pub fn entry(&mut self, path: Nibbles) -> Entry<'_> {
let sym = self.interner.intern(&path);
match self.nodes.entry(sym) {
alloy_primitives::map::Entry::Occupied(e) => {
Entry::Occupied(OccupiedEntry { path, entry: e })
}
alloy_primitives::map::Entry::Vacant(e) => {
Entry::Vacant(VacantEntry { path, entry: e })
}
}
}
/// Retains only the entries for which the predicate returns `true`.
pub fn retain(&mut self, mut f: impl FnMut(&Nibbles, &mut SparseNode) -> bool) {
let interner = &self.interner;
self.nodes.retain(|sym, node| {
let path = interner.resolve(*sym);
f(path, node)
});
}
/// Clears all entries, keeping allocations.
pub fn clear(&mut self) {
self.nodes.clear();
}
/// Iterates over all live `(path, node)` pairs.
pub fn iter(&self) -> impl Iterator<Item = (&Nibbles, &SparseNode)> {
self.nodes.iter().map(|(sym, node)| (self.interner.resolve(*sym), node))
}
/// Returns a heuristic for the in-memory size in bytes.
pub fn memory_size(&self) -> usize {
let mut size = core::mem::size_of::<Self>();
for (path, node) in self.iter() {
size += core::mem::size_of::<Symbol>() + core::mem::size_of::<SparseNode>();
size += path.len();
size += node.memory_size();
}
size
}
}
/// Entry API for [`SparseNodeInterner`].
#[allow(missing_debug_implementations)]
pub enum Entry<'a> {
/// An occupied entry.
Occupied(OccupiedEntry<'a>),
/// A vacant entry.
Vacant(VacantEntry<'a>),
}
/// An occupied entry in the [`SparseNodeInterner`].
#[allow(missing_debug_implementations)]
pub struct OccupiedEntry<'a> {
path: Nibbles,
entry: alloy_primitives::map::OccupiedEntry<'a, Symbol, SparseNode>,
}
impl<'a> OccupiedEntry<'a> {
/// Returns a reference to the key.
pub const fn key(&self) -> &Nibbles {
&self.path
}
/// Returns a reference to the value.
pub fn get(&self) -> &SparseNode {
self.entry.get()
}
/// Returns a mutable reference to the value.
pub fn get_mut(&mut self) -> &mut SparseNode {
self.entry.get_mut()
}
/// Replaces the value, returning the old one.
pub fn insert(&mut self, node: SparseNode) -> SparseNode {
self.entry.insert(node)
}
/// Converts into a mutable reference to the value.
pub fn into_mut(self) -> &'a mut SparseNode {
self.entry.into_mut()
}
}
impl ops::Deref for OccupiedEntry<'_> {
type Target = SparseNode;
fn deref(&self) -> &Self::Target {
self.entry.get()
}
}
impl ops::DerefMut for OccupiedEntry<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.entry.get_mut()
}
}
/// A vacant entry in the [`SparseNodeInterner`].
#[allow(missing_debug_implementations)]
pub struct VacantEntry<'a> {
path: Nibbles,
entry: alloy_primitives::map::VacantEntry<'a, Symbol, SparseNode>,
}
impl<'a> VacantEntry<'a> {
/// Returns a reference to the key.
pub const fn key(&self) -> &Nibbles {
&self.path
}
/// Inserts a value and returns a mutable reference to it.
pub fn insert(self, node: SparseNode) -> &'a mut SparseNode {
self.entry.insert(node)
}
}

View File

@@ -19,6 +19,9 @@ pub use parallel::*;
mod lower;
#[cfg(feature = "std")]
pub mod interner;
pub mod provider;
#[cfg(feature = "metrics")]

View File

@@ -47,7 +47,11 @@ impl LowerSparseSubtrie {
/// The given path is the path of a node which will be set into the [`SparseSubtrie`]'s `nodes`
/// map immediately upon being revealed. If the subtrie is blinded, or if its current root path
/// is longer than this one, than this one becomes the new root path of the subtrie.
pub(crate) fn reveal(&mut self, path: &Nibbles) {
pub(crate) fn reveal(
&mut self,
path: &Nibbles,
interner: &crate::interner::SharedPathInterner,
) {
match self {
Self::Blind(allocated) => {
debug_assert!(allocated.as_ref().is_none_or(|subtrie| subtrie.is_empty()));
@@ -55,7 +59,10 @@ impl LowerSparseSubtrie {
subtrie.path = *path;
Self::Revealed(subtrie)
} else {
Self::Revealed(Box::new(SparseSubtrie::new(*path)))
Self::Revealed(Box::new(SparseSubtrie::new(
*path,
std::sync::Arc::clone(interner),
)))
}
}
Self::Revealed(subtrie) => {

View File

@@ -1,4 +1,5 @@
use crate::{
interner::{shared_path_interner, SharedPathInterner},
lower::LowerSparseSubtrie,
provider::{RevealedNode, TrieNodeProvider},
LeafLookup, LeafLookupError, RlpNodeStackItem, SparseNode, SparseNodeType, SparseTrie,
@@ -19,6 +20,7 @@ use reth_trie_common::{
ProofTrieNode, RlpNode, TrieNode,
};
use smallvec::SmallVec;
use std::sync::Arc;
use tracing::{debug, instrument, trace};
/// The maximum length of a path, in nibbles, which belongs to the upper subtrie of a
@@ -101,8 +103,10 @@ pub struct ParallelismThresholds {
/// - Each leaf entry in the `subtries` and `upper_trie` collection must have a corresponding entry
/// in `values` collection. If the root node is a leaf, it must also have an entry in `values`.
/// - All keys in `values` collection are full leaf paths.
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(Clone)]
pub struct ParallelSparseTrie {
/// Shared path interner used across upper and lower subtries.
path_interner: SharedPathInterner,
/// This contains the trie nodes for the upper part of the trie.
upper_subtrie: Box<SparseSubtrie>,
/// An array containing the subtries at the second level of the trie.
@@ -133,11 +137,14 @@ pub struct ParallelSparseTrie {
impl Default for ParallelSparseTrie {
fn default() -> Self {
let path_interner = shared_path_interner();
Self {
upper_subtrie: Box::new(SparseSubtrie {
nodes: HashMap::from_iter([(Nibbles::default(), SparseNode::Empty)]),
..Default::default()
}),
upper_subtrie: {
let mut subtrie = SparseSubtrie::with_interner(Arc::clone(&path_interner));
subtrie.nodes.insert(Nibbles::default(), SparseNode::Empty);
Box::new(subtrie)
},
path_interner,
lower_subtries: Box::new(
[const { LowerSparseSubtrie::Blind(None) }; NUM_LOWER_SUBTRIES],
),
@@ -153,6 +160,36 @@ impl Default for ParallelSparseTrie {
}
}
impl PartialEq for ParallelSparseTrie {
fn eq(&self, other: &Self) -> bool {
self.upper_subtrie == other.upper_subtrie &&
self.lower_subtries == other.lower_subtries &&
self.prefix_set == other.prefix_set &&
self.updates == other.updates &&
self.branch_node_masks == other.branch_node_masks &&
self.update_actions_buffers == other.update_actions_buffers &&
self.parallelism_thresholds == other.parallelism_thresholds &&
self.subtrie_heat == other.subtrie_heat
}
}
impl Eq for ParallelSparseTrie {}
impl core::fmt::Debug for ParallelSparseTrie {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ParallelSparseTrie")
.field("upper_subtrie", &self.upper_subtrie)
.field("lower_subtries", &self.lower_subtries)
.field("prefix_set", &self.prefix_set)
.field("updates", &self.updates)
.field("branch_node_masks", &self.branch_node_masks)
.field("update_actions_buffers", &self.update_actions_buffers)
.field("parallelism_thresholds", &self.parallelism_thresholds)
.field("subtrie_heat", &self.subtrie_heat)
.finish()
}
}
impl SparseTrie for ParallelSparseTrie {
fn set_root(
&mut self,
@@ -242,7 +279,7 @@ impl SparseTrie for ParallelSparseTrie {
);
continue;
}
self.lower_subtries[idx].reveal(&node.path);
self.lower_subtries[idx].reveal(&node.path, &self.path_interner);
self.subtrie_heat.mark_modified(idx);
self.lower_subtries[idx]
.as_revealed_mut()
@@ -302,7 +339,7 @@ impl SparseTrie for ParallelSparseTrie {
// the first element of each group, the `path` here will necessarily be the
// shortest path being revealed for each subtrie. Therefore we can reveal the
// subtrie itself using this path and retain correct behavior.
self.lower_subtries[idx].reveal(&node.path);
self.lower_subtries[idx].reveal(&node.path, &self.path_interner);
Some((idx, self.lower_subtries[idx].take_revealed().expect("just revealed")))
})
.collect();
@@ -1530,7 +1567,7 @@ impl ParallelSparseTrie {
match SparseSubtrieType::from_path(path) {
SparseSubtrieType::Upper => None,
SparseSubtrieType::Lower(idx) => {
self.lower_subtries[idx].reveal(path);
self.lower_subtries[idx].reveal(path, &self.path_interner);
self.subtrie_heat.mark_modified(idx);
Some(self.lower_subtries[idx].as_revealed_mut().expect("just revealed"))
}
@@ -2298,7 +2335,7 @@ impl ParallelSparseTrie {
/// This is used for leaves that sit at the upper/lower subtrie boundary, where the leaf is
/// in a lower subtrie but its parent branch is in the upper subtrie.
fn is_boundary_leaf_reachable(
upper_nodes: &HashMap<Nibbles, SparseNode>,
upper_nodes: &crate::interner::SparseNodeInterner,
path: &Nibbles,
node: &TrieNode,
) -> bool {
@@ -2440,7 +2477,7 @@ impl SubtrieModifications {
/// This is a subtrie of the [`ParallelSparseTrie`] that contains a map from path to sparse trie
/// nodes.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SparseSubtrie {
/// The root path of this subtrie.
///
@@ -2450,8 +2487,8 @@ pub struct SparseSubtrie {
///
/// There should be a node for this path in `nodes` map.
pub(crate) path: Nibbles,
/// The map from paths to sparse trie nodes within this subtrie.
nodes: HashMap<Nibbles, SparseNode>,
/// Interned map from paths to sparse trie nodes within this subtrie.
nodes: crate::interner::SparseNodeInterner,
/// Subset of fields for mutable access while `nodes` field is also being mutably borrowed.
inner: SparseSubtrieInner,
}
@@ -2473,8 +2510,17 @@ enum FindNextToLeafOutcome {
impl SparseSubtrie {
/// Creates a new empty subtrie with the specified root path.
pub(crate) fn new(path: Nibbles) -> Self {
Self { path, ..Default::default() }
pub(crate) fn new(path: Nibbles, interner: SharedPathInterner) -> Self {
Self {
path,
nodes: crate::interner::SparseNodeInterner::new(interner),
inner: SparseSubtrieInner::default(),
}
}
/// Creates a new empty subtrie with the default root path.
pub(crate) fn with_interner(interner: SharedPathInterner) -> Self {
Self::new(Nibbles::default(), interner)
}
/// Returns true if this subtrie has any nodes, false otherwise.
@@ -2840,7 +2886,7 @@ impl SparseSubtrie {
// Update the branch node entry in the nodes map, handling cases where a blinded
// node is now replaced with a revealed node.
match self.nodes.entry(path) {
Entry::Occupied(mut entry) => match entry.get() {
crate::interner::Entry::Occupied(mut entry) => match entry.get() {
// Replace a hash node with a fully revealed branch node.
SparseNode::Hash(hash) => {
entry.insert(SparseNode::Branch {
@@ -2855,7 +2901,7 @@ impl SparseSubtrie {
}
_ => unreachable!("checked that node is either a hash or non-existent"),
},
Entry::Vacant(entry) => {
crate::interner::Entry::Vacant(entry) => {
entry.insert(SparseNode::new_branch(branch.state_mask));
}
}
@@ -2875,7 +2921,7 @@ impl SparseSubtrie {
}
}
TrieNode::Extension(ext) => match self.nodes.entry(path) {
Entry::Occupied(mut entry) => match entry.get() {
crate::interner::Entry::Occupied(mut entry) => match entry.get() {
// Replace a hash node with a revealed extension node.
SparseNode::Hash(hash) => {
let mut child_path = *entry.key();
@@ -2893,7 +2939,7 @@ impl SparseSubtrie {
}
_ => unreachable!("checked that node is either a hash or non-existent"),
},
Entry::Vacant(entry) => {
crate::interner::Entry::Vacant(entry) => {
let mut child_path = *entry.key();
child_path.extend(&ext.key);
entry.insert(SparseNode::new_ext(ext.key));
@@ -2936,7 +2982,7 @@ impl SparseSubtrie {
}
match self.nodes.entry(path) {
Entry::Occupied(mut entry) => match entry.get() {
crate::interner::Entry::Occupied(mut entry) => match entry.get() {
// Replace a hash node with a revealed leaf node and store leaf node value.
SparseNode::Hash(hash) => {
entry.insert(SparseNode::Leaf {
@@ -2948,7 +2994,7 @@ impl SparseSubtrie {
}
_ => unreachable!("checked that node is either a hash or non-existent"),
},
Entry::Vacant(entry) => {
crate::interner::Entry::Vacant(entry) => {
entry.insert(SparseNode::new_leaf(leaf.key));
}
}
@@ -2980,7 +3026,7 @@ impl SparseSubtrie {
if child.len() == B256::len_bytes() + 1 {
let hash = B256::from_slice(&child[1..]);
match self.nodes.entry(path) {
Entry::Occupied(entry) => match entry.get() {
crate::interner::Entry::Occupied(entry) => match entry.get() {
// Hash node with a different hash can't be handled.
SparseNode::Hash(previous_hash) if previous_hash != &hash => {
return Err(SparseTrieErrorKind::Reveal {
@@ -2991,7 +3037,7 @@ impl SparseSubtrie {
}
_ => {}
},
Entry::Vacant(entry) => {
crate::interner::Entry::Vacant(entry) => {
entry.insert(SparseNode::Hash(hash));
}
}
@@ -3058,7 +3104,8 @@ impl SparseSubtrie {
/// Removes all nodes and values from the subtrie, resetting it to a blank state
/// with only an empty root node. This is used when a storage root is deleted.
fn wipe(&mut self) {
self.nodes = HashMap::from_iter([(Nibbles::default(), SparseNode::Empty)]);
self.nodes.clear();
self.nodes.insert(Nibbles::default(), SparseNode::Empty);
self.inner.clear();
}
@@ -3082,12 +3129,8 @@ impl SparseSubtrie {
pub(crate) fn memory_size(&self) -> usize {
let mut size = core::mem::size_of::<Self>();
// Nodes map: key (Nibbles) + value (SparseNode)
for (path, node) in &self.nodes {
size += core::mem::size_of::<Nibbles>();
size += path.len(); // Nibbles heap allocation
size += node.memory_size();
}
// Nodes interner
size += self.nodes.memory_size();
// Values map: key (Nibbles) + value (Vec<u8>)
for (path, value) in &self.inner.values {
@@ -3689,6 +3732,7 @@ mod tests {
SparseSubtrieType,
};
use crate::{
interner::shared_path_interner,
parallel::ChangedSubtrie,
provider::{DefaultTrieNodeProvider, RevealedNode, TrieNodeProvider},
LeafLookup, LeafLookupError, SparseNode, SparseTrie, SparseTrieUpdates,
@@ -3722,7 +3766,10 @@ mod tests {
ProofTrieNode, RlpNode, TrieMask, TrieNode, EMPTY_ROOT_HASH,
};
use reth_trie_db::DatabaseTrieCursorFactory;
use std::collections::{BTreeMap, BTreeSet};
use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
/// Pad nibbles to the length of a B256 hash with zeros on the right.
fn pad_nibbles_right(mut nibbles: Nibbles) -> Nibbles {
@@ -4165,11 +4212,15 @@ mod tests {
fn test_get_changed_subtries() {
// Create a trie with three subtries
let mut trie = ParallelSparseTrie::default();
let subtrie_1 = Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x0, 0x0])));
let interner = shared_path_interner();
let subtrie_1 =
Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x0, 0x0]), Arc::clone(&interner)));
let subtrie_1_index = path_subtrie_index_unchecked(&subtrie_1.path);
let subtrie_2 = Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x1, 0x0])));
let subtrie_2 =
Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x1, 0x0]), Arc::clone(&interner)));
let subtrie_2_index = path_subtrie_index_unchecked(&subtrie_2.path);
let subtrie_3 = Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x3, 0x0])));
let subtrie_3 =
Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x3, 0x0]), Arc::clone(&interner)));
let subtrie_3_index = path_subtrie_index_unchecked(&subtrie_3.path);
// Add subtries at specific positions
@@ -4219,11 +4270,15 @@ mod tests {
fn test_get_changed_subtries_all() {
// Create a trie with three subtries
let mut trie = ParallelSparseTrie::default();
let subtrie_1 = Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x0, 0x0])));
let interner = shared_path_interner();
let subtrie_1 =
Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x0, 0x0]), Arc::clone(&interner)));
let subtrie_1_index = path_subtrie_index_unchecked(&subtrie_1.path);
let subtrie_2 = Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x1, 0x0])));
let subtrie_2 =
Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x1, 0x0]), Arc::clone(&interner)));
let subtrie_2_index = path_subtrie_index_unchecked(&subtrie_2.path);
let subtrie_3 = Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x3, 0x0])));
let subtrie_3 =
Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x3, 0x0]), Arc::clone(&interner)));
let subtrie_3_index = path_subtrie_index_unchecked(&subtrie_3.path);
// Add subtries at specific positions
@@ -4622,7 +4677,8 @@ mod tests {
#[test]
fn test_subtrie_update_hashes() {
let mut subtrie = Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x0, 0x0])));
let mut subtrie =
Box::new(SparseSubtrie::new(Nibbles::from_nibbles([0x0, 0x0]), shared_path_interner()));
// Create leaf nodes with paths 0x0...0, 0x00001...0, 0x0010...0
let leaf_1_full_path = Nibbles::from_nibbles([0; 64]);