Skip to content

Commit 153c561

Browse files
fix(hash_builder): store branch nodes with only leaf children in updates
Previously, branch nodes were only stored in `updated_branch_nodes` if they had children that were themselves branches (non-empty tree_masks/hash_masks). This caused branch nodes with only leaf children to be silently omitted from updates, breaking incremental trie sync for: - Small contracts with few storage slots - Sparse trie regions where all children are leaves - Fresh tries built without existing database data The fix adds a third condition: store the branch if its parent references it as a hashed child (which we just set in the parent's hash_mask). This ensures all branch nodes that would be looked up during trie traversal are present. Changes: - Root node (len == 0) is always stored - Non-root nodes are stored if they have branch children OR if the parent references this child in its hash_mask - Updated test expectations for tree_mask (now correctly marks stored children) - Added regression tests for the bug scenarios - Un-ignored tests that now pass with the fix Closes: #25 Amp-Thread-ID: https://ampcode.com/threads/T-019bf62a-fcc3-770a-b813-cf3151d34bf1 Co-authored-by: Amp <amp@ampcode.com>
1 parent eec17e7 commit 153c561

1 file changed

Lines changed: 144 additions & 17 deletions

File tree

src/hash_builder/mod.rs

Lines changed: 144 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -427,13 +427,37 @@ impl<K: AsRef<AddedRemovedKeys>> HashBuilder<K> {
427427
/// masks in the database. We will use that when consuming the intermediate nodes
428428
/// from the database to efficiently build the trie.
429429
fn store_branch_node(&mut self, current: &Nibbles, len: usize, children: Vec<B256>) {
430+
// Update the parent's hash_mask to indicate this child is a hashed branch node
430431
if len > 0 {
431432
let parent_index = len - 1;
432433
self.hash_masks[parent_index] |=
433434
TrieMask::from_nibble(current.get_unchecked(parent_index));
434435
}
435436

436-
let store_in_db_trie = !self.tree_masks[len].is_empty() || !self.hash_masks[len].is_empty();
437+
// Determine if this branch node should be stored in the database.
438+
//
439+
// A branch node must be stored if:
440+
// 1. It is the root node (len == 0), since it's the entry point for trie traversal, OR
441+
// 2. It has children that are themselves branches (indicated by non-empty tree/hash masks),
442+
// meaning we need to store it to allow incremental updates, OR
443+
// 3. Its parent references it as a hashed child. Since we just set the parent's hash_mask
444+
// above, we check if this child's bit is now set in the parent's hash_mask.
445+
//
446+
// The key insight is that even a branch node with only leaf children must be stored
447+
// if its parent will reference it by hash (which is always the case for branch nodes
448+
// whose RLP encoding >= 32 bytes).
449+
let store_in_db_trie = if len == 0 {
450+
// Root node is always stored
451+
true
452+
} else {
453+
let parent_index = len - 1;
454+
let flag = TrieMask::from_nibble(current.get_unchecked(parent_index));
455+
// Store if this node has branch children OR if the parent references this child
456+
!self.tree_masks[len].is_empty()
457+
|| !self.hash_masks[len].is_empty()
458+
|| (self.hash_masks[parent_index] & flag) != TrieMask::default()
459+
};
460+
437461
if store_in_db_trie {
438462
if len > 0 {
439463
let parent_index = len - 1;
@@ -631,11 +655,8 @@ mod tests {
631655

632656
/// Verify that branch updates are complete for multi-leaf tries.
633657
///
634-
/// NOTE: This test currently fails due to a bug in `store_branch_node` where branch nodes
635-
/// with only leaf children are not stored in updates. See:
636-
/// <https://github.com/alloy-rs/trie/pull/124>
658+
/// For tries with 2+ leaves, there must be at least one branch node in updates.
637659
#[test]
638-
#[ignore = "fails due to store_branch_node bug - see PR #124"]
639660
#[cfg(feature = "arbitrary")]
640661
#[cfg_attr(miri, ignore = "no proptest")]
641662
fn arbitrary_branch_updates_complete() {
@@ -796,7 +817,8 @@ mod tests {
796817
// `3`.
797818
// 2. Hash Mask: 0b0110. Of the above items, nibbles `1` and `2` correspond to children that
798819
// are branch nodes.
799-
// 3. Tree Mask: 0b0000. None of the children are stored in the database (yet).
820+
// 3. Tree Mask: 0b0110. Children at nibbles `1` and `2` are branch nodes that are stored in
821+
// the database/updates (they are referenced by hash from this parent node).
800822
// 4. Hashes: Hashes of the 2 sub-branch roots, at nibbles `1` and `2`. Calculated by
801823
// hashing the 0th and 1st element for the branch at nibble `1` , and the 0th and 2nd
802824
// element for the branch at nibble `2`. This basically means that every
@@ -842,8 +864,8 @@ mod tests {
842864
let update = updates.get(&Nibbles::from_nibbles_unchecked(hex!("01"))).unwrap();
843865
// Nibbles 0, 1, 2, 3 have children
844866
assert_eq!(update.state_mask, TrieMask::new(0b1111));
845-
// None of the children are stored in the database
846-
assert_eq!(update.tree_mask, TrieMask::new(0b0000));
867+
// Children at nibbles 1 and 2 are branch nodes stored in updates
868+
assert_eq!(update.tree_mask, TrieMask::new(0b0110));
847869
// Children under nibbles `1` and `2` are branch nodes with `hashes`
848870
assert_eq!(update.hash_mask, TrieMask::new(0b0110));
849871
// Calculated when running the hash builder
@@ -916,12 +938,123 @@ mod tests {
916938
assert_eq!(hb2.root(), expected);
917939
}
918940

941+
/// Test that branch nodes are correctly added to updates even when they only have leaf
942+
/// children. This was a bug where branch nodes with only leaf children were not being
943+
/// stored in updates because the condition only checked if the node had branch children.
944+
#[test]
945+
fn test_updates_root() {
946+
let mut hb = HashBuilder::default().with_updates(true);
947+
let account: Vec<u8> = Vec::new();
948+
949+
hb.add_leaf(
950+
Nibbles::unpack(hex!(
951+
"a711355ec1c8f7e26bb3ccbcb0b75d870d15846c0b98e5cc452db46c37faea40"
952+
)),
953+
account.as_ref(),
954+
);
955+
hb.add_leaf(
956+
Nibbles::unpack(hex!(
957+
"a77d337781e762f3577784bab7491fcc43e291ce5a356b9bc517ac52eed3a37a"
958+
)),
959+
account.as_ref(),
960+
);
961+
hb.add_leaf(
962+
Nibbles::unpack(hex!(
963+
"a77d397a32b8ab5eb4b043c65b1f00c93f517bc8883c5cd31baf8e8a279475e3"
964+
)),
965+
account.as_ref(),
966+
);
967+
hb.add_leaf(
968+
Nibbles::unpack(hex!(
969+
"a7f936599f93b769acf90c7178fd2ddcac1b5b4bc9949ee5a04b7e0823c2446e"
970+
)),
971+
account.as_ref(),
972+
);
973+
974+
let _root = hb.root();
975+
let (_, updates) = hb.split();
976+
977+
// Previously this would return an empty map because branch nodes with only
978+
// leaf children were not being stored. Now all branch nodes should be stored.
979+
assert!(!updates.is_empty(), "updates should not be empty");
980+
}
981+
982+
/// Test the tree handling for various branch configurations.
983+
/// This tests that all branch nodes in a multi-level trie are correctly stored.
984+
#[test]
985+
fn test_trie_updates_branch_nodes() {
986+
let default_leaf = b"hello";
987+
988+
// Create a trie structure with multiple branch levels:
989+
// Root branch at 0x with children at nibbles 0, 1, 2, 3
990+
// Each of these has further structure creating nested branches
991+
let data = vec![
992+
(hex!("0000").to_vec(), default_leaf.to_vec()),
993+
(hex!("0020").to_vec(), default_leaf.to_vec()),
994+
(hex!("1000").to_vec(), default_leaf.to_vec()),
995+
(hex!("2000").to_vec(), default_leaf.to_vec()),
996+
(hex!("2111").to_vec(), default_leaf.to_vec()),
997+
(hex!("2122").to_vec(), default_leaf.to_vec()),
998+
(hex!("2123").to_vec(), default_leaf.to_vec()),
999+
(hex!("3000").to_vec(), default_leaf.to_vec()),
1000+
(hex!("3001").to_vec(), default_leaf.to_vec()),
1001+
];
1002+
1003+
let mut hb = HashBuilder::default().with_updates(true);
1004+
for (key, val) in &data {
1005+
let nibbles = Nibbles::unpack(key);
1006+
hb.add_leaf(nibbles, val.as_ref());
1007+
}
1008+
1009+
let _root = hb.root();
1010+
let (_, updates) = hb.split();
1011+
1012+
// The trie structure creates 6 branch nodes that should all be in updates:
1013+
// 1. Root branch (path: empty)
1014+
// 2. Branch at 0x0 (children at 0, 2)
1015+
// 3. Branch at 0x2 (children at 0, 1)
1016+
// 4. Branch at 0x21 (children at 1, 2)
1017+
// 5. Branch at 0x212 (children at 2, 3)
1018+
// 6. Branch at 0x300 (children at 0, 1)
1019+
assert_eq!(updates.len(), 6, "expected 6 branch nodes in updates");
1020+
}
1021+
1022+
/// Test that small storage tries (like new contracts with few slots) correctly
1023+
/// store their branch nodes for incremental sync.
1024+
#[test]
1025+
fn test_small_storage_trie_updates() {
1026+
use alloy_primitives::keccak256;
1027+
1028+
let mut hb = HashBuilder::default().with_updates(true);
1029+
1030+
// Simulate a contract with 4 storage slots (common for simple contracts)
1031+
// Keys are keccak256(slot_index) as per Ethereum spec
1032+
let slots: BTreeMap<_, _> = (0u64..4)
1033+
.map(|i| {
1034+
let mut slot = [0u8; 32];
1035+
slot[31] = i as u8;
1036+
let key = keccak256(slot);
1037+
(key, alloy_rlp::encode(U256::from(i * 100)))
1038+
})
1039+
.collect();
1040+
1041+
for (key, value) in &slots {
1042+
hb.add_leaf(Nibbles::unpack(key), value);
1043+
}
1044+
1045+
let _root = hb.root();
1046+
let (_, updates) = hb.split();
1047+
1048+
// Even a small trie should have at least the root branch node stored
1049+
assert!(!updates.is_empty(), "small storage trie should have updates");
1050+
1051+
// Verify root is included (path = empty nibbles)
1052+
assert!(updates.contains_key(&Nibbles::default()), "root branch node should be in updates");
1053+
}
1054+
9191055
/// Test edge case: keys that diverge at the last nibble with empty values.
9201056
/// This creates very small leaf RLPs and deep branch structures.
921-
///
922-
/// NOTE: Fails due to store_branch_node bug - see PR #124
9231057
#[test]
924-
#[ignore = "fails due to store_branch_node bug - see PR #124"]
9251058
fn test_deep_divergence_empty_values() {
9261059
let mut hb = HashBuilder::default().with_updates(true);
9271060

@@ -939,10 +1072,7 @@ mod tests {
9391072
}
9401073

9411074
/// Test: siblings at different depths to verify mask propagation.
942-
///
943-
/// NOTE: Fails due to store_branch_node bug - see PR #124
9441075
#[test]
945-
#[ignore = "fails due to store_branch_node bug - see PR #124"]
9461076
fn test_mask_propagation_across_depths() {
9471077
let mut hb = HashBuilder::default().with_updates(true);
9481078

@@ -1322,11 +1452,8 @@ mod tests {
13221452
///
13231453
/// This tests that mask arrays are properly cleared/scoped when building
13241454
/// multiple subtrees under the same parent in successive inserts.
1325-
///
1326-
/// NOTE: Fails due to store_branch_node bug - see PR #124
13271455
#[test]
13281456
#[cfg(feature = "std")]
1329-
#[ignore = "fails due to store_branch_node bug - see PR #124"]
13301457
fn test_mask_isolation_successive_subtrees() {
13311458
let mut hb = HashBuilder::default().with_updates(true);
13321459

0 commit comments

Comments
 (0)