From 6097cf9ee7aba654181f88ae469739dffa4304cc Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Tue, 17 Mar 2026 18:10:23 +0100 Subject: [PATCH] fix(trie): Fix branch collapse edge-cases in ArenaParallelSparseTrie (#23053) Signed-off-by: Delweng Co-authored-by: Amp Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: Alexey Shekhirin <5773434+shekhirin@users.noreply.github.com> Co-authored-by: MagicJoshh Co-authored-by: Delweng Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: Huber Co-authored-by: Sergei Shulepov <2205845+pepyakin@users.noreply.github.com> Co-authored-by: Olivier Dupont Co-authored-by: YK Co-authored-by: Crypto Nomad Co-authored-by: ligt Co-authored-by: Sergei Shulepov --- .changelog/dull-seals-laugh.md | 5 + crates/trie/sparse/src/arena/mod.rs | 42 ++- .../trie/sparse/tests/suite/commit_updates.rs | 8 +- crates/trie/sparse/tests/suite/find_leaf.rs | 28 +- .../trie/sparse/tests/suite/get_leaf_value.rs | 8 +- crates/trie/sparse/tests/suite/lifecycle.rs | 48 +-- crates/trie/sparse/tests/suite/main.rs | 43 ++- crates/trie/sparse/tests/suite/prune.rs | 44 +-- .../trie/sparse/tests/suite/reveal_nodes.rs | 42 +-- crates/trie/sparse/tests/suite/root.rs | 22 +- crates/trie/sparse/tests/suite/set_root.rs | 26 +- crates/trie/sparse/tests/suite/size_hint.rs | 4 +- .../trie/sparse/tests/suite/take_updates.rs | 28 +- .../trie/sparse/tests/suite/update_leaves.rs | 276 ++++++++++++++---- crates/trie/sparse/tests/suite/wipe_clear.rs | 18 +- 15 files changed, 438 insertions(+), 204 deletions(-) create mode 100644 .changelog/dull-seals-laugh.md diff --git a/.changelog/dull-seals-laugh.md b/.changelog/dull-seals-laugh.md new file mode 100644 index 0000000000..0f2d20cffb --- /dev/null +++ b/.changelog/dull-seals-laugh.md @@ -0,0 +1,5 @@ +--- +reth-trie-sparse: minor +--- + +Fixed a bug in `ArenaParallelSparseTrie` where subtrie updates that would completely empty a subtrie were incorrectly dispatched to parallel workers instead of being processed inline, preventing correct branch collapse detection when blinded siblings are present. Refactored the `SparseTrie` test suite to accept a `fn() -> T` factory instead of requiring `T: Default`, enabling a new `arena_parallel_sparse_trie_always_parallel` test variant that exercises all tests with parallelism thresholds set to 1. Added `test_branch_collapse_multi_empty_subtries_blinded_remaining` to cover the case where removing multiple revealed leaves empties their subtries and leaves a single blinded sibling requiring a proof. diff --git a/crates/trie/sparse/src/arena/mod.rs b/crates/trie/sparse/src/arena/mod.rs index 10f8040d1e..cb039ab67b 100644 --- a/crates/trie/sparse/src/arena/mod.rs +++ b/crates/trie/sparse/src/arena/mod.rs @@ -2817,7 +2817,24 @@ impl SparseTrie for ArenaParallelSparseTrie { let num_subtrie_updates = update_idx - subtrie_start; - if num_subtrie_updates >= threshold { + // If all updates are removals and could empty the subtrie, + // force inline processing so the upper-arena collapse logic + // can detect blinded siblings and request proofs. + let all_removals = subtrie_updates + .iter() + // Filter out Touched, as they don't affect the structure of the trie. So an + // update set with 2 removals and one Touched could still result in an empty + // sub trie. + .filter(|(_, _, u)| matches!(u, LeafUpdate::Changed(_))) + .all(|(_, _, u)| matches!(u, LeafUpdate::Changed(v) if v.is_empty())); + let subtrie_num_leaves = match &self.upper_arena[child_idx] { + ArenaSparseNode::Subtrie(s) => s.num_leaves, + _ => 0, + }; + let might_empty_subtrie = + all_removals && num_subtrie_updates as u64 >= subtrie_num_leaves; + + if num_subtrie_updates >= threshold && !might_empty_subtrie { // Take subtrie for parallel update. trace!(target: TRACE_TARGET, ?subtrie_root_path, num_subtrie_updates, "Taking subtrie for parallel update"); let ArenaSparseNode::Subtrie(subtrie) = mem::replace( @@ -2978,7 +2995,10 @@ impl SparseTrie for ArenaParallelSparseTrie { } // Navigate to each taken subtrie via seek to propagate dirty state - // through intermediate branches, and collapse any EmptyRoot subtries. + // through intermediate branches. Taken subtries are guaranteed not to + // become EmptyRoot (the would-empty-subtrie check above forces those + // inline), so we only need to handle sibling collapses that may have + // occurred during inline processing while this subtrie was taken. { let mut cursor = mem::take(&mut self.buffers.cursor); cursor.reset(&self.upper_arena, self.root, Nibbles::default()); @@ -2987,13 +3007,17 @@ impl SparseTrie for ArenaParallelSparseTrie { let find_result = cursor.seek(&mut self.upper_arena, path); match find_result { SeekResult::RevealedSubtrie => { - let head_idx = cursor.head().expect("cursor is non-empty").index; - if let ArenaSparseNode::Subtrie(s) = &self.upper_arena[head_idx] && - matches!(s.arena[s.root], ArenaSparseNode::EmptyRoot) - { - self.maybe_unwrap_subtrie(&mut cursor); - continue; - } + debug_assert!( + { + let head_idx = cursor.head().expect("cursor is non-empty").index; + !matches!( + &self.upper_arena[head_idx], + ArenaSparseNode::Subtrie(s) if matches!(s.arena[s.root], ArenaSparseNode::EmptyRoot) + ) + }, + "taken subtrie became EmptyRoot — should have been forced inline" + ); + cursor.pop(&mut self.upper_arena); // The parent branch (now at cursor top) may have had a sibling diff --git a/crates/trie/sparse/tests/suite/commit_updates.rs b/crates/trie/sparse/tests/suite/commit_updates.rs index da1917201d..1417780571 100644 --- a/crates/trie/sparse/tests/suite/commit_updates.rs +++ b/crates/trie/sparse/tests/suite/commit_updates.rs @@ -5,7 +5,7 @@ use super::*; /// After calling `commit_updates` with taken updates, a subsequent mutation + `root()` + /// `take_updates()` should only report the delta from the new baseline — it must NOT /// re-report branch nodes from the first round. -pub(super) fn test_commit_updates_syncs_branch_masks() { +pub(super) fn test_commit_updates_syncs_branch_masks(new_trie: fn() -> T) { // 5 leaves spread across different subtrie regions. let mut key_a = B256::ZERO; key_a.0[0] = 0x10; @@ -27,7 +27,7 @@ pub(super) fn test_commit_updates_syncs_branch_masks() ]); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); // Cache initial hashes. let _ = trie.root(); @@ -79,7 +79,7 @@ pub(super) fn test_commit_updates_syncs_branch_masks() /// Committing empty updated/removed sets should not change trie behavior. /// /// Build a trie, compute root, then commit empty updates. Root should be unchanged. -pub(super) fn test_commit_updates_empty_is_noop() { +pub(super) fn test_commit_updates_empty_is_noop(new_trie: fn() -> T) { let mut key_a = B256::ZERO; key_a.0[0] = 0x10; let mut key_b = B256::ZERO; @@ -90,7 +90,7 @@ pub(super) fn test_commit_updates_empty_is_noop() { BTreeMap::from([(key_a, U256::from(1)), (key_b, U256::from(2)), (key_c, U256::from(3))]); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); let hash1 = trie.root(); assert_eq!(hash1, harness.original_root()); diff --git a/crates/trie/sparse/tests/suite/find_leaf.rs b/crates/trie/sparse/tests/suite/find_leaf.rs index 7391f6cbd4..e78ae18d73 100644 --- a/crates/trie/sparse/tests/suite/find_leaf.rs +++ b/crates/trie/sparse/tests/suite/find_leaf.rs @@ -1,6 +1,6 @@ use super::*; -pub(super) fn test_find_leaf_exists() { +pub(super) fn test_find_leaf_exists(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -9,7 +9,7 @@ pub(super) fn test_find_leaf_exists() { [(key1, U256::from(1)), (key2, U256::from(2)), (key3, U256::from(3))].into_iter().collect(); let harness = SuiteTestHarness::new(base_storage); - let trie: T = harness.init_trie_fully_revealed(false); + let trie: T = harness.init_trie_fully_revealed(false, new_trie); let key2_nibbles = Nibbles::unpack(key2); @@ -29,7 +29,7 @@ pub(super) fn test_find_leaf_exists() { ); } -pub(super) fn test_find_leaf_nonexistent() { +pub(super) fn test_find_leaf_nonexistent(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -38,7 +38,7 @@ pub(super) fn test_find_leaf_nonexistent() { [(key1, U256::from(1)), (key2, U256::from(2)), (key3, U256::from(3))].into_iter().collect(); let harness = SuiteTestHarness::new(base_storage); - let trie: T = harness.init_trie_fully_revealed(false); + let trie: T = harness.init_trie_fully_revealed(false, new_trie); let nonexistent_key = B256::with_last_byte(0x99); let nonexistent_nibbles = Nibbles::unpack(nonexistent_key); @@ -52,7 +52,7 @@ pub(super) fn test_find_leaf_nonexistent() { /// `find_leaf` on a path that traverses a blinded node returns /// `Err(LeafLookupError::BlindedNode)`. -pub(super) fn test_find_leaf_blinded() { +pub(super) fn test_find_leaf_blinded(new_trie: fn() -> T) { // Use ≥16 keys per nibble group so branch children become hash nodes (>32 bytes RLP), // ensuring partial reveal leaves blinded subtries. let mut base_storage = BTreeMap::new(); @@ -78,7 +78,7 @@ pub(super) fn test_find_leaf_blinded() { let harness = SuiteTestHarness::new(base_storage); // Reveal only group_a keys, leaving group_b's subtrie blinded. - let trie: T = harness.init_trie_with_targets(&group_a_keys, false); + let trie: T = harness.init_trie_with_targets(&group_a_keys, false, new_trie); // Look up a key in group_b — should hit a blinded node. let blinded_key = group_b_keys[0]; @@ -93,7 +93,7 @@ pub(super) fn test_find_leaf_blinded() { /// `find_leaf` with an expected value that doesn't match the actual leaf value /// returns `Err(LeafLookupError::ValueMismatch)`. -pub(super) fn test_find_leaf_value_mismatch() { +pub(super) fn test_find_leaf_value_mismatch(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -102,7 +102,7 @@ pub(super) fn test_find_leaf_value_mismatch() { [(key1, U256::from(1)), (key2, U256::from(2)), (key3, U256::from(3))].into_iter().collect(); let harness = SuiteTestHarness::new(base_storage); - let trie: T = harness.init_trie_fully_revealed(false); + let trie: T = harness.init_trie_fully_revealed(false, new_trie); let key2_nibbles = Nibbles::unpack(key2); let wrong_value = encode_fixed_size(&U256::from(999)).to_vec(); @@ -119,7 +119,7 @@ pub(super) fn test_find_leaf_value_mismatch() { /// /// Two leaves sharing prefix 0x12 create a branch with children at nibbles 3 and 5. /// Searching for a key with nibble 7 at that branch position should return `NonExistent`. -pub(super) fn test_find_leaf_nonexistent_branch_divergence() { +pub(super) fn test_find_leaf_nonexistent_branch_divergence(new_trie: fn() -> T) { // key1 nibbles: 1,2,3,4,0,0,... → B256 = 0x12340000... let mut key1 = B256::ZERO; key1.0[0] = 0x12; @@ -134,7 +134,7 @@ pub(super) fn test_find_leaf_nonexistent_branch_divergence() { +pub(super) fn test_find_leaf_nonexistent_extension_divergence(new_trie: fn() -> T) { // Single leaf: nibbles [1,2,3,4,5,6,0,0,...] → B256 = 0x12345600... let mut key1 = B256::ZERO; key1.0[0] = 0x12; @@ -165,7 +165,7 @@ pub(super) fn test_find_leaf_nonexistent_extension_divergence = once((key1, U256::from(1))).collect(); let harness = SuiteTestHarness::new(base_storage); - let trie: T = harness.init_trie_fully_revealed(false); + let trie: T = harness.init_trie_fully_revealed(false, new_trie); // Search path diverges from the extension: nibbles [1,2,7,8,0,0,...] → B256 = 0x12780000... let mut search_key = B256::ZERO; @@ -185,7 +185,7 @@ pub(super) fn test_find_leaf_nonexistent_extension_divergence() { +pub(super) fn test_find_leaf_nonexistent_leaf_divergence(new_trie: fn() -> T) { // Existing leaf at short path: nibbles [1,2,3,4,0,0,...] → B256 = 0x12340000... let mut existing_key = B256::ZERO; existing_key.0[0] = 0x12; @@ -194,7 +194,7 @@ pub(super) fn test_find_leaf_nonexistent_leaf_divergence = once((existing_key, U256::from(1))).collect(); let harness = SuiteTestHarness::new(base_storage); - let trie: T = harness.init_trie_fully_revealed(false); + let trie: T = harness.init_trie_fully_revealed(false, new_trie); // Search path extends past the existing leaf: nibbles [1,2,3,4,5,6,0,0,...] // → B256 = 0x12345600... diff --git a/crates/trie/sparse/tests/suite/get_leaf_value.rs b/crates/trie/sparse/tests/suite/get_leaf_value.rs index b073f8e2ba..a9873e72e3 100644 --- a/crates/trie/sparse/tests/suite/get_leaf_value.rs +++ b/crates/trie/sparse/tests/suite/get_leaf_value.rs @@ -2,7 +2,7 @@ use super::*; /// After inserting or updating a leaf via `update_leaves`, `get_leaf_value` /// should return the new value. -pub(super) fn test_get_leaf_value_after_update() { +pub(super) fn test_get_leaf_value_after_update(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -11,7 +11,7 @@ pub(super) fn test_get_leaf_value_after_update() { [(key1, U256::from(1)), (key2, U256::from(2)), (key3, U256::from(3))].into_iter().collect(); let harness = SuiteTestHarness::new(base_storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); // Insert a new leaf (key4) with value 42. let key4 = B256::with_last_byte(0x40); @@ -42,7 +42,7 @@ pub(super) fn test_get_leaf_value_after_update() { } /// After removing a leaf via `update_leaves`, `get_leaf_value` should return `None`. -pub(super) fn test_get_leaf_value_after_removal() { +pub(super) fn test_get_leaf_value_after_removal(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -51,7 +51,7 @@ pub(super) fn test_get_leaf_value_after_removal() { [(key1, U256::from(1)), (key2, U256::from(2)), (key3, U256::from(3))].into_iter().collect(); let harness = SuiteTestHarness::new(base_storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); let key2_nibbles = Nibbles::unpack(key2); let expected_value_rlp = encode_fixed_size(&U256::from(2)).to_vec(); diff --git a/crates/trie/sparse/tests/suite/lifecycle.rs b/crates/trie/sparse/tests/suite/lifecycle.rs index 7d33a2e2a8..d6ff6efee0 100644 --- a/crates/trie/sparse/tests/suite/lifecycle.rs +++ b/crates/trie/sparse/tests/suite/lifecycle.rs @@ -5,7 +5,7 @@ use super::*; /// Build a trie with enough leaves to produce hashed branch children (≥16 per subtrie), /// insert 1 new leaf + modify 1 existing, compute root, take updates, commit, then verify /// root is unchanged (cache hit) and updates are non-empty. -pub(super) fn test_full_lifecycle_update_root_take_commit() { +pub(super) fn test_full_lifecycle_update_root_take_commit(new_trie: fn() -> T) { // 16 leaves sharing prefix [1,0] to produce hashed branch children. let mut storage: BTreeMap = BTreeMap::new(); for i in 0u8..16 { @@ -20,7 +20,7 @@ pub(super) fn test_full_lifecycle_update_root_take_commit() { +pub(super) fn test_multi_round_update_commit_prune_cycle(new_trie: fn() -> T) { // Build a trie with 10 leaves. let mut storage: BTreeMap = BTreeMap::new(); let mut keys = Vec::new(); @@ -78,7 +78,7 @@ pub(super) fn test_multi_round_update_commit_prune_cycle() { +pub(super) fn test_reveal_update_root_basic_lifecycle(new_trie: fn() -> T) { let mut keys = Vec::new(); let mut storage: BTreeMap = BTreeMap::new(); for i in 0u8..5 { @@ -157,7 +157,7 @@ pub(super) fn test_reveal_update_root_basic_lifecycle() } let harness = SuiteTestHarness::new(storage.clone()); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); // Apply 2 modifications and 1 removal. let mut changeset: BTreeMap = BTreeMap::new(); @@ -185,7 +185,7 @@ pub(super) fn test_reveal_update_root_basic_lifecycle() /// Incremental reveal and update with retry loop. /// Partial proof → `update_leaves` hits blinded nodes → reveal more → retry succeeds. -pub(super) fn test_incremental_reveal_and_update_with_retry() { +pub(super) fn test_incremental_reveal_and_update_with_retry(new_trie: fn() -> T) { // Build 10 leaves across multiple subtries so partial reveal leaves some blinded. // Use 16 keys per group so branch children become hash nodes (>32 bytes RLP). let mut base_storage = BTreeMap::new(); @@ -211,7 +211,7 @@ pub(super) fn test_incremental_reveal_and_update_with_retry = BTreeMap::new(); @@ -269,7 +269,7 @@ pub(super) fn test_incremental_reveal_and_update_with_retry() { +pub(super) fn test_full_block_processing_lifecycle(new_trie: fn() -> T) { // --- Setup: Build account trie with 5 accounts --- // A1 storage: 5 slots, A2 storage: 3 slots, A3-A5: empty storage. // Account trie leaf values = RLP-encoded storage roots. @@ -318,9 +318,9 @@ pub(super) fn test_full_block_processing_lifecycle() { let mut acct_harness = SuiteTestHarness::new(acct_storage.clone()); // Initialize all tries fully revealed with update tracking. - let mut a1_trie: T = a1_harness.init_trie_fully_revealed(true); - let mut a2_trie: T = a2_harness.init_trie_fully_revealed(true); - let mut acct_trie: T = acct_harness.init_trie_fully_revealed(true); + let mut a1_trie: T = a1_harness.init_trie_fully_revealed(true, new_trie); + let mut a2_trie: T = a2_harness.init_trie_fully_revealed(true, new_trie); + let mut acct_trie: T = acct_harness.init_trie_fully_revealed(true, new_trie); // Cache initial hashes for all tries. let _ = a1_trie.root(); @@ -406,7 +406,7 @@ pub(super) fn test_full_block_processing_lifecycle() { /// `Touched` is used to prewarm accounts/slots before actual state changes arrive. /// When the real `Changed` update arrives, it overwrites the `Touched` entry. /// This test verifies that prewarming followed by mutation works correctly. -pub(super) fn test_touched_prewarm_then_changed_update() { +pub(super) fn test_touched_prewarm_then_changed_update(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -424,7 +424,7 @@ pub(super) fn test_touched_prewarm_then_changed_update( .collect(); let mut harness = SuiteTestHarness::new(base_storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); // Step 1: Prewarm 3 keys with Touched — all should be drained (paths are revealed). let mut leaf_updates: B256Map = @@ -468,9 +468,9 @@ pub(super) fn test_touched_prewarm_then_changed_update( /// A `Touched` update hits a blinded node, triggering a proof request. After the proof is /// revealed, a `Changed` update for the same key succeeds. This is the prewarm-miss → /// reveal → update sequence. -pub(super) fn test_touched_on_blinded_triggers_proof_then_changed_succeeds< - T: SparseTrie + Default, ->() { +pub(super) fn test_touched_on_blinded_triggers_proof_then_changed_succeeds( + new_trie: fn() -> T, +) { // Two groups of 16 keys each to create blinded subtries. let mut base_storage = BTreeMap::new(); @@ -493,7 +493,7 @@ pub(super) fn test_touched_on_blinded_triggers_proof_then_changed_succeeds< let mut harness = SuiteTestHarness::new(base_storage); // Reveal only group A — group B's subtrie is blinded. - let mut trie: T = harness.init_trie_with_targets(&group_a_keys, false); + let mut trie: T = harness.init_trie_with_targets(&group_a_keys, false, new_trie); // Step 1: Touched on a key in group B's blinded subtrie → callback fires. let target_key = group_b_keys[0]; @@ -540,7 +540,7 @@ pub(super) fn test_touched_on_blinded_triggers_proof_then_changed_succeeds< /// Simulates the `SparseStateTrie::update_account` pattern: read existing leaf via /// `get_leaf_value`, decode, modify (change one field while preserving another), re-encode, /// and update. Verifies that root matches reference. -pub(super) fn test_get_leaf_value_for_storage_root_lookup() { +pub(super) fn test_get_leaf_value_for_storage_root_lookup(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -551,7 +551,7 @@ pub(super) fn test_get_leaf_value_for_storage_root_lookup() { +pub(super) fn test_find_leaf_before_update_to_check_existence(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -599,7 +599,7 @@ pub(super) fn test_find_leaf_before_update_to_check_existence() { +pub(super) fn test_prune_then_reuse_for_next_block(new_trie: fn() -> T) { // Build a trie with 10 leaves. let mut storage: BTreeMap = BTreeMap::new(); let mut keys = Vec::new(); @@ -662,7 +662,7 @@ pub(super) fn test_prune_then_reuse_for_next_block() { } let mut harness = SuiteTestHarness::new(storage.clone()); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); // Cache initial hashes. let _ = trie.root(); diff --git a/crates/trie/sparse/tests/suite/main.rs b/crates/trie/sparse/tests/suite/main.rs index 63ccf8e003..3622301499 100644 --- a/crates/trie/sparse/tests/suite/main.rs +++ b/crates/trie/sparse/tests/suite/main.rs @@ -1,7 +1,7 @@ //! Generic `SparseTrie` test suite. //! -//! Tests are written as generic functions `test_foo()` and stamped out -//! for every concrete implementation via the [`sparse_trie_tests`] macro. +//! Tests are written as generic functions `test_foo(new_trie: fn() -> T)` and +//! stamped out for every concrete implementation via the [`sparse_trie_tests`] macro. //! //! Tests are organized into modules by which `SparseTrie` method is the most likely root cause //! of failure for each test case: @@ -117,13 +117,14 @@ impl SuiteTestHarness { /// Initializes a trie with the harness root node and reveals all proof nodes for the /// given target keys. Returns the initialized trie. - fn init_trie_with_targets( + fn init_trie_with_targets( &self, target_keys: &[B256], retain_updates: bool, + new_trie: fn() -> T, ) -> T { let root_node = self.root_node(); - let mut trie = T::default(); + let mut trie = (new_trie)(); trie.set_root(root_node.node, root_node.masks, retain_updates) .expect("set_root should succeed"); @@ -138,9 +139,13 @@ impl SuiteTestHarness { } /// Initializes a trie and reveals proofs for all keys in the base storage. - fn init_trie_fully_revealed(&self, retain_updates: bool) -> T { + fn init_trie_fully_revealed( + &self, + retain_updates: bool, + new_trie: fn() -> T, + ) -> T { let keys: Vec = self.storage().keys().copied().collect(); - self.init_trie_with_targets(&keys, retain_updates) + self.init_trie_with_targets(&keys, retain_updates, new_trie) } } @@ -158,7 +163,7 @@ macro_rules! sparse_trie_tests { $( #[test] fn $test_fn() { - super::$test_fn::(); + super::$test_fn(ParallelSparseTrie::default); } )* } @@ -169,7 +174,27 @@ macro_rules! sparse_trie_tests { $( #[test] fn $test_fn() { - super::$test_fn::(); + super::$test_fn(ArenaParallelSparseTrie::default); + } + )* + } + + mod arena_parallel_sparse_trie_always_parallel { + use reth_trie_sparse::{ArenaParallelSparseTrie, ArenaParallelismThresholds}; + + $( + #[test] + fn $test_fn() { + super::$test_fn(|| { + ArenaParallelSparseTrie::default().with_parallelism_thresholds( + ArenaParallelismThresholds { + min_dirty_leaves: 1, + min_revealed_nodes: 1, + min_updates: 1, + min_leaves_for_prune: 1, + }, + ) + }); } )* } @@ -245,6 +270,8 @@ sparse_trie_tests! { test_orphaned_value_update_falls_through_to_full_insertion, test_branch_collapse_updates_leaf_key_len_across_subtries, test_remove_leaf_does_not_reveal_blind_subtries, + test_branch_collapse_multi_empty_subtries_blinded_remaining, + test_subtrie_emptied_by_deletes_with_touched, // root test_root_empty_trie, diff --git a/crates/trie/sparse/tests/suite/prune.rs b/crates/trie/sparse/tests/suite/prune.rs index 23c62eb0eb..7866a18497 100644 --- a/crates/trie/sparse/tests/suite/prune.rs +++ b/crates/trie/sparse/tests/suite/prune.rs @@ -1,6 +1,6 @@ use super::*; -pub(super) fn test_prune_retains_specified_leaves() { +pub(super) fn test_prune_retains_specified_leaves(new_trie: fn() -> T) { let mut key_a = B256::ZERO; key_a.0[0] = 0x10; let mut key_b = B256::ZERO; @@ -21,7 +21,7 @@ pub(super) fn test_prune_retains_specified_leaves() { ]); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); // Compute root before prune. let hash1 = trie.root(); @@ -47,7 +47,7 @@ pub(super) fn test_prune_retains_specified_leaves() { /// Build a trie with 10+ leaves spread across multiple subtries, fully reveal /// and compute root. Then prune retaining only 1 leaf. `size_hint()` must /// decrease and `prune` must return > 0. -pub(super) fn test_prune_reduces_node_count() { +pub(super) fn test_prune_reduces_node_count(new_trie: fn() -> T) { // Create 16 keys with different first nibbles to spread across subtries. let keys: Vec = (0u8..16) .map(|i| { @@ -61,7 +61,7 @@ pub(super) fn test_prune_reduces_node_count() { keys.iter().enumerate().map(|(i, k)| (*k, U256::from(i + 1))).collect(); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); // Compute root to cache hashes (required for pruning). let _root = trie.root(); @@ -83,7 +83,7 @@ pub(super) fn test_prune_reduces_node_count() { /// Pruning with an empty retained set should convert all subtrees to /// hash stubs (maximum pruning). Root hash must be unchanged. -pub(super) fn test_prune_empty_retained_set() { +pub(super) fn test_prune_empty_retained_set(new_trie: fn() -> T) { let keys: Vec = (0u8..16) .map(|i| { let mut k = B256::ZERO; @@ -96,7 +96,7 @@ pub(super) fn test_prune_empty_retained_set() { keys.iter().enumerate().map(|(i, k)| (*k, U256::from(i + 1))).collect(); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); let hash_before = trie.root(); @@ -116,7 +116,7 @@ pub(super) fn test_prune_empty_retained_set() { ); } -pub(super) fn test_prune_requires_computed_hashes() { +pub(super) fn test_prune_requires_computed_hashes(new_trie: fn() -> T) { let keys: Vec = (0u8..5) .map(|i| { let mut k = B256::ZERO; @@ -129,7 +129,7 @@ pub(super) fn test_prune_requires_computed_hashes() { keys.iter().enumerate().map(|(i, k)| (*k, U256::from(i + 1))).collect(); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); // Dirty the trie by updating a leaf — do NOT call root() to compute hashes. let mut leaf_updates: B256Map = B256Map::default(); @@ -142,7 +142,7 @@ pub(super) fn test_prune_requires_computed_hashes() { // Compare against pruning after root() is called (clean state). // With dirty nodes, pruning is limited because dirty subtrees lack cached hashes. - let mut trie_clean: T = harness.init_trie_fully_revealed(false); + let mut trie_clean: T = harness.init_trie_fully_revealed(false, new_trie); trie_clean.root(); let clean_pruned = trie_clean.prune(&retained); @@ -152,7 +152,7 @@ pub(super) fn test_prune_requires_computed_hashes() { ); } -pub(super) fn test_prune_then_update_and_recompute_root() { +pub(super) fn test_prune_then_update_and_recompute_root(new_trie: fn() -> T) { let keys: Vec = (0u8..5) .map(|i| { let mut k = B256::ZERO; @@ -165,7 +165,7 @@ pub(super) fn test_prune_then_update_and_recompute_root keys.iter().enumerate().map(|(i, k)| (*k, U256::from(i + 1))).collect(); let harness = SuiteTestHarness::new(storage.clone()); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); trie.root(); @@ -187,7 +187,7 @@ pub(super) fn test_prune_then_update_and_recompute_root assert_eq!(root_after, expected_root, "root after prune + update should match reference trie"); } -pub(super) fn test_prune_then_reveal_pruned_subtree() { +pub(super) fn test_prune_then_reveal_pruned_subtree(new_trie: fn() -> T) { let keys: Vec = (0u8..5) .map(|i| { let mut k = B256::ZERO; @@ -200,7 +200,7 @@ pub(super) fn test_prune_then_reveal_pruned_subtree() { keys.iter().enumerate().map(|(i, k)| (*k, U256::from(i + 1))).collect(); let harness = SuiteTestHarness::new(storage.clone()); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); trie.root(); @@ -227,7 +227,7 @@ pub(super) fn test_prune_then_reveal_pruned_subtree() { /// Pruning a trie with both large (hashed) and small (embedded) node values /// should preserve the root hash. -pub(super) fn test_prune_mixed_embedded_and_hashed_nodes() { +pub(super) fn test_prune_mixed_embedded_and_hashed_nodes(new_trie: fn() -> T) { let mut storage = BTreeMap::new(); // 4 keys with large values (produce hashed nodes: RLP ≥ 32 bytes) @@ -243,7 +243,7 @@ pub(super) fn test_prune_mixed_embedded_and_hashed_nodes() { +pub(super) fn test_prune_then_update_no_panic(new_trie: fn() -> T) { // Build a trie with 64 leaves (16 keys × 4 first-nibble groups). let mut storage = BTreeMap::new(); for group in 0..4u8 { @@ -271,7 +271,7 @@ pub(super) fn test_prune_then_update_no_panic() { } let harness = SuiteTestHarness::new(storage.clone()); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); let root_before_prune = trie.root(); @@ -297,19 +297,19 @@ pub(super) fn test_prune_then_update_no_panic() { /// When the root is not a branch (e.g., a single /// leaf or empty root), `prune` should immediately return 0 without walking. -pub(super) fn test_prune_only_descends_into_branch_root() { +pub(super) fn test_prune_only_descends_into_branch_root(new_trie: fn() -> T) { // Single-leaf trie: root is a leaf node, not a branch. let storage: BTreeMap = BTreeMap::from([(B256::with_last_byte(0x10), U256::from(1))]); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); let _root = trie.root(); let pruned = trie.prune(&[]); assert_eq!(pruned, 0, "non-branch root should not prune any nodes"); // Empty root: also not a branch. - let mut empty_trie = T::default(); + let mut empty_trie = (new_trie)(); let pruned_empty = empty_trie.prune(&[]); assert_eq!(pruned_empty, 0, "empty root should not prune any nodes"); } @@ -317,7 +317,7 @@ pub(super) fn test_prune_only_descends_into_branch_root /// Small subtrie root nodes (RLP < 32 bytes) are /// handled correctly during prune. After `root()` + `prune()`, a subsequent `root()` /// still returns the same hash. -pub(super) fn test_prune_handles_small_subtrie_root_nodes() { +pub(super) fn test_prune_handles_small_subtrie_root_nodes(new_trie: fn() -> T) { // Build a trie with two groups of leaves to create a branch root with mixed // subtrie sizes: // - Group A (nibble 0x1): 16 leaves with large values → hashable subtrie root (RLP ≥ 32 bytes) @@ -335,7 +335,7 @@ pub(super) fn test_prune_handles_small_subtrie_root_nodes() { +pub(super) fn test_reveal_nodes_empty_slice(new_trie: fn() -> T) { // Set up a trie with a root node. let mut key_a = B256::ZERO; key_a.0[0] = 0x10; @@ -15,7 +15,7 @@ pub(super) fn test_reveal_nodes_empty_slice() { let harness = SuiteTestHarness::new(storage); let root_node = harness.root_node(); - let mut trie = T::default(); + let mut trie = (new_trie)(); trie.set_root(root_node.node, root_node.masks, true).expect("set_root should succeed"); let root_before = trie.root(); @@ -31,7 +31,7 @@ pub(super) fn test_reveal_nodes_empty_slice() { /// /// Revealing a single leaf node within a branch should make it accessible and /// produce correct root hashes. -pub(super) fn test_reveal_nodes_single_leaf() { +pub(super) fn test_reveal_nodes_single_leaf(new_trie: fn() -> T) { let mut key_a = B256::ZERO; key_a.0[0] = 0x10; let mut key_b = B256::ZERO; @@ -44,7 +44,7 @@ pub(super) fn test_reveal_nodes_single_leaf() { let harness = SuiteTestHarness::new(storage); // Set root and reveal only one leaf's proof. - let mut trie: T = harness.init_trie_with_targets(&[key_a], true); + let mut trie: T = harness.init_trie_with_targets(&[key_a], true, new_trie); let root = trie.root(); assert_eq!(root, harness.original_root()); } @@ -53,7 +53,7 @@ pub(super) fn test_reveal_nodes_single_leaf() { /// /// Revealing the same proof nodes twice should not corrupt the trie or change /// the root hash. The second reveal is a no-op. -pub(super) fn test_reveal_nodes_idempotent() { +pub(super) fn test_reveal_nodes_idempotent(new_trie: fn() -> T) { let mut key_a = B256::ZERO; key_a.0[0] = 0x10; let mut key_b = B256::ZERO; @@ -66,7 +66,7 @@ pub(super) fn test_reveal_nodes_idempotent() { let harness = SuiteTestHarness::new(storage); // First reveal: set root and reveal all proof nodes. - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); let root_first = trie.root(); assert_eq!(root_first, harness.original_root()); @@ -85,7 +85,7 @@ pub(super) fn test_reveal_nodes_idempotent() { /// Branch node masks provided during reveal should be stored and used for update tracking. /// After modifying a leaf and computing the root, `take_updates()` should contain entries /// reflecting which branch nodes were updated vs removed, guided by the stored masks. -pub(super) fn test_reveal_nodes_with_branch_masks() { +pub(super) fn test_reveal_nodes_with_branch_masks(new_trie: fn() -> T) { // Build a trie with 16 leaves sharing first nibble 0x1 to produce non-root branch nodes // with hashed children (needed for masks to produce InsertUpdated actions). let mut storage: BTreeMap = BTreeMap::new(); @@ -99,7 +99,7 @@ pub(super) fn test_reveal_nodes_with_branch_masks() { let harness = SuiteTestHarness::new(storage); // Initialize trie with masks (from proofs) and retain_updates=true. - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); // Compute root to cache initial branch hashes. let _ = trie.root(); @@ -129,7 +129,7 @@ pub(super) fn test_reveal_nodes_with_branch_masks() { /// /// Calling `reveal_nodes` when the root is `EmptyRoot` should return `Ok(())` without /// modifying trie state, even when non-empty proof nodes are provided. -pub(super) fn test_reveal_nodes_skips_on_empty_root() { +pub(super) fn test_reveal_nodes_skips_on_empty_root(new_trie: fn() -> T) { // Build a harness with real data so we can obtain non-trivial proof nodes. let storage: BTreeMap = BTreeMap::from([ (B256::with_last_byte(1), U256::from(10)), @@ -142,7 +142,7 @@ pub(super) fn test_reveal_nodes_skips_on_empty_root() { let (mut proof_nodes, _) = harness.proof_v2(&mut targets); // Create a trie with an empty root. - let mut trie = T::default(); + let mut trie = (new_trie)(); trie.set_root(TrieNodeV2::EmptyRoot, None, true).expect("set_root EmptyRoot should succeed"); // Reveal non-empty proof nodes — should be a no-op on an empty root. @@ -161,7 +161,9 @@ pub(super) fn test_reveal_nodes_skips_on_empty_root() { /// When `reveal_nodes` receives proof nodes that include entries not reachable from the /// current trie root (e.g., boundary leaves for unrelated subtries), those nodes should /// be silently skipped without corrupting state. -pub(super) fn test_reveal_nodes_filters_unreachable_boundary_leaves() { +pub(super) fn test_reveal_nodes_filters_unreachable_boundary_leaves( + new_trie: fn() -> T, +) { // Create a trie with two groups of keys under different first nibbles. // Group A: 3 keys under nibble 0x1 // Group B: 3 keys under nibble 0x2 @@ -192,7 +194,7 @@ pub(super) fn test_reveal_nodes_filters_unreachable_boundary_leaves = keys_a.iter().map(|k| ProofV2Target::new(*k)).collect(); @@ -237,7 +239,7 @@ pub(super) fn test_reveal_nodes_filters_unreachable_boundary_leaves() { +pub(super) fn test_reveal_insert_reveal_preserves_branch_state(new_trie: fn() -> T) { // Two original keys and one to insert. let key_a = B256::with_last_byte(0x00); let key_b = B256::with_last_byte(0x01); @@ -249,7 +251,7 @@ pub(super) fn test_reveal_insert_reveal_preserves_branch_state() { +pub(super) fn test_remove_then_reveal_does_not_overwrite_collapsed_node( + new_trie: fn() -> T, +) { // Nibbles [0,0,..], [1,1,..], [1,2,..] — root branch has children at nibbles 0 and 1. // Packed into B256 keys: byte 0x00 → nibbles [0,0], byte 0x11 → nibbles [1,1], etc. let key_a = { @@ -302,7 +306,7 @@ pub(super) fn test_remove_then_reveal_does_not_overwrite_collapsed_node = BTreeMap::from([(key_a, U256::ZERO)]); @@ -329,7 +333,9 @@ pub(super) fn test_remove_then_reveal_does_not_overwrite_collapsed_node() { +pub(super) fn test_insert_then_reveal_does_not_overwrite_branch( + new_trie: fn() -> T, +) { // Original trie: keys 0x0001.. and 0x0002.. share prefix 0x00 → extension root. let key_a = { let mut k = B256::ZERO; @@ -350,7 +356,7 @@ pub(super) fn test_insert_then_reveal_does_not_overwrite_branch() { - let mut trie = T::default(); +pub(super) fn test_root_empty_trie(new_trie: fn() -> T) { + let mut trie = (new_trie)(); assert_eq!(trie.root(), EMPTY_ROOT_HASH, "empty trie should return EMPTY_ROOT_HASH"); } @@ -10,7 +10,7 @@ pub(super) fn test_root_empty_trie() { /// /// After fully revealing and computing root once, calling `root()` again without /// mutations should return the same hash and `is_root_cached()` should be true. -pub(super) fn test_root_cached_returns_without_recomputation() { +pub(super) fn test_root_cached_returns_without_recomputation(new_trie: fn() -> T) { let mut key_a = B256::ZERO; key_a.0[0] = 0x10; let mut key_b = B256::ZERO; @@ -21,7 +21,7 @@ pub(super) fn test_root_cached_returns_without_recomputation() { +pub(super) fn test_root_after_single_leaf_update(new_trie: fn() -> T) { let mut key_a = B256::ZERO; key_a.0[0] = 0x10; let mut key_b = B256::ZERO; @@ -47,7 +47,7 @@ pub(super) fn test_root_after_single_leaf_update() { BTreeMap::from([(key_a, U256::from(1)), (key_b, U256::from(2)), (key_c, U256::from(3))]); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); let original_root = trie.root(); assert_eq!(original_root, harness.original_root(), "initial root should match reference"); @@ -75,7 +75,7 @@ pub(super) fn test_root_after_single_leaf_update() { /// /// Two tries built from the same 5 key-value pairs inserted in different orders /// must produce the same root hash. -pub(super) fn test_root_deterministic_across_update_orders() { +pub(super) fn test_root_deterministic_across_update_orders(new_trie: fn() -> T) { // Define 5 key-value pairs spread across different subtrie regions. let mut k1 = B256::ZERO; k1.0[0] = 0x10; @@ -99,7 +99,7 @@ pub(super) fn test_root_deterministic_across_update_orders() { +pub(super) fn test_root_handles_small_root_node_without_hash(new_trie: fn() -> T) { // A single small leaf produces a root node whose RLP is < 32 bytes. let key = B256::with_last_byte(1); let value = U256::from(1); @@ -140,7 +140,7 @@ pub(super) fn test_root_handles_small_root_node_without_hash = BTreeMap::from([(key, value)]); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); let root1 = trie.root(); assert_eq!(root1, harness.original_root(), "first root() should match reference trie"); diff --git a/crates/trie/sparse/tests/suite/set_root.rs b/crates/trie/sparse/tests/suite/set_root.rs index e0710f59e2..cad0d3a2ce 100644 --- a/crates/trie/sparse/tests/suite/set_root.rs +++ b/crates/trie/sparse/tests/suite/set_root.rs @@ -4,7 +4,7 @@ use super::*; /// /// Two leaves whose first nibbles differ produce a branch root node. /// After `set_root` + `reveal_nodes`, `root()` must match the reference hash. -pub(super) fn test_set_root_with_branch_node() { +pub(super) fn test_set_root_with_branch_node(new_trie: fn() -> T) { // Keys whose first nibbles differ → branch at root. let mut key_a = B256::ZERO; key_a.0[0] = 0x10; // first nibble = 1 @@ -14,7 +14,7 @@ pub(super) fn test_set_root_with_branch_node() { BTreeMap::from([(key_a, U256::from(100)), (key_b, U256::from(200))]); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); let root = trie.root(); assert_eq!(root, harness.original_root()); } @@ -23,12 +23,12 @@ pub(super) fn test_set_root_with_branch_node() { /// /// A single key-value pair produces a leaf root node. After `set_root` + `root()`, /// the hash must match the reference trie. -pub(super) fn test_set_root_with_leaf_node() { +pub(super) fn test_set_root_with_leaf_node(new_trie: fn() -> T) { let storage: BTreeMap = BTreeMap::from([(B256::ZERO, U256::from(42))]); let harness = SuiteTestHarness::new(storage); let root_node = harness.root_node(); - let mut trie = T::default(); + let mut trie = (new_trie)(); trie.set_root(root_node.node, root_node.masks, true).expect("set_root should succeed"); let root = trie.root(); assert_eq!(root, harness.original_root()); @@ -38,7 +38,7 @@ pub(super) fn test_set_root_with_leaf_node() { /// /// Two keys sharing a long common prefix produce an extension root node. /// After `set_root` + `reveal_nodes`, `root()` must match the reference hash. -pub(super) fn test_set_root_with_extension_node() { +pub(super) fn test_set_root_with_extension_node(new_trie: fn() -> T) { // Keys that share first byte 0xAB → extension root. let mut key_a = B256::ZERO; key_a.0[0] = 0xAB; @@ -49,7 +49,7 @@ pub(super) fn test_set_root_with_extension_node() { BTreeMap::from([(key_a, U256::from(100)), (key_b, U256::from(200))]); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); let root = trie.root(); assert_eq!(root, harness.original_root()); } @@ -58,7 +58,7 @@ pub(super) fn test_set_root_with_extension_node() { /// /// When `set_root` is called with `retain_updates = true`, subsequent mutations /// should be tracked and `take_updates()` should return non-empty results. -pub(super) fn test_set_root_retains_updates_when_requested() { +pub(super) fn test_set_root_retains_updates_when_requested(new_trie: fn() -> T) { // Build a trie with enough leaves to produce non-root branch nodes with hash children. // We need leaves sharing a prefix nibble so that intermediate branch nodes are created, // and enough entries that children are hashed (RLP ≥ 32 bytes). @@ -71,7 +71,7 @@ pub(super) fn test_set_root_retains_updates_when_requested() { +pub(super) fn test_set_root_does_not_retain_updates_when_not_requested( + new_trie: fn() -> T, +) { let mut key_a = B256::ZERO; key_a.0[0] = 0x10; let mut key_b = B256::ZERO; @@ -113,7 +115,7 @@ pub(super) fn test_set_root_does_not_retain_updates_when_not_requested = BTreeMap::from([(key_a, U256::from(99))]); @@ -135,8 +137,8 @@ pub(super) fn test_set_root_does_not_retain_updates_when_not_requested() { - let mut trie = T::default(); +pub(super) fn test_set_root_with_empty_root(new_trie: fn() -> T) { + let mut trie = (new_trie)(); trie.set_root(TrieNodeV2::EmptyRoot, None, true).expect("set_root should succeed"); assert_eq!(trie.root(), EMPTY_ROOT_HASH); } diff --git a/crates/trie/sparse/tests/suite/size_hint.rs b/crates/trie/sparse/tests/suite/size_hint.rs index bba03a0918..7bce85455e 100644 --- a/crates/trie/sparse/tests/suite/size_hint.rs +++ b/crates/trie/sparse/tests/suite/size_hint.rs @@ -5,7 +5,7 @@ use super::*; /// Builds a 5-leaf trie, records `size_hint`, adds 2 leaves, records again, /// removes 1 leaf, records again. Asserts s2 > s1 and s3 < s2 (monotonic /// relative to leaf count changes). -pub(super) fn test_size_hint_reflects_leaf_count() { +pub(super) fn test_size_hint_reflects_leaf_count(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -26,7 +26,7 @@ pub(super) fn test_size_hint_reflects_leaf_count() { // Include new key targets so proofs cover them. let all_targets = vec![key1, key2, key3, key4, key5, new_key1, new_key2]; - let mut trie: T = harness.init_trie_with_targets(&all_targets, false); + let mut trie: T = harness.init_trie_with_targets(&all_targets, false, new_trie); let s1 = trie.size_hint(); diff --git a/crates/trie/sparse/tests/suite/take_updates.rs b/crates/trie/sparse/tests/suite/take_updates.rs index 0bba8aa9c6..b567f75392 100644 --- a/crates/trie/sparse/tests/suite/take_updates.rs +++ b/crates/trie/sparse/tests/suite/take_updates.rs @@ -1,6 +1,8 @@ use super::*; -pub(super) fn test_take_updates_returns_empty_when_not_tracking() { +pub(super) fn test_take_updates_returns_empty_when_not_tracking( + new_trie: fn() -> T, +) { let mut key_a = B256::ZERO; key_a.0[0] = 0x10; let mut key_b = B256::ZERO; @@ -9,7 +11,7 @@ pub(super) fn test_take_updates_returns_empty_when_not_tracking() { +pub(super) fn test_take_updates_resets_after_take(new_trie: fn() -> T) { let mut storage: BTreeMap = BTreeMap::new(); for i in 0u8..16 { let mut key = B256::ZERO; @@ -30,7 +32,7 @@ pub(super) fn test_take_updates_resets_after_take() { } let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); // Cache initial branch hashes. let _ = trie.root(); @@ -80,7 +82,9 @@ pub(super) fn test_take_updates_resets_after_take() { /// (non-empty `BranchNodeMasks`). After removing one group entirely and modifying the /// other, `take_updates` should report real branches in `removed_nodes` and modified /// branches in `updated_nodes`, with the two sets mutually exclusive. -pub(super) fn test_take_updates_contains_updated_and_removed_nodes() { +pub(super) fn test_take_updates_contains_updated_and_removed_nodes( + new_trie: fn() -> T, +) { // 3-level branching under two groups: // // Group 0x1 (survives, gets modified): @@ -122,7 +126,7 @@ pub(super) fn test_take_updates_contains_updated_and_removed_nodes() { +pub(super) fn test_take_updates_cross_cancellation_across_root_calls( + new_trie: fn() -> T, +) { let val = U256::from(1u64); let mut key_existing = B256::ZERO; @@ -227,7 +233,7 @@ pub(super) fn test_take_updates_cross_cancellation_across_root_calls() { +pub(super) fn test_take_updates_no_duplicate_updated_and_removed_nodes( + new_trie: fn() -> T, +) { // 3 leaves sharing the first nibble → branch at nibble 0x0. let mut key_a = B256::ZERO; key_a.0[0] = 0x00; @@ -283,7 +291,7 @@ pub(super) fn test_take_updates_no_duplicate_updated_and_removed_nodes() { +pub(super) fn test_update_leaves_insert_new_leaf(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -17,7 +17,7 @@ pub(super) fn test_update_leaves_insert_new_leaf() { // Initialize trie with all 3 existing keys revealed, plus the new key target. let all_targets = vec![key1, key2, key3, new_key]; - let mut trie: T = harness.init_trie_with_targets(&all_targets, true); + let mut trie: T = harness.init_trie_with_targets(&all_targets, true, new_trie); // Insert the new leaf. let new_value = U256::from(4); @@ -48,7 +48,7 @@ pub(super) fn test_update_leaves_insert_new_leaf() { /// /// Starting from a 3-leaf trie, changing one leaf's value via `update_leaves` should /// produce a root hash matching a reference trie with the updated value. -pub(super) fn test_update_leaves_modify_existing_leaf() { +pub(super) fn test_update_leaves_modify_existing_leaf(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -57,7 +57,7 @@ pub(super) fn test_update_leaves_modify_existing_leaf() BTreeMap::from([(key1, U256::from(1)), (key2, U256::from(2)), (key3, U256::from(3))]); let harness = SuiteTestHarness::new(base_storage); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); // Modify an existing leaf with a new value. let new_value = U256::from(999); @@ -83,11 +83,11 @@ pub(super) fn test_update_leaves_modify_existing_leaf() /// /// Calling `update_leaves` with one key on a default (empty) trie should produce a root /// hash matching a reference trie with that single leaf. -pub(super) fn test_insert_single_leaf_into_empty_trie() { +pub(super) fn test_insert_single_leaf_into_empty_trie(new_trie: fn() -> T) { let key = B256::with_last_byte(42); let value = U256::from(1); - let mut trie = T::default(); + let mut trie = (new_trie)(); let mut leaf_updates = SuiteTestHarness::leaf_updates(&BTreeMap::from([(key, value)])); // Empty trie has no blinded nodes, so update_leaves should succeed in one call. @@ -112,7 +112,7 @@ pub(super) fn test_insert_single_leaf_into_empty_trie() /// /// All 256 keys are inserted in a single `update_leaves` call. The root must match /// a reference trie and `take_updates()` must return non-empty results. -pub(super) fn test_insert_multiple_leaves_into_empty_trie() { +pub(super) fn test_insert_multiple_leaves_into_empty_trie(new_trie: fn() -> T) { // Build 256 keys with alternating prefix patterns (matching original test). let storage: BTreeMap = (0..=255u8) .map(|b| { @@ -123,7 +123,7 @@ pub(super) fn test_insert_multiple_leaves_into_empty_trie() { +pub(super) fn test_update_all_leaves_with_new_values(new_trie: fn() -> T) { // Build 256 keys with alternating prefix patterns. let keys: Vec = (0..=255u8) .map(|b| if b % 2 == 0 { B256::repeat_byte(b) } else { B256::with_last_byte(b) }) @@ -164,7 +164,7 @@ pub(super) fn test_update_all_leaves_with_new_values() let expected_old = SuiteTestHarness::new(old_storage.clone()); let expected_new = SuiteTestHarness::new(new_storage.clone()); - let mut trie = T::default(); + let mut trie = (new_trie)(); trie.set_updates(true); // Insert all 256 keys with old values. @@ -194,14 +194,16 @@ pub(super) fn test_update_all_leaves_with_new_values() /// Insert key `0x50..` then key `0x51..` (adjacent first-byte keys that share first nibble `5`), /// computing root after each. The final root must match the reference trie with both keys. /// `take_updates()` should return empty since no branch masks were set. -pub(super) fn test_two_leaves_at_adjacent_keys_root_correctness() { +pub(super) fn test_two_leaves_at_adjacent_keys_root_correctness( + new_trie: fn() -> T, +) { let mut key_50 = B256::ZERO; key_50.0[0] = 0x50; let mut key_51 = B256::ZERO; key_51.0[0] = 0x51; let value = U256::from(1); - let mut trie = T::default(); + let mut trie = (new_trie)(); trie.set_updates(true); // Insert first leaf and compute root. @@ -234,7 +236,7 @@ pub(super) fn test_two_leaves_at_adjacent_keys_root_correctness() { +pub(super) fn test_update_leaves_remove_leaf(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -243,7 +245,7 @@ pub(super) fn test_update_leaves_remove_leaf() { BTreeMap::from([(key1, U256::from(1)), (key2, U256::from(2)), (key3, U256::from(3))]); let harness = SuiteTestHarness::new(base_storage); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); // Remove key2 by setting its value to U256::ZERO (produces LeafUpdate::Changed(vec![])). let mut leaf_updates = SuiteTestHarness::leaf_updates(&BTreeMap::from([(key2, U256::ZERO)])); @@ -267,7 +269,7 @@ pub(super) fn test_update_leaves_remove_leaf() { /// extension. Three leaves sharing prefix `0x5` create a branch at nibble 5; removing one /// child should collapse the structure. The root hash must match a reference trie with the /// remaining two leaves. -pub(super) fn test_remove_leaf_branch_collapses_to_extension() { +pub(super) fn test_remove_leaf_branch_collapses_to_extension(new_trie: fn() -> T) { // Keys sharing prefix 0x5: two share 0x50 (children at 0x502..) and one at 0x53. // This creates a branch at nibble 5 with children at nibbles 0 and 3. let mut key_50231 = B256::ZERO; @@ -291,7 +293,7 @@ pub(super) fn test_remove_leaf_branch_collapses_to_extension() { +pub(super) fn test_remove_leaf_branch_collapses_to_leaf(new_trie: fn() -> T) { // Two leaves with different first nibbles → branch root. let key_a = B256::with_last_byte(0x10); // first nibble = 1 let key_b = B256::with_last_byte(0x20); // first nibble = 2 @@ -321,7 +323,7 @@ pub(super) fn test_remove_leaf_branch_collapses_to_leaf BTreeMap::from([(key_a, U256::from(100)), (key_b, U256::from(200))]); let harness = SuiteTestHarness::new(base_storage); - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); // Compute root to cache hashes, take and commit updates to establish baseline masks. let _ = trie.root(); @@ -357,12 +359,12 @@ pub(super) fn test_remove_leaf_branch_collapses_to_leaf /// Removing the only leaf in a trie should produce /// `EMPTY_ROOT_HASH`. -pub(super) fn test_remove_last_leaf_produces_empty_root() { +pub(super) fn test_remove_last_leaf_produces_empty_root(new_trie: fn() -> T) { let key = B256::with_last_byte(0x12); let base_storage: BTreeMap = BTreeMap::from([(key, U256::from(1))]); let harness = SuiteTestHarness::new(base_storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); // Remove the only leaf. let mut leaf_updates = SuiteTestHarness::leaf_updates(&BTreeMap::from([(key, U256::ZERO)])); @@ -374,7 +376,7 @@ pub(super) fn test_remove_last_leaf_produces_empty_root /// Build 6 leaves then remove one-by-one, verifying root at each /// step against a reference trie. Final removal produces `EMPTY_ROOT_HASH`. -pub(super) fn test_insert_then_remove_sequence() { +pub(super) fn test_insert_then_remove_sequence(new_trie: fn() -> T) { // Helper: build a B256 key from a nibble prefix, zero-padded. let key_from_nibbles = |nibbles: &[u8]| -> B256 { let mut bytes = [0u8; 32]; @@ -402,7 +404,7 @@ pub(super) fn test_insert_then_remove_sequence() { let base_storage: BTreeMap = all_keys.iter().map(|&k| (k, val)).collect(); let mut harness = SuiteTestHarness::new(base_storage.clone()); - let mut trie = T::default(); + let mut trie = (new_trie)(); let mut leaf_updates = SuiteTestHarness::leaf_updates(&base_storage); harness.reveal_and_update(&mut trie, &mut leaf_updates); @@ -436,7 +438,7 @@ pub(super) fn test_insert_then_remove_sequence() { /// After computing `root()` (which caches hashes on all nodes), attempting to remove a key /// that doesn't exist should leave the cache intact so the next `root()` call returns the /// same hash without recomputation. -pub(super) fn test_remove_nonexistent_leaf_preserves_hashes() { +pub(super) fn test_remove_nonexistent_leaf_preserves_hashes(new_trie: fn() -> T) { let key_a = B256::with_last_byte(0x10); let key_b = B256::with_last_byte(0x20); let key_c = B256::with_last_byte(0x30); @@ -445,7 +447,7 @@ pub(super) fn test_remove_nonexistent_leaf_preserves_hashes() { +pub(super) fn test_update_leaves_blinded_node_requests_proof(new_trie: fn() -> T) { // Use enough keys under two different first nibbles so that branch children become // hash nodes (>32 bytes RLP). This ensures partial reveal leaves blinded subtries. let mut base_storage = BTreeMap::new(); @@ -494,7 +496,7 @@ pub(super) fn test_update_leaves_blinded_node_requests_proof() { +pub(super) fn test_update_leaves_retry_after_reveal(new_trie: fn() -> T) { // Same setup as blinded_node_requests_proof: two groups of 16 keys each under // different first nibbles, so branch children become hash nodes. let mut base_storage = BTreeMap::new(); @@ -542,7 +544,7 @@ pub(super) fn test_update_leaves_retry_after_reveal() { let harness = SuiteTestHarness::new(base_storage.clone()); // Reveal only group_a keys, leaving group_b's subtrie blinded. - let mut trie: T = harness.init_trie_with_targets(&group_a_keys, false); + let mut trie: T = harness.init_trie_with_targets(&group_a_keys, false, new_trie); // Modify a key in group_b's blinded subtrie. let target_key = group_b_keys[0]; @@ -585,7 +587,7 @@ pub(super) fn test_update_leaves_retry_after_reveal() { ); } -pub(super) fn test_remove_leaf_blinded_sibling_requires_reveal() { +pub(super) fn test_remove_leaf_blinded_sibling_requires_reveal(new_trie: fn() -> T) { // Build a branch with two children: one revealed leaf at nibble 0x1, and a blinded // subtrie at nibble 0x2 (16 keys so it becomes a hash node > 32 bytes). let mut base_storage = BTreeMap::new(); @@ -608,7 +610,7 @@ pub(super) fn test_remove_leaf_blinded_sibling_requires_reveal() { +pub(super) fn test_update_leaves_removal_branch_collapse_blinded_sibling( + new_trie: fn() -> T, +) { // Branch: nibble 0x1 = one revealed leaf, nibble 0x2 = 16 blinded keys (hash node). let mut base_storage = BTreeMap::new(); @@ -671,7 +673,7 @@ pub(super) fn test_update_leaves_removal_branch_collapse_blinded_sibling< let harness = SuiteTestHarness::new(base_storage); // Reveal only the leaf at nibble 0x1, leaving nibble 0x2 blinded. - let mut trie: T = harness.init_trie_with_targets(&[revealed_key], false); + let mut trie: T = harness.init_trie_with_targets(&[revealed_key], false, new_trie); // Snapshot state before the removal attempt. let revealed_path = Nibbles::unpack(revealed_key); @@ -712,7 +714,9 @@ pub(super) fn test_update_leaves_removal_branch_collapse_blinded_sibling< /// When removals in a subtrie would empty it and collapse the parent branch onto /// a blinded sibling, `update_leaves` should detect this and request a proof for /// the blinded sibling via the callback, deferring the updates. -pub(super) fn test_update_leaves_subtrie_collapse_requests_proof() { +pub(super) fn test_update_leaves_subtrie_collapse_requests_proof( + new_trie: fn() -> T, +) { // Build a branch with two children: // nibble 0x1 → a subtrie with 2 revealed leaves // nibble 0x2 → 16 blinded keys (hash node > 32 bytes) @@ -742,7 +746,8 @@ pub(super) fn test_update_leaves_subtrie_collapse_requests_proof() { +pub(super) fn test_update_leaves_multiple_keys_same_blinded_node( + new_trie: fn() -> T, +) { // Branch: nibble 0x1 = 16 revealed keys (hash node), nibble 0x2 = 16 blinded keys. let mut base_storage = BTreeMap::new(); @@ -791,7 +798,7 @@ pub(super) fn test_update_leaves_multiple_keys_same_blinded_node = (0u8..3) @@ -816,7 +823,7 @@ pub(super) fn test_update_leaves_multiple_keys_same_blinded_node() { +pub(super) fn test_update_leaves_touched_fully_revealed(new_trie: fn() -> T) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -825,7 +832,7 @@ pub(super) fn test_update_leaves_touched_fully_revealed [(key1, U256::from(1)), (key2, U256::from(2)), (key3, U256::from(3))].into_iter().collect(); let harness = SuiteTestHarness::new(base_storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); let root_before = trie.root(); @@ -848,7 +855,9 @@ pub(super) fn test_update_leaves_touched_fully_revealed /// `LeafUpdate::Touched` on a path with a blinded node should /// invoke the callback and keep the key in the updates map. No trie mutation should occur. -pub(super) fn test_update_leaves_touched_blinded_requests_proof() { +pub(super) fn test_update_leaves_touched_blinded_requests_proof( + new_trie: fn() -> T, +) { // Two groups of 16 keys each under different first nibbles so that branch children // become hash nodes (>32 bytes RLP). Partial reveal leaves one subtrie blinded. let mut base_storage = BTreeMap::new(); @@ -870,7 +879,7 @@ pub(super) fn test_update_leaves_touched_blinded_requests_proof() { - let mut trie = T::default(); +pub(super) fn test_update_leaves_touched_nonexistent_key(new_trie: fn() -> T) { + let mut trie = (new_trie)(); let target_key = B256::with_last_byte(42); let mut leaf_updates: B256Map = once((target_key, LeafUpdate::Touched)).collect(); @@ -924,7 +933,9 @@ pub(super) fn test_update_leaves_touched_nonexistent_key() { +pub(super) fn test_update_leaves_touched_nonexistent_in_populated_trie( + new_trie: fn() -> T, +) { let key1 = B256::with_last_byte(0x10); let key2 = B256::with_last_byte(0x20); let key3 = B256::with_last_byte(0x30); @@ -933,7 +944,7 @@ pub(super) fn test_update_leaves_touched_nonexistent_in_populated_trie() { +pub(super) fn test_update_leaves_multiple_mixed_updates(new_trie: fn() -> T) { let key_a = B256::with_last_byte(0x10); // will be inserted (new key) let key_b = B256::with_last_byte(0x20); // will be modified let key_c = B256::with_last_byte(0x30); // will be removed @@ -980,7 +991,7 @@ pub(super) fn test_update_leaves_multiple_mixed_updates // Fully reveal existing trie, plus proof for key_a (new key to be inserted). let all_keys = vec![key_a, key_b, key_c, key_d, key_e]; - let mut trie: T = harness.init_trie_with_targets(&all_keys, false); + let mut trie: T = harness.init_trie_with_targets(&all_keys, false, new_trie); // Build mixed leaf updates. let new_value_a = U256::from(100); @@ -1027,7 +1038,9 @@ pub(super) fn test_update_leaves_multiple_mixed_updates /// not just those that previously had a cached hash. This test inserts leaves without /// calling `root()` (so no hashes are cached), then removes a leaf and verifies /// `root()` returns the correct hash. -pub(super) fn test_remove_leaf_marks_ancestors_dirty_unconditionally() { +pub(super) fn test_remove_leaf_marks_ancestors_dirty_unconditionally( + new_trie: fn() -> T, +) { // Create a trie with 5 leaves. let mut keys = Vec::new(); let mut storage: BTreeMap = BTreeMap::new(); @@ -1042,7 +1055,7 @@ pub(super) fn test_remove_leaf_marks_ancestors_dirty_unconditionally = keys.iter().map(|k| ProofV2Target::new(*k)).collect(); @@ -1082,9 +1095,9 @@ pub(super) fn test_remove_leaf_marks_ancestors_dirty_unconditionally() { +pub(super) fn test_orphaned_value_update_falls_through_to_full_insertion( + new_trie: fn() -> T, +) { // Create a trie with 3 leaves sharing a branch prefix, plus 2 additional leaves // in different subtries. Keys chosen so removal of key_c collapses the branch // at the shared prefix. @@ -1128,7 +1141,7 @@ pub(super) fn test_orphaned_value_update_falls_through_to_full_insertion< .collect(); let mut harness = SuiteTestHarness::new(initial_storage.clone()); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); // Insert all leaves. let mut insert_updates = SuiteTestHarness::leaf_updates(&initial_storage); @@ -1185,7 +1198,9 @@ pub(super) fn test_orphaned_value_update_falls_through_to_full_insertion< /// When removing a leaf causes a branch to collapse at a subtrie /// boundary, the remaining sibling leaf's `key_len` metadata must be updated. After the /// collapse the remaining leaf must be findable, updatable, and contribute to the correct root. -pub(super) fn test_branch_collapse_updates_leaf_key_len_across_subtries() { +pub(super) fn test_branch_collapse_updates_leaf_key_len_across_subtries( + new_trie: fn() -> T, +) { // Create two leaves that share a branch at a subtrie boundary. // Keys share the same first nibble (0x1) so they form a branch one level down, // which is a subtrie boundary for the parallel sparse trie. @@ -1196,7 +1211,7 @@ pub(super) fn test_branch_collapse_updates_leaf_key_len_across_subtries = once((key_a, U256::ZERO)).collect(); @@ -1239,7 +1254,7 @@ pub(super) fn test_branch_collapse_updates_leaf_key_len_across_subtries() { +pub(super) fn test_remove_leaf_does_not_reveal_blind_subtries(new_trie: fn() -> T) { // Create a trie with 10 leaves across different first-nibble subtries. // We'll prune some subtries, then remove a leaf whose branch collapse // involves a sibling in a pruned (blinded) subtrie. @@ -1253,7 +1268,7 @@ pub(super) fn test_remove_leaf_does_not_reveal_blind_subtries( + new_trie: fn() -> T, +) { + // Three keys sharing first nibble 0xd, differing at second nibble. + let key_d7 = { + let mut key = B256::ZERO; + key.0[0] = 0xd7; + key + }; + let key_d8 = { + let mut key = B256::ZERO; + key.0[0] = 0xd8; + key + }; + let key_dd = { + let mut key = B256::ZERO; + key.0[0] = 0xdd; + key + }; + + let base_storage: BTreeMap = + BTreeMap::from([(key_d7, U256::from(1)), (key_d8, U256::from(2)), (key_dd, U256::from(3))]); + + let harness = SuiteTestHarness::new(base_storage); + + // Reveal only 0xd7 and 0xdd, leaving 0xd8's subtrie blinded. + let mut trie: T = harness.init_trie_with_targets(&[key_d7, key_dd], false, new_trie); + + // Remove both revealed leaves — their subtries empty, branch collapses to + // single child (0xd8) which is blinded. + let mut leaf_updates = SuiteTestHarness::leaf_updates(&BTreeMap::from([ + (key_d7, U256::ZERO), + (key_dd, U256::ZERO), + ])); + + let mut targets: Vec = Vec::new(); + trie.update_leaves(&mut leaf_updates, |key, min_len| { + targets.push(ProofV2Target::new(key).with_min_len(min_len)); + }) + .expect("update_leaves should succeed"); + + // Callback should fire for the blinded child at 0xd8. + assert!(!targets.is_empty(), "callback should fire for blinded child during branch collapse"); + // Removal keys should remain in the map for retry. + assert!(!leaf_updates.is_empty(), "removal keys should remain in map after blinded hit"); + + // Reveal the blinded subtrie. + let (mut proof_nodes, _) = harness.proof_v2(&mut targets); + trie.reveal_nodes(&mut proof_nodes).expect("reveal_nodes should succeed"); + + // Retry — now the sibling is revealed, branch can collapse. + trie.update_leaves(&mut leaf_updates, |_, _| {}) + .expect("update_leaves should succeed on retry"); + assert!(leaf_updates.is_empty(), "keys should be drained after successful retry"); + + // Root should match reference trie with only key_d8. + let expected_harness = SuiteTestHarness::new(BTreeMap::from([(key_d8, U256::from(2))])); + let root = trie.root(); + assert_eq!( + root, + expected_harness.original_root(), + "root should match trie with only the previously-blinded leaf" + ); +} + +/// Regression: subtrie emptied by deletes mixed with `LeafUpdate::Touched`. +/// +/// When all `Changed` updates in a subtrie are removals and they would empty the subtrie, +/// the `might_empty_subtrie` guard must still trigger even if `Touched` entries are present. +/// `Touched` is a no-op that doesn't prevent the subtrie from being emptied. +pub(super) fn test_subtrie_emptied_by_deletes_with_touched(new_trie: fn() -> T) { + // Two leaves under prefix 0xAB (the target subtrie), one under 0xAC (sibling at + // depth 1 to force the 0xAB child into a subtrie at depth 2), one under 0xCD + // (sibling at depth 0 to force a branch at the root). + let mut key_ab1 = B256::ZERO; + key_ab1[0] = 0xAB; + key_ab1[31] = 0x11; + let mut key_ab2 = B256::ZERO; + key_ab2[0] = 0xAB; + key_ab2[31] = 0x22; + let mut key_ab3 = B256::ZERO; + key_ab3[0] = 0xAB; + key_ab3[31] = 0x33; + let mut key_ac1 = B256::ZERO; + key_ac1[0] = 0xAC; + key_ac1[31] = 0x44; + let mut key_cd1 = B256::ZERO; + key_cd1[0] = 0xCD; + key_cd1[31] = 0x01; + + let value = U256::from(1u64); + + let base_storage: BTreeMap = + [(key_ab1, value), (key_ab2, value), (key_ac1, value), (key_cd1, value)] + .into_iter() + .collect(); + + let harness = SuiteTestHarness::new(base_storage.clone()); + let all_keys = vec![key_ab1, key_ab2, key_ac1, key_cd1]; + let mut trie: T = harness.init_trie_with_targets(&all_keys, false, new_trie); + + // Verify initial root matches. + let root = trie.root(); + assert_eq!(root, harness.original_root(), "initial root mismatch"); + + // Delete both 0xAB leaves + Touched on a third 0xAB key (not in the trie). + // Touched is a no-op but must not prevent the might_empty_subtrie guard. + let mut leaf_updates: B256Map = [ + (key_ab1, LeafUpdate::Changed(Vec::new())), + (key_ab2, LeafUpdate::Changed(Vec::new())), + (key_ab3, LeafUpdate::Touched), + ] + .into_iter() + .collect(); + + harness.reveal_and_update(&mut trie, &mut leaf_updates); + + // Root should match reference trie with ab1 and ab2 removed. + let mut expected_storage = base_storage; + expected_storage.remove(&key_ab1); + expected_storage.remove(&key_ab2); + let expected_harness = SuiteTestHarness::new(expected_storage); + + let actual_root = trie.root(); + assert_eq!(actual_root, expected_harness.original_root(), "post-delete root mismatch"); +} diff --git a/crates/trie/sparse/tests/suite/wipe_clear.rs b/crates/trie/sparse/tests/suite/wipe_clear.rs index 08e1e0d41d..5a4ff2d4ad 100644 --- a/crates/trie/sparse/tests/suite/wipe_clear.rs +++ b/crates/trie/sparse/tests/suite/wipe_clear.rs @@ -2,7 +2,7 @@ use super::*; /// Calling `wipe()` resets the trie so that /// `root()` returns `EMPTY_ROOT_HASH`. -pub(super) fn test_wipe_resets_to_empty_root() { +pub(super) fn test_wipe_resets_to_empty_root(new_trie: fn() -> T) { let storage: BTreeMap = BTreeMap::from([ (B256::with_last_byte(0x10), U256::from(1)), (B256::with_last_byte(0x20), U256::from(2)), @@ -12,7 +12,7 @@ pub(super) fn test_wipe_resets_to_empty_root() { ]); let harness = SuiteTestHarness::new(storage); - let mut trie: T = harness.init_trie_fully_revealed(false); + let mut trie: T = harness.init_trie_fully_revealed(false, new_trie); // Compute root to confirm the trie is populated. let root_before = trie.root(); @@ -28,7 +28,9 @@ pub(super) fn test_wipe_resets_to_empty_root() { /// `clear()` resets the trie to empty but preserves /// update tracking mode. After clear, `root()` returns `EMPTY_ROOT_HASH` and /// `take_updates()` returns empty (non-wiped) updates. -pub(super) fn test_clear_resets_trie_but_preserves_update_tracking() { +pub(super) fn test_clear_resets_trie_but_preserves_update_tracking( + new_trie: fn() -> T, +) { let storage: BTreeMap = BTreeMap::from([ (B256::with_last_byte(0x10), U256::from(1)), (B256::with_last_byte(0x20), U256::from(2)), @@ -37,7 +39,7 @@ pub(super) fn test_clear_resets_trie_but_preserves_update_tracking() { +pub(super) fn test_wipe_produces_wiped_updates(new_trie: fn() -> T) { let storage: BTreeMap = BTreeMap::from([ (B256::with_last_byte(0x10), U256::from(1)), (B256::with_last_byte(0x20), U256::from(2)), @@ -68,7 +70,7 @@ pub(super) fn test_wipe_produces_wiped_updates() { let harness = SuiteTestHarness::new(storage); // retain_updates = true so update tracking is active - let mut trie: T = harness.init_trie_fully_revealed(true); + let mut trie: T = harness.init_trie_fully_revealed(true, new_trie); // Compute root to populate the trie fully. let root_before = trie.root(); @@ -90,7 +92,7 @@ pub(super) fn test_wipe_produces_wiped_updates() { /// A cleared trie can be fully re-initialized and used /// normally. After `clear()`, set a new root from a different dataset, reveal /// nodes, insert a leaf, and verify `root()` matches the reference. -pub(super) fn test_clear_then_reuse_trie() { +pub(super) fn test_clear_then_reuse_trie(new_trie: fn() -> T) { // Phase 1: build a trie with 5 leaves and compute root. let storage_1: BTreeMap = BTreeMap::from([ (B256::with_last_byte(0x10), U256::from(1)), @@ -100,7 +102,7 @@ pub(super) fn test_clear_then_reuse_trie() { (B256::with_last_byte(0x50), U256::from(5)), ]); let harness_1 = SuiteTestHarness::new(storage_1); - let mut trie: T = harness_1.init_trie_fully_revealed(false); + let mut trie: T = harness_1.init_trie_fully_revealed(false, new_trie); let root_1 = trie.root(); assert_eq!(root_1, harness_1.original_root());