diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 56132b7e8..469f7e3ca 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -454,7 +454,7 @@ RCLConsensus::Adaptor::onClose( // falling back to ZERO entropy) until the rejoiner starts proposing. if (proposing && prevLedger->rules().enabled(featureConsensusEntropy)) { - cacheActiveUNL(); + cacheUNLReport(); generateEntropySecret(); pos.myCommitment = sha512Half( myEntropySecret_, @@ -1154,25 +1154,15 @@ RCLConsensus::Adaptor::updateOperatingMode(std::size_t const positions) const std::size_t RCLConsensus::Adaptor::quorumThreshold() const { - // 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. + // Prefer expected proposers (recent proposers ∩ UNL) — this + // adapts to actual network conditions rather than relying on + // the potentially stale UNL Report. Falls back to full + // UNL Report for cold boot (first round). // - // 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(); + // Round 1: threshold based on full UNL (conservative) + // Round 2+: threshold based on who actually proposed last round + auto const base = expectedProposers_.empty() ? unlReportNodeIds_.size() + : expectedProposers_.size(); if (base == 0) return 1; // safety: need at least one commit return (base * 80 + 99) / 100; @@ -1183,26 +1173,33 @@ RCLConsensus::Adaptor::setExpectedProposers(hash_set proposers) { if (!proposers.empty()) { - // Recent proposers from last round — best signal for who's active. - // Always include ourselves. - proposers.insert(validatorKeys_.nodeID); - expectedProposers_ = std::move(proposers); + // Intersect with active UNL — only expect commits from + // validators we trust. Non-UNL proposers are ignored. + hash_set filtered; + for (auto const& id : proposers) + { + if (unlReportNodeIds_.count(id)) + filtered.insert(id); + } + filtered.insert(validatorKeys_.nodeID); + expectedProposers_ = std::move(filtered); JLOG(j_.debug()) << "RNG: expectedProposers from recent proposers: " - << expectedProposers_.size(); + << expectedProposers_.size() << " (filtered from " + << proposers.size() << ")"; return; } - // First round (no previous proposers): fall back to activeUNL. - // cacheActiveUNL() was called just before this, so it's populated. - if (!activeUNLNodeIds_.empty()) + // First round (no previous proposers): fall back to UNL Report. + // cacheUNLReport() was called just before this, so it's populated. + if (!unlReportNodeIds_.empty()) { - expectedProposers_ = activeUNLNodeIds_; - JLOG(j_.debug()) << "RNG: expectedProposers from activeUNL: " + expectedProposers_ = unlReportNodeIds_; + JLOG(j_.debug()) << "RNG: expectedProposers from UNL Report: " << expectedProposers_.size(); return; } - // No data at all (shouldn't happen — cacheActiveUNL falls back to + // No data at all (shouldn't happen — cacheUNLReport falls back to // trusted keys). Leave empty → hasQuorumOfCommits uses 80% fallback. JLOG(j_.warn()) << "RNG: no expectedProposers available"; } @@ -1277,7 +1274,7 @@ RCLConsensus::Adaptor::buildCommitSet(LedgerIndex seq) for (auto const& [nodeId, commit] : pendingCommits_) { - if (!isActiveUNLMember(nodeId)) + if (!isUNLReportMember(nodeId)) continue; auto kit = nodeIdToKey_.find(nodeId); @@ -1329,7 +1326,7 @@ RCLConsensus::Adaptor::buildEntropySet(LedgerIndex seq) for (auto const& [nodeId, reveal] : pendingReveals_) { - if (!isActiveUNLMember(nodeId)) + if (!isUNLReportMember(nodeId)) continue; auto kit = nodeIdToKey_.find(nodeId); @@ -1407,16 +1404,16 @@ RCLConsensus::Adaptor::clearRngState() commitSetMap_.reset(); entropySetMap_.reset(); pendingRngFetches_.clear(); - activeUNLNodeIds_.clear(); + unlReportNodeIds_.clear(); expectedProposers_.clear(); commitProofs_.clear(); proposalProofs_.clear(); } void -RCLConsensus::Adaptor::cacheActiveUNL() +RCLConsensus::Adaptor::cacheUNLReport() { - activeUNLNodeIds_.clear(); + unlReportNodeIds_.clear(); // Try UNL Report from the validated ledger if (auto const prevLedger = ledgerMaster_.getValidatedLedger()) @@ -1430,7 +1427,7 @@ RCLConsensus::Adaptor::cacheActiveUNL() auto const pk = obj.getFieldVL(sfPublicKey); if (publicKeyType(makeSlice(pk))) { - activeUNLNodeIds_.insert( + unlReportNodeIds_.insert( calcNodeID(PublicKey(makeSlice(pk)))); } } @@ -1439,24 +1436,24 @@ RCLConsensus::Adaptor::cacheActiveUNL() } // Fallback to normal UNL if no report or empty - if (activeUNLNodeIds_.empty()) + if (unlReportNodeIds_.empty()) { for (auto const& masterKey : app_.validators().getTrustedMasterKeys()) { - activeUNLNodeIds_.insert(calcNodeID(masterKey)); + unlReportNodeIds_.insert(calcNodeID(masterKey)); } } // Always include ourselves - activeUNLNodeIds_.insert(validatorKeys_.nodeID); + unlReportNodeIds_.insert(validatorKeys_.nodeID); - JLOG(j_.debug()) << "RNG: cacheActiveUNL size=" << activeUNLNodeIds_.size(); + JLOG(j_.debug()) << "RNG: cacheUNLReport size=" << unlReportNodeIds_.size(); } bool -RCLConsensus::Adaptor::isActiveUNLMember(NodeID const& nodeId) const +RCLConsensus::Adaptor::isUNLReportMember(NodeID const& nodeId) const { - return activeUNLNodeIds_.count(nodeId) > 0; + return unlReportNodeIds_.count(nodeId) > 0; } bool @@ -1543,7 +1540,7 @@ RCLConsensus::Adaptor::handleAcquiredRngSet(std::shared_ptr const& map) NodeID nodeId; std::memcpy(nodeId.data(), acctId.data(), nodeId.size()); - if (!isActiveUNLMember(nodeId)) + if (!isUNLReportMember(nodeId)) { JLOG(j_.debug()) << "RNG: rejecting non-UNL entry from " << nodeId << " in acquired set"; @@ -1599,7 +1596,7 @@ RCLConsensus::Adaptor::handleAcquiredRngSet(std::shared_ptr const& map) NodeID nodeId; std::memcpy(nodeId.data(), acctId.data(), nodeId.size()); - if (!isActiveUNLMember(nodeId)) + if (!isUNLReportMember(nodeId)) { JLOG(j_.debug()) << "RNG: rejecting non-UNL entry from " << nodeId << " in acquired set"; @@ -1758,7 +1755,7 @@ RCLConsensus::Adaptor::harvestRngData( << " reveal=" << (position.myReveal ? "yes" : "no"); // Reject data from validators not in the active UNL - if (!isActiveUNLMember(nodeId)) + if (!isUNLReportMember(nodeId)) { JLOG(j_.debug()) << "RNG: rejecting data from non-UNL validator " << nodeId; diff --git a/src/ripple/app/consensus/RCLConsensus.h b/src/ripple/app/consensus/RCLConsensus.h index bcbefdfc3..8a2d2aa2e 100644 --- a/src/ripple/app/consensus/RCLConsensus.h +++ b/src/ripple/app/consensus/RCLConsensus.h @@ -105,10 +105,10 @@ class RCLConsensus hash_set pendingRngFetches_; // Cached set of NodeIDs from UNL Report (or fallback UNL) - hash_set activeUNLNodeIds_; + hash_set unlReportNodeIds_; // Expected proposers for commit quorum — derived from last round's - // actual proposers (best signal), falling back to activeUNL. + // actual proposers (best signal), falling back to UNL Report. hash_set expectedProposers_; /** Proof data from a proposal signature, for embedding in SHAMap @@ -228,7 +228,7 @@ class RCLConsensus quorumThreshold() const; /** Set expected proposers for this round's commit quorum. - Cascade: recent proposers > activeUNL > (empty = 80% fallback). + Cascade: recent proposers > UNL Report > (empty = 80% fallback). */ void setExpectedProposers(hash_set proposers); @@ -279,11 +279,11 @@ class RCLConsensus Reads from UNL Report (in-ledger), falls back to normal UNL. */ void - cacheActiveUNL(); + cacheUNLReport(); /** Check if a NodeID is in the active UNL for this round */ bool - isActiveUNLMember(NodeID const& nodeId) const; + isUNLReportMember(NodeID const& nodeId) const; /** Generate new entropy secret for this round */ void diff --git a/src/ripple/consensus/Consensus.h b/src/ripple/consensus/Consensus.h index 0a6027d0c..715f68e41 100644 --- a/src/ripple/consensus/Consensus.h +++ b/src/ripple/consensus/Consensus.h @@ -744,8 +744,8 @@ Consensus::startRoundInternal( // Populate UNL cache for all nodes (including observers). // onClose only caches for proposing validators, so observers // would otherwise have an empty set and reject all RNG data. - adaptor_.cacheActiveUNL(); - // Set expected proposers: recent proposers > activeUNL > 80% fallback + adaptor_.cacheUNLReport(); + // Set expected proposers: recent proposers > UNL Report > 80% fallback adaptor_.setExpectedProposers(std::move(lastProposers)); }