From d77321aa75fcefe9b48385fc1c5ea4d81b3c0ffe Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Thu, 24 Sep 2026 15:23:34 +0700 Subject: [PATCH] fix(export): retry shares when manifest attribution changes Scope receive suppression to the share wire hash, validated sequence and resolved master identity. A valid share received before its manifest can then be retried at the same validated ledger without reopening work for unrelated manifest churn. Log the attribution used for suppression. Reproduced with a real token-based validator restart and delayed manifest processing: four later copies were suppressed, leaving two of three contributions. The fix admits exactly one copy. Verified RNG on/off and manifests-first/late DSF cases with three replays each; 11 suites, 202 cases, zero failures. DSF fixtures remain in the integration worktree for separate landing. --- src/xrpld/overlay/detail/PeerImp.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index a1fb0e842b..8d2116a7dc 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1235,11 +1235,14 @@ PeerImp::onMessage(std::shared_ptr const& m) auto const validatedSeq = app_.getLedgerMaster().getValidLedgerIndex(); for (std::size_t i = 0; i < shares->size(); ++i) { - // Admission depends on the validated chain. Reconsider an identical - // frame after validation advances, while suppressing duplicates - // against the same receiver state. + // Admission depends on the validated chain and signing-key attribution. + // An unknown key may become attributable when its manifest arrives, + // even while validation is unchanged. Scope retries to this key's + // resolved identity, so unrelated manifest churn does not reopen them. + auto const resolvedMaster = + app_.validatorManifests().getMasterKey((*shares)[i].signingKey); auto const admissionKey = - sha512Half((*shares)[i].wireHash(), validatedSeq); + sha512Half((*shares)[i].wireHash(), validatedSeq, resolvedMaster); bool const freshForState = app_.getHashRouter().addSuppressionPeer(admissionKey, id_); JLOG(journal_.trace()) @@ -1247,7 +1250,9 @@ PeerImp::onMessage(std::shared_ptr const& m) << " peer=" << id_ << " origin=" << (*shares)[i].originTxn << " position=" << unsigned((*shares)[i].committeePosition) << " wire=" << (*shares)[i].wireHash() - << " suppressionSeq=" << validatedSeq << " fresh=" << freshForState; + << " suppressionSeq=" << validatedSeq << " suppressionMaster=" + << toBase58(TokenType::NodePublic, resolvedMaster) + << " fresh=" << freshForState; if (freshForState) fresh.push_back(i); }