Skip to content

fix: Derive traversal node IDs from the branch actually descended - #7942

Draft
bthomee wants to merge 1 commit into
bthomee/shamap-stack-04-clamp-selectbranch-depthfrom
bthomee/shamap-stack-05-derive-traversal-node-ids
Draft

fix: Derive traversal node IDs from the branch actually descended#7942
bthomee wants to merge 1 commit into
bthomee/shamap-stack-04-clamp-selectbranch-depthfrom
bthomee/shamap-stack-05-derive-traversal-node-ids

Conversation

@bthomee

@bthomee bthomee commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Part 5/8 of a stack. Base: part 4 (bthomee/shamap-stack-04-clamp-selectbranch-depth).

belowHelper built each stack entry's SHAMapNodeID from branch, the branch
used to reach the subtree root, rather than childBranch, the branch it had
just descended. The resulting IDs carried a correct depth but named a
different subtree, and nothing rejected them: such an ID has a legal depth and
a legal mask, so only comparing it against an actual leaf key exposes the
mismatch. The affected stacks feed read-only traversals whose consumers use
only the depth, so no ledger state, hash, or peer message was affected, but
any future consumer of getNodeID() would have silently received the wrong
position.

Rather than fix the one call, make the mistake unrepresentable.
NodePathStack replaces the bare std::stack and refuses to accept an ID at
all: every push takes the branch being descended and derives the ID itself, so
a node and its ID cannot disagree. isPrefixOf assertions on each push catch a
wrong branch at the point it happens rather than wherever the ID is later
read. Leaf entries now keep the depth they were reached at instead of a
normalized kLeafDepth, which is what lets those assertions hold:
addGiveItem splits a leaf from the depth it actually sits at.

The new traversal tests fail on the previous code: reverting the branch
derivation trips the leaf-key assertion on the first iteration. Also adds a
deepFanOutKeysAtLeafDepth helper and mirrors them against it, since the
existing deepFanOutKeys's fan-out at the 6th nibble keeps its tree only
about 6 levels deep and never exercised the depth-63/64 code these tests are
meant to protect, plus a case that collapses the entire depth-63 chain of
single-child inner nodes into a leaf on the final delete, which the
every-other-key deletion pattern the other new tests use never triggers.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens SHAMap traversal correctness by making it impossible to associate a node with a mismatched SHAMapNodeID during traversals (and related walks), and adds targeted traversal tests to exercise deep/leaf-depth paths that previously weren’t covered.

Changes:

  • Introduces NodePathStack to derive node IDs from the actual descended branch (and assert key/ID consistency) rather than accepting caller-supplied IDs.
  • Refactors traversal/walk code paths (walkTowardsKey, belowHelper, iterator/bounds traversal) to use NodePathStack.
  • Adds new traversal-focused unit tests, including leaf-depth fan-out and inner-node collapse scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/tests/libxrpl/shamap/SHAMap.cpp Adds traversal/bounds/iteration tests covering deep and leaf-depth traversal scenarios, including deletion/collapse cases.
src/libxrpl/shamap/SHAMapSync.cpp Switches proof-path collection walk to use the new NodePathStack.
src/libxrpl/shamap/SHAMap.cpp Refactors traversal helpers and bounds/iteration logic to build consistent node/ID paths via NodePathStack.
include/xrpl/shamap/SHAMap.h Defines NodePathStack and updates internal method/iterator plumbing to use it.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +325 to +326
map.addItem(SHAMapNodeType::TnAccountState, makeShamapitem(k, std::move(vuc)));
map.invariants();
@bthomee
bthomee force-pushed the bthomee/shamap-stack-04-clamp-selectbranch-depth branch from 2bed752 to ed28138 Compare August 3, 2026 23:25
@bthomee
bthomee force-pushed the bthomee/shamap-stack-05-derive-traversal-node-ids branch from 199fe0d to 651f082 Compare August 3, 2026 23:25
@bthomee
bthomee force-pushed the bthomee/shamap-stack-04-clamp-selectbranch-depth branch from ed28138 to 7b7d835 Compare August 4, 2026 00:12
@bthomee
bthomee force-pushed the bthomee/shamap-stack-05-derive-traversal-node-ids branch from 651f082 to fcbd9fe Compare August 4, 2026 00:12
`belowHelper` built each stack entry's `SHAMapNodeID` from `branch`, the branch
used to reach the subtree root, rather than `childBranch`, the branch it had
just descended. The resulting IDs carried a correct depth but named a
different subtree, and nothing rejected them: such an ID has a legal depth and
a legal mask, so only comparing it against an actual leaf key exposes the
mismatch. The affected stacks feed read-only traversals whose consumers use
only the depth, so no ledger state, hash, or peer message was affected, but
any future consumer of `getNodeID()` would have silently received the wrong
position.

Rather than fix the one call, make the mistake unrepresentable.
`NodePathStack` replaces the bare `std::stack` and refuses to accept an ID at
all: every push takes the branch being descended and derives the ID itself, so
a node and its ID cannot disagree. `isPrefixOf` assertions on each push catch a
wrong branch at the point it happens rather than wherever the ID is later
read. Leaf entries now keep the depth they were reached at instead of a
normalized `kLeafDepth`, which is what lets those assertions hold:
`addGiveItem` splits a leaf from the depth it actually sits at.

The new traversal tests fail on the previous code: reverting the branch
derivation trips the leaf-key assertion on the first iteration. Also adds a
`deepFanOutKeysAtLeafDepth` helper and mirrors them against it, since the
existing `deepFanOutKeys`'s fan-out at the 6th nibble keeps its tree only
about 6 levels deep and never exercised the depth-63/64 code these tests are
meant to protect, plus a case that collapses the entire depth-63 chain of
single-child inner nodes into a leaf on the final delete, which the
every-other-key deletion pattern the other new tests use never triggers.
@bthomee
bthomee force-pushed the bthomee/shamap-stack-04-clamp-selectbranch-depth branch from 7b7d835 to a09fec0 Compare August 4, 2026 00:49
@bthomee
bthomee force-pushed the bthomee/shamap-stack-05-derive-traversal-node-ids branch from fcbd9fe to 6318ca4 Compare August 4, 2026 00:49
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.

2 participants