From 960808b1720766b52457ea198307e722c8da51fb Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Fri, 6 Feb 2026 14:34:28 +0700 Subject: [PATCH] fix(consensus): skip RNG wait when quorum is impossible and base threshold on active UNL When fewer participants are present than the quorum threshold, skip the RNG commit wait immediately instead of waiting the full pipeline timeout. Also base the quorum on activeUNLNodeIds_ (UNL Report with fallback) instead of the full trusted key set, so the denominator reflects who is actually active on the network. --- src/ripple/app/consensus/RCLConsensus.cpp | 25 ++++++++++++++-- src/ripple/consensus/Consensus.h | 36 +++++++++++++++++------ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index fb1bb8dd2..8309c9184 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -1149,9 +1149,28 @@ RCLConsensus::Adaptor::updateOperatingMode(std::size_t const positions) const std::size_t RCLConsensus::Adaptor::quorumThreshold() const { - auto [quorum, trustedKeys] = getQuorumKeys(); - // Use 80% quorum for RNG commit/reveal - return (trustedKeys.size() * 80 + 99) / 100; + // Base quorum on the active UNL — the same set used by + // isActiveUNLMember() to filter RNG data. This comes from the + // UNL Report (in-ledger) with fallback to the trusted key list. + // If a node drops off the UNL Report, the denominator shrinks + // and quorum becomes achievable with fewer participants. + // + // TODO: This needs more careful thought. Open questions: + // - Should there be a minimum absolute count (e.g. at least 3 + // committers) to prevent weak entropy from tiny partitions? + // - Is 80% the right percentage for RNG, or should it differ + // from the tx consensus threshold? + // - What happens if the UNL Report is stale and over-counts + // active validators? The "impossible quorum" early-exit in + // Consensus.h mitigates the worst case (no delay), but the + // node still falls back to ZERO entropy. + // - On a fresh network with no UNL Report, the fallback is + // getTrustedMasterKeys() which includes all configured + // validators — possibly including offline ones. + auto const base = activeUNLNodeIds_.size(); + if (base == 0) + return 1; // safety: need at least one commit + return (base * 80 + 99) / 100; } bool diff --git a/src/ripple/consensus/Consensus.h b/src/ripple/consensus/Consensus.h index 67d80e987..2d2505e83 100644 --- a/src/ripple/consensus/Consensus.h +++ b/src/ripple/consensus/Consensus.h @@ -1440,20 +1440,38 @@ Consensus::phaseEstablish() // consensus close logic and nodes inject partial/zero entropy // while others are still collecting — causing ledger mismatches. // + // However, if we've already converged on the txSet (which we + // have — haveConsensus() passed above) and there aren't enough + // participants to ever reach quorum, skip immediately. With + // 3 nodes and quorum=3, losing one node means 2/3 commits + // forever — waiting 3s per round just delays recovery. + // // NOTE: Late-joining nodes (e.g. restarting after a crash) // cannot help here. They enter the round as proposing=false // and onClose() skips commitment generation for non-proposers. // It takes at least one full round of observing before - // consensus promotes them to proposing. So waiting beyond - // a few seconds is pointless — use rngPIPELINE_TIMEOUT (3s) - // rather than ledgerMAX_CONSENSUS (10s) to avoid penalizing - // the recovery path. + // consensus promotes them to proposing. { - bool timeout = - result_->roundTime.read() > parms.rngPIPELINE_TIMEOUT; - if (!timeout) - return; // Wait for more commits - // On timeout: fall through to normal close (zero entropy) + // participants = peers + ourselves + auto const participants = currPeerPositions_.size() + 1; + auto const threshold = adaptor_.quorumThreshold(); + bool const impossible = participants < threshold; + + if (impossible) + { + JLOG(j_.debug()) + << "RNG: skipping commit wait (participants=" + << participants << " < threshold=" << threshold << ")"; + // Fall through to close with zero entropy + } + else + { + bool timeout = + result_->roundTime.read() > parms.rngPIPELINE_TIMEOUT; + if (!timeout) + return; // Wait for more commits + // On timeout: fall through to normal close (zero entropy) + } } } else if (estState_ == EstablishState::ConvergingCommit)