From db302a0f7833b846dbbdc68a5b136ec7f8f57512 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Thu, 9 Apr 2026 17:23:53 +0700 Subject: [PATCH] fix(rng): add selfSeedReveal to fix CSF reveal counting The CSF never self-seeded its own reveal into pendingReveals_ because harvestRngData only processes peer proposals, not self. The real code handles this in decorateMessage, but the CSF has no equivalent. Add selfSeedReveal() called from the tick at reveal transition. Both the real ConsensusExtensions and the CSF Extensions implement it. The real code now has belt-and-suspenders: tick + decorateMessage. This fixes CSF peers having N-1 reveals instead of N, which caused every peer to compute entropy from a different subset. --- src/test/csf/Peer.h | 13 +++++++++++++ src/xrpld/app/consensus/ConsensusExtensions.cpp | 11 +++++++++++ src/xrpld/app/consensus/ConsensusExtensions.h | 6 ++++++ src/xrpld/consensus/ConsensusExtensionsTick.h | 6 ++++++ 4 files changed, 36 insertions(+) diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h index d62cf29da..f6b25be85 100644 --- a/src/test/csf/Peer.h +++ b/src/test/csf/Peer.h @@ -462,6 +462,19 @@ struct Peer return myEntropySecret_; } + void + selfSeedReveal() + { + if (!enableRngConsensus_) + return; + // Self-seed our own reveal into pendingReveals_ so it + // counts toward reveal quorum. The real code does this + // in decorateMessage; the CSF does it here since it has + // no equivalent serialization hook. + if (myEntropySecret_ != uint256{}) + pendingReveals_[peer.id] = myEntropySecret_; + } + void setEntropyFailed() { diff --git a/src/xrpld/app/consensus/ConsensusExtensions.cpp b/src/xrpld/app/consensus/ConsensusExtensions.cpp index c99ba7eec..431f14c44 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.cpp +++ b/src/xrpld/app/consensus/ConsensusExtensions.cpp @@ -508,6 +508,17 @@ ConsensusExtensions::setEntropyFailed() entropyFailed_ = true; } +void +ConsensusExtensions::selfSeedReveal() +{ + auto const& valKeys = app_.getValidatorKeys(); + if (myEntropySecret_ != uint256{}) + { + pendingReveals_[valKeys.nodeID] = myEntropySecret_; + nodeIdToKey_.insert_or_assign(valKeys.nodeID, valKeys.keys->publicKey); + } +} + //@@start clear-rng-state void ConsensusExtensions::clearRngState() diff --git a/src/xrpld/app/consensus/ConsensusExtensions.h b/src/xrpld/app/consensus/ConsensusExtensions.h index 97c1e98bc..88890ec2b 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.h +++ b/src/xrpld/app/consensus/ConsensusExtensions.h @@ -195,6 +195,12 @@ public: void setEntropyFailed(); + /// Self-seed our own reveal into pendingReveals_. + /// Called from extensionsTick at reveal transition. + /// In production, decorateMessage also self-seeds (belt + suspenders). + void + selfSeedReveal(); + void clearRngState(); diff --git a/src/xrpld/consensus/ConsensusExtensionsTick.h b/src/xrpld/consensus/ConsensusExtensionsTick.h index c63f4c3eb..7876640ff 100644 --- a/src/xrpld/consensus/ConsensusExtensionsTick.h +++ b/src/xrpld/consensus/ConsensusExtensionsTick.h @@ -415,6 +415,12 @@ extensionsTick(Ext& ext, Ctx const& ctx) auto newPos = ctx.getPosition(); newPos.myReveal = ext.getEntropySecret(); + // Self-seed our own reveal into pendingReveals so it + // counts toward reveal quorum and appears in the + // entropy set. harvestRngData only sees peer proposals, + // not our own. + ext.selfSeedReveal(); + ctx.updatePosition(newPos); if (ctx.mode == ConsensusMode::proposing)