fix(consensus): shorten RNG pipeline timeout to 3s for faster recovery

Add rngPIPELINE_TIMEOUT (3s) to replace ledgerMAX_CONSENSUS (10s) in
the commit/reveal quorum gates. Late-joining nodes enter as
proposing=false and cannot contribute commitments until promoted, so
waiting beyond a few seconds just delays the ZERO-entropy fallback and
penalizes recovery. Add inline comments documenting the late-joiner
constraint and SHAMap sync's role as a dropped-proposal safety net.
This commit is contained in:
Nicholas Dudfield
2026-02-06 14:04:53 +07:00
parent 382e6fa673
commit a9dffd38ff
3 changed files with 46 additions and 2 deletions

View File

@@ -439,6 +439,14 @@ RCLConsensus::Adaptor::onClose(
// Bootstrap commit-reveal: generate entropy and include commitment
// in our very first proposal so peers can collect it during consensus.
//
// This is gated on `proposing` — a node that just restarted enters
// as proposing=false (observing) and must watch at least one full
// round before consensus promotes it to proposing. During those
// observation rounds it cannot contribute to the RNG pipeline at
// all: no commitment, no reveal, no SHAMap entries. The surviving
// proposers will close those rounds with fewer commits (possibly
// falling back to ZERO entropy) until the rejoiner starts proposing.
if (proposing && prevLedger->rules().enabled(featureConsensusEntropy))
{
cacheActiveUNL();

View File

@@ -863,6 +863,16 @@ Consensus<Adaptor>::peerProposalInternal(
// built our own local set for diffing. During ConvergingTx all
// data arrives via proposal leaves — fetching a peer's commitSet
// before we have our own just generates unnecessary traffic.
//
// IMPORTANT: SHAMap fetch/diff/merge is a safety net for the
// rare case where active proposers have slightly different
// commit/reveal sets due to dropped proposals. It does NOT
// help late-joining nodes: a node that restarts mid-round
// enters as proposing=false and cannot generate commitments
// (onClose gates on proposing). It must observe for at least
// one full round before consensus promotes it to proposing.
// The primary data transport is proposals themselves — the
// SHAMap sync is belt-and-suspenders, not the critical path.
if constexpr (requires(Adaptor & a) {
a.fetchRngSetIfNeeded(std::optional<uint256>{});
})
@@ -1429,9 +1439,18 @@ Consensus<Adaptor>::phaseEstablish()
// Without this gate, execution falls through to the normal
// consensus close logic and nodes inject partial/zero entropy
// while others are still collecting — causing ledger mismatches.
//
// 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.
{
bool timeout =
result_->roundTime.read() > parms.ledgerMAX_CONSENSUS;
result_->roundTime.read() > parms.rngPIPELINE_TIMEOUT;
if (!timeout)
return; // Wait for more commits
// On timeout: fall through to normal close (zero entropy)
@@ -1457,7 +1476,7 @@ Consensus<Adaptor>::phaseEstablish()
else if (estState_ == EstablishState::ConvergingReveal)
{
bool timeout =
result_->roundTime.read() > parms.ledgerMAX_CONSENSUS;
result_->roundTime.read() > parms.rngPIPELINE_TIMEOUT;
bool ready = false;
if ((haveConsensus() && adaptor_.hasMinimumReveals()) || timeout)

View File

@@ -88,6 +88,23 @@ struct ConsensusParms
*/
std::chrono::milliseconds ledgerMAX_CONSENSUS = std::chrono::seconds{10};
/** Maximum time to wait for RNG commit/reveal quorum before giving up.
*
* This is intentionally shorter than ledgerMAX_CONSENSUS because
* waiting longer won't help: a node that missed the start of the
* round (e.g. restarting after a crash) enters as proposing=false
* and cannot generate commitments until consensus promotes it to
* proposing — which takes at least one full round of observing.
* Waiting the full 10s just delays the inevitable ZERO-entropy
* fallback and slows recovery for the restarting node (it can't
* catch up until the survivors close a ledger).
*
* 3s is long enough for commits to propagate on any reasonable
* network, but short enough that a missing-node scenario recovers
* quickly via the ZERO-entropy fallback path.
*/
std::chrono::milliseconds rngPIPELINE_TIMEOUT = std::chrono::seconds{3};
//! Minimum number of seconds to wait to ensure others have computed the LCL
std::chrono::milliseconds ledgerMIN_CLOSE = std::chrono::seconds{2};