fix: Judge a map an InboundLedger's walk abandoned - #8087
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a correctness gap in InboundLedger acquisition where a SHAMap walk can abandon (invalidate) a map but still return an empty “missing nodes” result, which previously could be misinterpreted as “nothing left to fetch.” The change makes that “map became invalid” state visible to the two call sites (tryDB() and the aggressive-retry path in trigger()) that treat emptiness as completion.
Changes:
- Add
InboundLedger::hasInvalidMap()to detect when either ledger map has been invalidated during acquisition. - In
tryDB()andtrigger()(aggressive retry), explicitly fail the acquisition if a walk invalidated a map, instead of treating an empty result as completion. - Add regression tests covering (1) local fetch-pack resolution that abandons only one map and (2) aggressive retry judging a locally-resolved invalid map (plus the headerless “no map to judge” case).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/xrpld/app/ledger/InboundLedger.h | Declares hasInvalidMap() with rationale about empty walk results vs abandoned maps. |
| src/xrpld/app/ledger/detail/InboundLedger.cpp | Implements hasInvalidMap() and adds invalid-map checks in tryDB(), trigger(), plus telemetry in done(). |
| src/test/app/InboundLedger_test.cpp | Adds tests and helpers to reproduce the invalid-map-empty-walk edge cases and verify the new guards. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
8ab3228 to
e95a946
Compare
e95a946 to
da7fa30
Compare
da7fa30 to
10793e3
Compare
10793e3 to
e3c3c5d
Compare
e3c3c5d to
740d931
Compare
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
2 similar comments
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
740d931 to
cf2f800
Compare
|
All conflicts have been resolved. Assigned reviewers can now start or resume their review. |
cf2f800 to
840a476
Compare
840a476 to
600994a
Compare
A walk hands back a bare list of hashes, so an empty result doesn't distinguish a satisfied map from one the walk abandoned. InboundLedger::hasInvalidMap() now reports the difference. Three places that read emptiness as "nothing left to fetch" ask it first: tryDB(), since its two walks set haveState_/haveTransactions_ independently and one map can be abandoned while the other is merely incomplete; trigger()'s aggressive-retry branch, since the getNeededHashes() walk it just ran can reach the verdict itself; and trigger()'s state-map walk, the one walk that runs with mtx_ released. That last one is asked outside the guard that re-reads the flags after re-locking, since the verdict is about the map rather than about the round: another thread can report the ledger complete while the lock is released, and the guard would then drop the verdict, leaving a ledger reported complete whose map cannot be the one the header names. The claim is withdrawn alongside the failure there for that reason.
600994a to
9ffc300
Compare
There was a problem hiding this comment.
This diff adds InboundLedger::hasInvalidMap() and threads it into tryDB() and the two branches of trigger() so an empty "missing hashes" result from a walk that actually abandoned a map is no longer misread as "nothing left to fetch." The new guards are placed before the existing empty-result branches, the state-map section correctly withdraws complete_ alongside setting failed_ when the walk lands after the lock was re-acquired, and done() gets a documented SOMETIMES() marking the remaining (already-existing, now narrower) race. The two new tests stage fetch packs so a whole chain resolves locally without going through addKnownNode(), matching the described tryDB()/aggressive-retry asymmetry, and use setTimeouts()/clearProgress() to reach the aggressive branch without waiting on the real timer chain. I traced the flag/lock interactions in both call sites and didn't find a case where the new guard fires incorrectly or fails to fire when it should — the logic is consistent with the detailed comments and the rest of the stack. No changed lines stood out as bugs worth flagging.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Part 8/16 of a stack. Base: part 7 (
bthomee/shamap-invalid-07-getmissingnodes).High Level Overview of Change
An
InboundLedgerwalk can abandon a map (mark it invalid) without that ever becoming visible throughthe flags
tryDB()/trigger()already check, since an empty "missing nodes" result reads identicallyto "nothing left to fetch." Both call sites now ask
InboundLedger::hasInvalidMap()first.Context of Change
A walk hands back a bare list of hashes, so an empty result does not distinguish a satisfied map from
one that has been abandoned.
InboundLedger::hasInvalidMap()reports the difference, and the two placesthat read emptiness as nothing left to fetch ask it first.
tryDB()asks after both of its walks and before the settle below, because the walks sethaveState_and
haveTransactions_independently: one map can be abandoned while the other is merely incomplete, soneither flag is set and the settle that would otherwise catch it never runs.
trigger()'saggressive-retry branch asks before testing whether anything is still needed, since the
getNeededHashes()walk it just ran can reach the verdict itself. Where there is no header yet there isno map to judge, and
hasInvalidMap()reportsfalse, which is right becausegetNeededHashes()hasthen asked for the header and the non-empty branch is the one that runs.
done()keeps the settle asits backstop and gains a
SOMETIMES()naming the race that still reaches it:trigger()walks the statemap with
mtx_released, so a walk on another thread can reach the verdict after the flags said theacquisition was finished.
Both new cases stage a fetch pack rather than peer data, since a fetch pack is checked against each
node's own hash and never structurally, so a whole chain resolves locally without passing through
addKnownNode(). The first coverstryDB(): the transaction root is the chain, so its walk abandonsthat map, while the state root is a hash nothing supplies, so that map is merely incomplete — the
asymmetry is what leaves both flags unset. The second covers
trigger(), recording a timeout countabove
kLedgerBecomeAggressiveThresholddirectly rather than waiting for the timer chain.API Impact
None of the checkboxes below apply: this is an internal
xrpldcorrectness fix with nolibxrpl,public-API, or peer-protocol surface.