fix(export): deduplicate export sigs across proposals within a round

This commit is contained in:
Nicholas Dudfield
2026-03-18 09:32:27 +07:00
parent 3a055663cc
commit 829441b52e
2 changed files with 30 additions and 4 deletions

View File

@@ -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();

View File

@@ -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<uint256, std::set<PublicKey>> sigs_;
std::set<uint256> 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()
{