Skip to content

Conversation

@zerosnacks
Copy link
Member

@zerosnacks zerosnacks commented Jan 25, 2026

Context

Builds on top of #123 to address issue reported in #25.

Summary

Adds an opt-in full-branch-updates feature flag that stores all branch nodes referenced by hash in updated_branch_nodes, including those with only leaf children. This is required for complete incremental trie sync.

Supersedes #25.

The Problem

The current store_branch_node only stores a branch if it has branch children:

let store_in_db_trie = !self.tree_masks[len].is_empty() || !self.hash_masks[len].is_empty();

This means branch nodes whose children are all leaves are omitted from updates. This is intentional as a write I/O optimization for reth, but breaks incremental sync for other consumers.

Affected scenarios:

  • Small contracts with few storage slots
  • Sparse trie regions where all children are leaves
  • Fresh tries built without existing database data

The Solution

Add a full-branch-updates feature flag that enables a third condition: store the branch if its parent references it as a hashed child.

#[cfg(feature = "full-branch-updates")]
{
    let parent_index = len - 1;
    let flag = TrieMask::from_nibble(current.get_unchecked(parent_index));
    has_branch_children
        || (self.hash_masks[parent_index] & flag) != TrieMask::default()
}

#[cfg(not(feature = "full-branch-updates"))]
{
    has_branch_children
}

Key design decisions:

  • Uses compile-time #[cfg] checks - zero runtime overhead for the default path
  • Default behavior (without feature) is unchanged - reth continues working as before
  • Consumers needing complete updates opt-in via features = ["full-branch-updates"]

Tests

Tests requiring the new behavior are gated behind the full-branch-updates feature:

  • test_updates_root - Verifies root branch is stored for simple two-leaf trie
  • test_trie_updates_branch_nodes - Verifies expected number of branch updates
  • test_small_storage_trie_updates - Tests the exact failing scenario from fix root updates bug in hash_builder #25
  • test_deep_divergence_empty_values - Edge case with empty leaf values
  • test_mask_propagation_across_depths - Multi-depth mask propagation
  • test_mask_isolation_successive_subtrees - Verifies mask isolation across subtrees
  • arbitrary_branch_updates_complete - Proptest verifying completeness (assertion only with feature)

Impact

  • Not a breaking change - default behavior is preserved
  • Root hash computation is unchanged (only affects updated_branch_nodes)
  • Consumers can opt-in when they need complete trie updates

@codspeed-hq
Copy link

codspeed-hq bot commented Jan 25, 2026

CodSpeed Performance Report

Merging this PR will degrade performance by 12.58%

Comparing fix/store-branch-node-updates (c5ce39f) with zerosnacks/proptest-coverage (eec17e7)

Summary

❌ 3 regressed benchmarks
✅ 1 untouched benchmark

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
trie[8] 405.3 ns 463.6 ns -12.58%
trie[16] 408.3 ns 466.7 ns -12.5%
trie[32] 414.4 ns 472.8 ns -12.34%

@zerosnacks zerosnacks force-pushed the fix/store-branch-node-updates branch from 1ff2437 to 37cb96c Compare January 25, 2026 17:43
@zerosnacks zerosnacks marked this pull request as draft January 25, 2026 17:43
@zerosnacks zerosnacks changed the base branch from main to zerosnacks/proptest-coverage January 25, 2026 17:44
@zerosnacks zerosnacks force-pushed the fix/store-branch-node-updates branch from 37cb96c to 8de5f1e Compare January 25, 2026 17:46
@zerosnacks zerosnacks force-pushed the zerosnacks/proptest-coverage branch 2 times, most recently from 7f64ea9 to 3fc8c1e Compare January 25, 2026 17:54
@zerosnacks zerosnacks force-pushed the fix/store-branch-node-updates branch 2 times, most recently from 2e91a3d to 153c561 Compare January 25, 2026 18:07
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
Co-authored-by: Amp <[email protected]>
Amp-Thread-ID: https://ampcode.com/threads/T-019bf64d-c82f-7682-9933-a37c563468a6
@zerosnacks
Copy link
Member Author

cc @Rjected + @mediocregopher + @shekhirin for visibility

PR builds on test suite expansion in #123

@mediocregopher
Copy link
Collaborator

mediocregopher commented Jan 26, 2026

@zerosnacks not storing the root node or branch nodes with only leaf children are deliberate design decions; storing a branch on disk requires write IO, and uses more space, so there's a tradeoff in terms of how much read IO it saves us later. If you can show benchmarks which challenge this design then we could be open to this change, but as it is I would say this behavior is intentional.

EDIT: I guess the above applies to reth specifically, which uses the HashBuilder heavily. If other projects want to use the HashBuilder differently we can also talk about making this behavior optional.

@zerosnacks zerosnacks changed the title fix(hash_builder): store branch nodes with only leaf children in updates feat(hash_builder): add complete-updates feature for storing all branch nodes Jan 26, 2026
@zerosnacks zerosnacks force-pushed the fix/store-branch-node-updates branch from f5917ba to c496a04 Compare January 26, 2026 17:03
Makes the behavior of storing branch nodes with only leaf children opt-in
via the `complete-updates` feature flag. This addresses feedback that the
change would introduce unnecessary write I/O overhead for reth.

Without this feature (default): branch nodes with only leaf children are
omitted from `updated_branch_nodes` as an optimization to reduce disk writes.

With this feature: all branch nodes referenced by hash are stored, which is
required for complete incremental trie sync where consumers need the full
set of nodes to reconstruct the trie.

The feature uses compile-time `#[cfg]` checks, so there is zero runtime
overhead for the default path.

Co-authored-by: Amp <[email protected]>
Amp-Thread-ID: https://ampcode.com/threads/T-019bfb37-3ca2-7078-8ab0-776518763f83
@zerosnacks zerosnacks force-pushed the fix/store-branch-node-updates branch 3 times, most recently from c973d95 to 16b7b5d Compare January 26, 2026 17:40
@zerosnacks zerosnacks changed the title feat(hash_builder): add complete-updates feature for storing all branch nodes feat(hash_builder): add full-branch-updates feature for storing all branch nodes Jan 26, 2026
@zerosnacks zerosnacks force-pushed the fix/store-branch-node-updates branch 2 times, most recently from 5371f61 to 3e40407 Compare January 26, 2026 18:00
…odes

Makes the behavior of storing branch nodes with only leaf children opt-in
via the `full-branch-updates` feature flag. This addresses feedback that the
change would introduce unnecessary write I/O overhead for reth.

Without this feature (default): branch nodes with only leaf children are
omitted from `updated_branch_nodes` as an optimization to reduce disk writes.

With this feature: all branch nodes referenced by hash are stored, which is
required for complete incremental trie sync where consumers need the full
set of nodes to reconstruct the trie.

The feature uses compile-time `#[cfg]` checks, so there is zero runtime
overhead for the default path.

Co-authored-by: Amp <[email protected]>
Amp-Thread-ID: https://ampcode.com/threads/T-019bfb37-3ca2-7078-8ab0-776518763f83
@zerosnacks zerosnacks force-pushed the fix/store-branch-node-updates branch from 3e40407 to c5ce39f Compare January 26, 2026 18:02
@zerosnacks
Copy link
Member Author

@mediocregopher I've updated the PR to add a new opt-in feature flag full-branch-updates to resolve #25. This should therefore have no performance impact on Reth.

The CodSpeed regression appears to be flakiness on their side, I am unable to reproduce it locally.

@zerosnacks zerosnacks marked this pull request as ready for review January 26, 2026 18:20
let store_in_db_trie = store_in_db_trie || len == 0 || {
let parent_index = len - 1;
let flag = TrieMask::from_nibble(current.get_unchecked(parent_index));
(self.hash_masks[parent_index] & flag) != TrieMask::default()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zerosnacks just to make sure you're aware, this is not handling cases of branch nodes which are children of extension nodes. We never store the extension's hash in its parent branch, and therefore this wouldn't set store_in_db_trie for the child branch (also because I think parent_index isn't handling the extension case either)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I'll close this PR for now as I lack sufficient context and this is sensitive code

@zerosnacks zerosnacks closed this Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants