From c6fa973cf670e16e592b4f0a07f378a5211fc901 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Thu, 9 Apr 2026 15:18:45 +0700 Subject: [PATCH] fix(rng): compute entropy from entropySetMap instead of pendingReveals H2: Compute final entropy from the agreed-upon entropySetMap_ SHAMap rather than from the local pendingReveals_ in-memory map. Previously, two nodes with different reveal subsets at timeout would compute different entropy from their local pendingReveals_ maps, despite both passing haveConsensus() (which only checks txSetHash). This could cause a ledger fork. Now the entropy computation reads directly from the entropySetMap_ whose hash was published in proposals and converged via SHAMap fetch/merge. Nodes that agree on entropySetHash deterministically produce the same entropy regardless of local pendingReveals_ state. If entropySetMap_ is null (bootstrap skip, pipeline failure), the existing shouldZeroEntropy() fallback handles it. --- .../app/consensus/ConsensusExtensions.cpp | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/xrpld/app/consensus/ConsensusExtensions.cpp b/src/xrpld/app/consensus/ConsensusExtensions.cpp index 923603862..fe59df2d2 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.cpp +++ b/src/xrpld/app/consensus/ConsensusExtensions.cpp @@ -1101,18 +1101,34 @@ ConsensusExtensions::onPreBuild(CanonicalTXSet& retriableTxs, LedgerIndex seq) << seq << " (reveals=" << pendingReveals_.size() << " threshold=" << quorumThreshold() << ")"; } - else + else if (entropySetMap_) { - // Sort reveals deterministically by Validator Public Key + // Compute entropy from the agreed-upon entropySet SHAMap + // rather than from local pendingReveals_. The map's hash + // was published in proposals and converged via fetch/merge, + // so all nodes with the same entropySetHash produce the + // same entropy — preventing reveal-subset divergence at + // timeout boundaries. + // + // Each leaf is an STTx with sfSigningPubKey (validator key) + // and sfDigest (the reveal). std::vector> sorted; - sorted.reserve(pendingReveals_.size()); - - for (auto const& [nodeId, reveal] : pendingReveals_) - { - auto it = nodeIdToKey_.find(nodeId); - if (it != nodeIdToKey_.end()) - sorted.emplace_back(it->second, reveal); - } + entropySetMap_->visitLeaves( + [&](boost::intrusive_ptr const& item) { + try + { + SerialIter sit(item->slice()); + STTx tx(std::ref(sit)); + auto const pk = tx.getFieldVL(sfSigningPubKey); + if (!publicKeyType(makeSlice(pk))) + return; + sorted.emplace_back( + PublicKey(makeSlice(pk)), tx.getFieldH256(sfDigest)); + } + catch (...) + { + } + }); if (!sorted.empty()) { @@ -1133,7 +1149,7 @@ ConsensusExtensions::onPreBuild(CanonicalTXSet& retriableTxs, LedgerIndex seq) JLOG(j_.info()) << "RNG: Injecting entropy " << finalEntropy << " from " << sorted.size() << " reveals" - << " for ledger " << seq; + << " (from entropySetMap) for ledger " << seq; } } //@@end rng-inject-entropy-selection