fix(rng): require UNLReport view for non-fallback entropy

This commit is contained in:
Nicholas Dudfield committed 2026-06-17 11:32:51 +07:00
1 parent 23f745bd01
commit 8368e12ab3
5 files changed
+45 -57

No files matched your search

@@ -1061,9 +1061,11 @@ class ConsensusExtensions_test : public beast::unit_test::suite
auto const closeTime = NetClock::time_point{NetClock::duration{654}};
auto const txSetHash = makeHash("explicit-final-nonzero-txset");
auto const reveal = makeHash("explicit-final-nonzero-reveal");
auto const viewLedger = makeUNLReportLedger(
nonStandaloneEnv, std::vector<PublicKey>{publicKey});
ConsensusExtensions revealCe{
nonStandaloneEnv.app(), activeNoopJournal()};
revealCe.cacheUNLReport(ledger);
revealCe.cacheUNLReport(viewLedger);
harvestCommitReveal(
revealCe,
nodeId,
@@ -1163,7 +1165,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
ce.onRoundStart(RCLCxLedger{ledger}, {});
ce.setRngEnabledThisRound(true);
BEAST_EXPECT(ce.belowValidatorQuorum());
CanonicalTXSet retriableTxs{makeHash("rng-zero-fallback-salt")};
auto const seq = ledger->seq() + 1;
@@ -1224,8 +1225,10 @@ class ConsensusExtensions_test : public beast::unit_test::suite
auto const closeTime = NetClock::time_point{NetClock::duration{321}};
auto const txSetHash = makeHash("prebuild-entropy-txset");
auto const reveal = makeHash("prebuild-entropy-reveal");
auto const viewLedger =
makeUNLReportLedger(env, std::vector<PublicKey>{publicKey});
ConsensusExtensions ce{env.app(), activeNoopJournal()};
ce.cacheUNLReport(ledger);
ce.cacheUNLReport(viewLedger);
harvestCommitReveal(
ce,
nodeId,
@@ -1241,7 +1244,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite
auto const entropySetHash = ce.buildEntropySet(seq);
BEAST_EXPECT(ce.isSidecarSet(entropySetHash));
BEAST_EXPECT(!ce.belowValidatorQuorum());
CanonicalTXSet retriableTxs{makeHash("entropy-set-prebuild-salt")};
ce.onPreBuild(retriableTxs, seq, txSetHash);
@@ -2878,7 +2880,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite
BEAST_EXPECT(ce.exportSigConvergenceFailed());
ce.setEntropyFailed();
BEAST_EXPECT(ce.belowValidatorQuorum());
ce.generateEntropySecret();
BEAST_EXPECT(!ce.hasAnyReveals());
-15
View File
@@ -477,21 +477,6 @@ struct Peer
return !pendingReveals_.empty();
}
bool
shouldZeroEntropy() const
{
if (entropyFailed_ || pendingReveals_.empty())
return true;
// Legacy validator-quorum-only predicate retained for old CSF
// assertions. Current production tiering is handled in
// finalizeRoundEntropy(), which can mint participant_aligned below
// validator quorum when the advertised set reaches tier2Threshold().
auto const threshold = unlNodes_.empty()
? std::size_t{1}
: calculateQuorumThreshold(unlNodes_.size());
return pendingReveals_.size() < threshold;
}
uint256
buildCommitSet(Ledger::Seq seq)
{
+16 -20
View File
@@ -261,10 +261,10 @@ ConsensusExtensions::entropyGateThreshold() const
// (0.8*effective), so this is the participant-alignment floor and
// sub-quorum rounds reach injection; under heavy nUNL the band collapses
// (tier2 >= quorum) and this is the 80% quorum, so only validator_quorum
// survives. This governs
// proceed-vs-fall-back ONLY — the selector still labels the agreed set's
// tier from its participant count, so a node that proceeds here never mints
// a different tier than its peers (divergent local views fall back).
// survives. This governs proceed-vs-fall-back only. Non-fallback tier
// labels are allowed only when the round view is anchored by UNLReport; the
// trusted-fallback view is local configuration and selectEntropy() maps it
// to consensus_fallback.
return std::min(quorumThreshold(), tier2Threshold());
}
@@ -416,17 +416,6 @@ ConsensusExtensions::hasAnyReveals() const
return !pendingReveals_.empty();
}
bool
ConsensusExtensions::belowValidatorQuorum() const
{
if (entropyFailed_ || !entropySetMap_)
return true;
auto const leafCount =
std::distance(entropySetMap_->begin(), entropySetMap_->end());
return leafCount == 0 || leafCount < quorumThreshold();
}
ConsensusExtensions::EntropySelection
ConsensusExtensions::selectEntropy(
uint256 const& baseTxSetHash,
@@ -454,11 +443,18 @@ ConsensusExtensions::selectEntropy(
entropyTierValidatorQuorum,
20};
// No agreed entropy set (round failed, or none was built) → fallback. We do
// NOT fall back merely for being below the 80% quorum the way
// belowValidatorQuorum() does: a sub-quorum-but-aligned set may still
// qualify for participant_aligned (tier 2). The tier ladder below decides
// from the agreed participant count.
// Non-fallback entropy labels depend on validator-view thresholds. Without
// an on-ledger UNLReport, that view is derived from local trusted config,
// so two nodes can agree on the same entropy set but label it with
// different tiers. A non-standalone node therefore mints only
// consensus_fallback until the round view is ledger-anchored.
if (!activeValidatorView()->fromUNLReport)
return fallback();
// No agreed entropy set (round failed, or none was built) → fallback. A
// sub-quorum-but-aligned set may still qualify for participant_aligned
// (tier 2); the tier ladder below decides from the agreed participant
// count.
if (entropyFailed_ || !entropySetMap_)
return fallback();
+5 -10
View File
@@ -189,13 +189,6 @@ public:
bool
hasAnyReveals() const;
/// True when the agreed entropy set would NOT reach validator_quorum
/// (tier 3): absent/failed, or fewer leaves than quorumThreshold(). A
/// tier-3 eligibility predicate only — it does NOT account for the tier-2
/// floor, so never gate injection on it (selectEntropy() does the tiering).
bool
belowValidatorQuorum() const;
/// Result of the shared deterministic entropy selector: the digest to
/// inject plus its tier/count labels. Both injection paths derive these
/// identically from the AGREED entropySetMap_ so they cannot drift.
@@ -209,7 +202,9 @@ public:
/// Deterministically choose the entropy to inject for this round from the
/// AGREED entropySetMap_ (never local pendingReveals_), labelled by agreed
/// participant count: validator_quorum (>= quorumThreshold),
/// participant_aligned (>= tier2Threshold) or consensus_fallback.
/// participant_aligned (>= tier2Threshold) or consensus_fallback. In
/// non-standalone mode, non-fallback labels require an UNLReport-backed
/// active view; the trusted-fallback view is local config and mints Tier 1.
/// baseTxSetHash is the BASE (pre-injection) tx set hash used for the
/// fallback digest.
EntropySelection
@@ -342,8 +337,8 @@ public:
clearRngState();
/// txSetHash is the BASE (pre-injection) consensus tx set hash —
/// an input to the Tier 1 consensus_fallback digest. It must never be the hash
/// of a set that could contain the entropy pseudo-tx itself.
/// an input to the Tier 1 consensus_fallback digest. It must never be the
/// hash of a set that could contain the entropy pseudo-tx itself.
void
onPreBuild(
CanonicalTXSet& retriableTxs,
@@ -173,8 +173,8 @@ is the safe degradation path, not a consensus failure.
Examples with six active validators, validator_quorum threshold five, and
participant_aligned threshold four (six is the smallest view with a non-empty
Tier 2 band — at five validators quorum and participant_aligned coincide at
four, leaving no band):
Tier 2 band and non-zero tolerated Byzantine count; at five validators quorum
and participant_aligned coincide at four, leaving no band):
- Five honest validators align on one entropy hash and one validator advertises
a bogus hash: proceed with validator_quorum entropy for the honest quorum.
@@ -194,11 +194,22 @@ seq)` — and labeled with `EntropyTier = consensus_fallback` and
`min_tier`/`min_count` arguments to `dice()`/`random()`: a hook that demands
validator-tier entropy fails closed with `TOO_LITTLE_ENTROPY` on fallback
ledgers, while a hook that opts into fallback-grade randomness must do so
explicitly at the call site. The fallback digest derives from the BASE
(pre-injection) tx set hash to avoid circularity, and entropy pseudo-tx
deduplication is value-based: if an explicit-final synthetic set already
contains the exact pseudo-tx, injection skips it; a present-but-different
pseudo-tx is logged as a determinism violation and left in the agreed set.
explicitly at the call site. Valid `min_tier` values are the stored entropy
tiers 1..3; `min_count` must fit the on-ledger `EntropyCount` UINT16 field.
Invalid requirements return `INVALID_ARGUMENT`, while valid-but-unmet
requirements return `TOO_LITTLE_ENTROPY`.
Open-ledger hook execution is provisional. During speculative open-ledger
execution, `dice()`/`random()` can only use the previous ledger's finalized
entropy; final buildLCL execution sees the current ledger's entropy pseudo-tx
after it updates the SLE. Hooks that need final entropy must treat open-ledger
RNG results as previews.
The fallback digest derives from the BASE (pre-injection) tx set hash to avoid
circularity, and entropy pseudo-tx deduplication is value-based: if an
explicit-final synthetic set already contains the exact pseudo-tx, injection
skips it; a present-but-different pseudo-tx is logged as a determinism
violation and left in the agreed set.
## Sidecar Convergence Rules