refactor: Use unsigned int for branch-related operations - #7938
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors SHAMap branch/depth/index handling to use unsigned types (and kBranchFactor) where values are not expected to go negative, and it tightens a few traversal/proof-path edge cases in SHAMap-related code.
Changes:
- Switch SHAMap branch/depth parameters and loop indices from
inttounsigned int(and replace hard-coded16withkBranchFactorin multiple places). - Refactor
belowHelper’s scan-direction plumbing from a tuple-of-functions to aBelowDirectionenum. - Add a depth bound in
hasLeafNodeto avoid uncaught throws on malformed maps; adjust proof-path maximum length to usekLeafDepth + 1.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp | Updates leaf ID creation call to the new unsigned-depth createID signature. |
| src/libxrpl/shamap/SHAMapSync.cpp | Converts traversal indices/branches to unsigned and updates proof-path constraints/types. |
| src/libxrpl/shamap/SHAMapNodeID.cpp | Updates createID to unsigned depth and derives mask table size from kLeafDepth. |
| src/libxrpl/shamap/SHAMapInnerNode.cpp | Converts child/branch APIs and internal loops/bitmasks to unsigned-safe forms. |
| src/libxrpl/shamap/SHAMapDelta.cpp | Replaces hard-coded branch-factor constants and loop index types in delta walkers (incl. parallel walker sizing). |
| src/libxrpl/shamap/SHAMap.cpp | Converts descent/below-helper branch types to unsigned; refactors below scan logic and adjusts some branch-iteration loops. |
| include/xrpl/shamap/SHAMapNodeID.h | Public header signature changes for getChildNodeID/createID to unsigned. |
| include/xrpl/shamap/SHAMapInnerNode.h | Public header signature changes for branch-related APIs and inline bitmask logic. |
| include/xrpl/shamap/SHAMap.h | Updates SHAMap public/private method signatures for unsigned branches and introduces BelowDirection. |
| include/xrpl/shamap/detail/TaggedPointer.ipp | Updates sparse/dense iteration/indexing logic to unsigned types and unsigned bitmasks. |
| include/xrpl/shamap/detail/TaggedPointer.h | Updates getChildIndex and popcnt16 related code to unsigned types/bitmasks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -466,32 +468,24 @@ SHAMap::belowHelper( | |||
| } | |||
| inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(node); | |||
| stack.emplace(inner, stack.top().second.getChildNodeID(branch)); | |||
There was a problem hiding this comment.
✅ Auto-resolved: the latest review no longer flags this issue, so it appears to have been addressed. If that is not correct, reopen this thread and it will be re-checked on the next review.
There was a problem hiding this comment.
✅ Auto-resolved: the latest review no longer flags this issue, so it appears to have been addressed. If that is not correct, reopen this thread and it will be re-checked on the next review.
| SHAMapNodeID | ||
| SHAMapNodeID::createID(int depth, uint256 const& key) | ||
| SHAMapNodeID::createID(unsigned int depth, uint256 const& key) | ||
| { | ||
| XRPL_ASSERT( | ||
| depth >= 0 && depth <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::createID : valid depth"); | ||
| XRPL_ASSERT(depth <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::createID : valid depth"); | ||
| return SHAMapNodeID(depth, key & depthMask(depth)); |
There was a problem hiding this comment.
Fixed in #7941. That PR already contains the "clamp the depth" commit for selectBranch in this same file. The unguarded index is in depthMask's 65-entry table, so the constructor and isPrefixOfAtDepth share the exposure. Both places that set a depth now clamp: the constructor (the single point every depth_ passes through) and createID, which needs its own bound because it selects the mask while evaluating the constructor's argument; the constructor body runs too late to help.
Clamping is used rather than a throw, so this deliberately differs from getChildNodeID, since createID is reached from getSHAMapNodeID with a peer-supplied depth, and two of that function's three callers — InboundTransactions::gotData and PeerImp::onMessage — sit on paths with no handler between them and a thread boundary (JobQueue::processTask/Job::doJob and asio's ioContext_.run() both lack one). A logic_error there would terminate the process instead of dropping the message, so throwing would trade an OOB read for a remote crash vector.
Clamping also repairs id_ alongside depth_, since a node ID whose id and depth disagree breaks the invariant every read of id_ depends on. Leaving the depth unclamped would also let getRawString narrow it to one byte, turning depth 256 into a node claiming to be the root.
It's worth noting no current caller can reach this — verifyProofPath and getSHAMapNodeID both bound the depth first, so it's hardening, not a live bug.
There was a problem hiding this comment.
✅ Auto-resolved: the latest review no longer flags this issue, so it appears to have been addressed. If that is not correct, reopen this thread and it will be re-checked on the next review.
There was a problem hiding this comment.
✅ Auto-resolved: the latest review no longer flags this issue, so it appears to have been addressed. If that is not correct, reopen this thread and it will be re-checked on the next review.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
080c08b to
72936bc
Compare
There was a problem hiding this comment.
This is a clean, largely mechanical refactor from int to unsigned int for branch/depth indices in the SHAMap code, replacing now-redundant >= 0 asserts, swapping bare 16s for SHAMapInnerNode::kBranchFactor, and replacing the belowHelper tuple-of-lambdas mechanism with a BelowDirection enum. I traced the belowHelper/firstBelow/lastBelow rewrite and the lowerBound unsigned-decrement loop carefully since those are the highest-risk spots for off-by-one/underflow bugs, and both preserve the original iteration semantics correctly. The added depth bound in hasLeafNode correctly closes the described gap. No correctness or security issues found in the changed lines.
Branch indices, depths, and array positions in the SHAMap code were typed `int` despite never legitimately going negative, so several call sites carried a manual runtime check purely to rule out a value the type system could have ruled out on its own: `SHAMapNodeID::createID` asserted `depth >= 0`, `SHAMapInnerNode::setChild` asserted `m >= 0`. Switched these, and the branch parameters on `descend`/`descendThrow`/`descendAsync`, `getChildNodeID`, `getChildIndex`, and the loop indices iterating a node's 16 branches, to `unsigned int`, dropping the now-redundant half of each assert and replacing a few bare `16`s with `SHAMapInnerNode::kBranchFactor`. `belowHelper`'s `int` branch parameter is replaced together with the mechanism it fed: a `std::tuple<int, std::function<bool(int)>, std::function<void(int&)>>` describing which direction to scan is replaced by a `BelowDirection` enum. Also fixes two `srcDstNumAllocated - 1` subtractions in `TaggedPointer.ipp`'s sparse-array shift logic that this same cleanup should have caught, a stray `while child` to `which child` typo in the `StackEntry` tuple comment, and closes a gap in `hasLeafNode`: its `do`/`while` had no bound on how deep it could descend, so a malformed map with an inner node at `kLeafDepth` would reach `getChildNodeID` and throw uncaught instead of failing cleanly. That new bound is marked `UNREACHABLE` rather than left as a bare check, since `addKnownNode` already rejects such a map as invalid before it could be reached: no test can cover the line, so it is excluded from coverage rather than reported as an untested branch.
72936bc to
561ca50
Compare
There was a problem hiding this comment.
Clean, well-scoped mechanical refactor from int to unsigned int for branch/depth indices across the SHAMap code. I traced the trickiest transformations by hand: the lowerBound reverse-scan loop (branch - 1 >= 0 → branch > 0u; --branch), the belowHelper tuple-of-lambdas → BelowDirection enum rewrite, and the two srcDstNumAllocated - 1 fixes in TaggedPointer.ipp — all preserve original semantics given the invariant that the affected branch index is always present in the relevant bitset before the subtraction (so no unsigned-underflow path is reachable). The added hasLeafNode depth-bound check and the SHAMapNodeID.h/.cpp param renames are correct and match the stated intent. No newly-introduced correctness or security issues found in the changed (+) lines.
godexsoft
left a comment
There was a problem hiding this comment.
The code looks fine to me, largely a mechanical change.
I wonder why we use 'unsigned int' instead of 'uint32_t' and whether we should switch?
That's a larger question beyond the scope of this PR I think - there are pros and cons to each data type. |
Part 1/8 of a stack. Base:
develop.Branch indices, depths, and array positions in the SHAMap code were typed
intdespite never legitimately going negative, so several call sites carrieda manual runtime check purely to rule out a value the type system could have
ruled out on its own:
SHAMapNodeID::createIDasserteddepth >= 0,SHAMapInnerNode::setChildassertedm >= 0. Switched these, and the branchparameters on
descend/descendThrow/descendAsync,getChildNodeID,getChildIndex, and the loop indices iterating a node's 16 branches, tounsigned int, dropping the now-redundant half of each assert and replacing afew bare
16s withSHAMapInnerNode::kBranchFactor.belowHelper'sintbranch parameter is replaced together with the mechanismit fed: a
std::tuple<int, std::function<bool(int)>, std::function<void(int&)>>describing which direction to scan is replaced by a
BelowDirectionenum.Also fixes two
srcDstNumAllocated - 1subtractions inTaggedPointer.ipp's sparse-array shift logic that this same cleanup shouldhave caught, a stray
while childtowhich childtypo in theStackEntrytuple comment, and closes a gap in
hasLeafNode: itsdo/whilehad nobound on how deep it could descend, so a malformed map with an inner node at
kLeafDepthwould reachgetChildNodeIDand throw uncaught instead offailing cleanly.