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)