diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp index 0b6c53850..91fe6f1af 100644 --- a/src/xrpld/app/consensus/RCLConsensus.cpp +++ b/src/xrpld/app/consensus/RCLConsensus.cpp @@ -313,6 +313,7 @@ RCLConsensus::Adaptor::propose(RCLCxPeerPos::Proposal const& proposal) //@@start export-sig-attachment // Attach export signatures for any ttEXPORT txns in the current set. // Each "signature" is: txnHash (32 bytes) + validator pubkey (33 bytes). + // Only attach once per export per round (markSent deduplicates). // XAHAUD_NO_EXPORT_SIG=1 disables sig attachment (for testing sub-quorum). if (auto const* noSig = std::getenv("XAHAUD_NO_EXPORT_SIG"); noSig && std::string(noSig) == "1") @@ -329,6 +330,11 @@ RCLConsensus::Adaptor::propose(RCLCxPeerPos::Proposal const& proposal) if (stx && stx->getTxnType() == ttEXPORT) { auto const txHash = stx->getTransactionID(); + + // Only attach our sig on the first proposal this round. + if (!exportSigCollector().markSent(txHash)) + continue; + Serializer s; s.addBitString(txHash); s.addRaw(validatorKeys_.keys->publicKey.slice()); @@ -1668,6 +1674,7 @@ RCLConsensus::Adaptor::validatorKey() const void RCLConsensus::Adaptor::clearRngState() { + exportSigCollector().clearRound(); pendingCommits_.clear(); pendingReveals_.clear(); nodeIdToKey_.clear(); diff --git a/src/xrpld/app/misc/ExportSigCollector.h b/src/xrpld/app/misc/ExportSigCollector.h index 12ca5645b..09462e104 100644 --- a/src/xrpld/app/misc/ExportSigCollector.h +++ b/src/xrpld/app/misc/ExportSigCollector.h @@ -8,13 +8,16 @@ namespace ripple { -/// Minimal export signature collector for the retriable export approach. -/// Tracks which validators have "signed" each pending export. +/// Export signature collector for the retriable export approach. +/// Tracks which validators have "signed" each pending export, and +/// which exports we've already attached our own sig to (so we don't +/// redundantly re-send on every proposal). /// Thread-safe. class ExportSigCollector { mutable std::mutex mutex_; std::unordered_map> sigs_; + std::set sentThisRound_; public: void @@ -46,10 +49,26 @@ public: std::lock_guard lock(mutex_); sigs_.erase(txnHash); } + + /// Returns true if we haven't sent our sig for this tx yet this round. + /// Marks it as sent on first call. + bool + markSent(uint256 const& txnHash) + { + std::lock_guard lock(mutex_); + return sentThisRound_.insert(txnHash).second; + } + + /// Clear per-round state. Call at the start of each consensus round. + void + clearRound() + { + std::lock_guard lock(mutex_); + sentThisRound_.clear(); + } }; -/// Global instance for the prototype. -/// In production this would be owned by Application. +/// Global instance. In production this would be owned by Application. inline ExportSigCollector& exportSigCollector() {