From d87cfdc6046b12e2e96d715611d07306fabdee75 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Tue, 28 Apr 2026 09:17:17 +0700 Subject: [PATCH] fix(consensus): clear export sigs when export disabled --- src/test/app/ExportSigCollector_test.cpp | 25 +++++++++++++++++++ .../consensus/ConsensusExtensions_test.cpp | 24 ++++++++++++++++++ .../app/consensus/ConsensusExtensions.cpp | 13 ++++++++++ src/xrpld/app/misc/ExportSigCollector.h | 8 ++++++ 4 files changed, 70 insertions(+) diff --git a/src/test/app/ExportSigCollector_test.cpp b/src/test/app/ExportSigCollector_test.cpp index a5ab4072d..52c7e437a 100644 --- a/src/test/app/ExportSigCollector_test.cpp +++ b/src/test/app/ExportSigCollector_test.cpp @@ -122,12 +122,37 @@ public: BEAST_EXPECT(collector.signatureCount(tx) == 0); } + void + testClearAll() + { + testcase("clear all signatures and round state"); + + ExportSigCollector collector; + auto const verifiedTx = makeHash("clear-all-verified"); + auto const unverifiedTx = makeHash("clear-all-unverified"); + auto const sig = makeSignature(12); + + collector.addVerifiedSignature(verifiedTx, validator_, sig, 10); + collector.addUnverifiedSignature(unverifiedTx, validator_, sig, 10); + BEAST_EXPECT(collector.signatureCount(verifiedTx) == 1); + BEAST_EXPECT(collector.hasUnverifiedSignatures()); + BEAST_EXPECT(collector.markSent(verifiedTx)); + BEAST_EXPECT(!collector.markSent(verifiedTx)); + + collector.clearAll(); + + BEAST_EXPECT(collector.signatureCount(verifiedTx) == 0); + BEAST_EXPECT(!collector.hasUnverifiedSignatures()); + BEAST_EXPECT(collector.markSent(verifiedTx)); + } + void run() override { testCleanupUsesFirstSeenSeq(); testUpgradeSetsFirstSeenSeq(); testRemoveInvalidUnverifiedSignature(); + testClearAll(); } }; diff --git a/src/test/consensus/ConsensusExtensions_test.cpp b/src/test/consensus/ConsensusExtensions_test.cpp index 8b8671793..a052f3018 100644 --- a/src/test/consensus/ConsensusExtensions_test.cpp +++ b/src/test/consensus/ConsensusExtensions_test.cpp @@ -469,6 +469,29 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT(ext.fetchedExportSets.empty()); } + void + testExportDisabledRoundClearsCollector() + { + testcase("Export disabled round clears collector"); + + using namespace jtx; + Env env{*this, envconfig(), supported_amendments(), nullptr}; + ConsensusExtensions ce{env.app(), env.journal}; + auto const tx = makeHash("export-disabled-clears-collector"); + auto const pk = makeValidatorKeys().front(); + std::uint8_t const sigBytes[] = {1, 2, 3}; + Buffer const sig{sigBytes, sizeof(sigBytes)}; + + ce.setExportEnabledThisRound(true); + ce.exportSigCollector().addVerifiedSignature(tx, pk, sig, 10); + ce.clearRngState(); + BEAST_EXPECT(ce.exportSigCollector().signatureCount(tx) == 1); + + ce.setExportEnabledThisRound(false); + ce.clearRngState(); + BEAST_EXPECT(ce.exportSigCollector().signatureCount(tx) == 0); + } + public: void run() override @@ -478,6 +501,7 @@ public: testExportSigGateAllowsAlignedQuorumDespiteMinorityConflict(); testExportSigGateFetchesAdvertisedPeerSets(); testExportSigGateSkipsWhenExportDisabled(); + testExportDisabledRoundClearsCollector(); } }; diff --git a/src/xrpld/app/consensus/ConsensusExtensions.cpp b/src/xrpld/app/consensus/ConsensusExtensions.cpp index 6b2fa7fed..2754370a3 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.cpp +++ b/src/xrpld/app/consensus/ConsensusExtensions.cpp @@ -788,6 +788,13 @@ ConsensusExtensions::clearRngState() exportSigCollector_.clearRound(); if (auto const closed = app_.getLedgerMaster().getClosedLedger()) exportSigCollector_.cleanupStale(closed->info().seq); + if (!exportEnabledThisRound_) + { + // Export disabled is an amendment boundary, not a retry boundary. + // Drop cached signatures so an emergency stop cannot leave old quorum + // material waiting for a later re-enable. + exportSigCollector_.clearAll(); + } //@@end round-stop-export-reset //@@start round-stop-rng-reset pendingCommits_.clear(); @@ -1873,6 +1880,9 @@ void ConsensusExtensions::onTrustedPeerMessage( ::protocol::TMProposeSet const& wireMsg) { + if (!exportEnabled()) + return; + if (wireMsg.exportsignatures_size() == 0) return; @@ -2044,6 +2054,9 @@ ConsensusExtensions::attachExportSignatures( { auto const& valKeys = app_.getValidatorKeys(); + if (!exportEnabled()) + return; + // Attach export signatures for any ttEXPORT txns in the open ledger. // Gated on featureExport amendment. // RuntimeConfig no_export_sig disables sig attachment (testing sub-quorum). diff --git a/src/xrpld/app/misc/ExportSigCollector.h b/src/xrpld/app/misc/ExportSigCollector.h index 47a222608..08e6648cb 100644 --- a/src/xrpld/app/misc/ExportSigCollector.h +++ b/src/xrpld/app/misc/ExportSigCollector.h @@ -266,6 +266,14 @@ public: sigs_.erase(txnHash); } + void + clearAll() + { + std::lock_guard lock(mutex_); + sigs_.clear(); + sentThisRound_.clear(); + } + /// Get a snapshot of VERIFIED sigs (pubkeys only) for building /// the SHAMap. Only verified sigs appear in convergence. std::unordered_map>