mirror of
https://github.com/Xahau/xahaud.git
synced 2026-06-22 10:06:39 +00:00
test(rng): pin tier-2 threshold to pre-nUNL original view
tier2Threshold() anchors to originalViewSize (pre-nUNL), not the effective post-nUNL size(): nUNL shrinks the effective view while leaving faulty nodes in it, so a sub-quorum fraction of the effective view can exceed the Byzantine bound. A one-line regression to size() would relabel a forkable cohort as participant_aligned -- and no existing test caught it (the only nUNL test never calls tier2Threshold()/selectEntropy, and the only tier-2 selection test has no nUNL, so original == effective there). Add testTier2ThresholdAnchorsToOriginalView: 8 active validators, 2 disabled via NegativeUNL (original 8, effective 6), asserting tier2Threshold()==5 (from the original 8) not 4 (from the effective 6), with quorum/gate cross-checks. Mutation-verified: flipping the production read to size() turns exactly this test red (tier2Threshold + entropyGateThreshold) while the existing tier-2 selection test stays green. Also: pointer comment at tier2Threshold() linking the invariant to its guard test, and a design-doc fix -- the worked example used n=5, where the band is empty (quorum == participant_aligned == 4) so it illustrated an unreachable tier; now n=6.
This commit is contained in:
@@ -953,7 +953,8 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
// The defining safety invariant, for EVERY view size: two aligned
|
||||
// cohorts always share an honest validator, i.e. their overlap (2t - n)
|
||||
// strictly exceeds the tolerated Byzantine count f = floor(n/5). And
|
||||
// the Tier 2 bar is never stricter than the Tier 3 validator_quorum bar.
|
||||
// the Tier 2 bar is never stricter than the Tier 3 validator_quorum
|
||||
// bar.
|
||||
for (std::size_t n = 1; n <= 256; ++n)
|
||||
{
|
||||
auto const t = calculateParticipantThreshold(n);
|
||||
@@ -1033,7 +1034,8 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(zeroTx);
|
||||
if (zeroTx)
|
||||
{
|
||||
// Tier 1 consensus_fallback digest over (prevLedgerHash, base set, seq).
|
||||
// Tier 1 consensus_fallback digest over (prevLedgerHash, base set,
|
||||
// seq).
|
||||
auto const expectedFallback = sha512Half(
|
||||
HashPrefix::entropyFallback,
|
||||
ledger->info().hash,
|
||||
@@ -1355,6 +1357,66 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(f.second == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testTier2ThresholdAnchorsToOriginalView()
|
||||
{
|
||||
testcase(
|
||||
"Tier 2 threshold anchors to pre-nUNL original view under "
|
||||
"NegativeUNL");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{
|
||||
*this,
|
||||
envconfig(validator, ""),
|
||||
supported_amendments() | featureConsensusEntropy |
|
||||
featureNegativeUNL,
|
||||
nullptr};
|
||||
forceNonStandalone(env.app());
|
||||
|
||||
// Eight active validators with two disabled via NegativeUNL: the
|
||||
// original-UNL denominator is 8 while the effective view is 6. The two
|
||||
// anchor DIFFERENT participant thresholds, which is the whole reason
|
||||
// Tier 2 keys off the original view:
|
||||
// tier2Threshold(original 8) == 5 (the equivocation-safe floor)
|
||||
// tier2Threshold(effective 6) == 4 (what a regression to size()
|
||||
// gives)
|
||||
// A 4-of-original-8 cohort has overlap 2*4 - 8 == 0, which does NOT
|
||||
// exceed f = floor(8/5) = 1 -- labelling it participant_aligned would
|
||||
// be forkable. Anchoring to the original view suppresses that mint;
|
||||
// this test pins the anchor so a future refactor to size() fails
|
||||
// loudly.
|
||||
constexpr std::size_t kActive = 8;
|
||||
constexpr std::size_t kDisabled = 2;
|
||||
std::vector<PublicKey> activeKeys;
|
||||
activeKeys.reserve(kActive);
|
||||
for (std::size_t i = 0; i < kActive; ++i)
|
||||
activeKeys.push_back(randomKeyPair(KeyType::secp256k1).first);
|
||||
std::vector<PublicKey> const disabledKeys(
|
||||
activeKeys.begin(), activeKeys.begin() + kDisabled);
|
||||
|
||||
auto const viewLedger =
|
||||
makeUNLReportLedger(env, activeKeys, disabledKeys);
|
||||
BEAST_EXPECT(viewLedger->rules().enabled(featureNegativeUNL));
|
||||
|
||||
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
||||
ce.cacheUNLReport(viewLedger);
|
||||
auto const view = ce.activeValidatorView();
|
||||
|
||||
BEAST_EXPECT(view->fromUNLReport);
|
||||
BEAST_EXPECT(view->originalViewSize == kActive); // 8, pre-nUNL
|
||||
BEAST_EXPECT(view->size() == kActive - kDisabled); // 6, effective
|
||||
|
||||
// The anchor. Tier 2 derives from originalViewSize (8 -> 5), NOT the
|
||||
// effective size (6 -> 4); the gate is min(quorum, tier2) and quorum
|
||||
// tracks the effective view (6 -> 5), so a regression to size() would
|
||||
// drop both the floor and the gate to 4.
|
||||
BEAST_EXPECT(calculateParticipantThreshold(kActive) == 5);
|
||||
BEAST_EXPECT(calculateParticipantThreshold(kActive - kDisabled) == 4);
|
||||
BEAST_EXPECT(ce.tier2Threshold() == 5);
|
||||
BEAST_EXPECT(ce.quorumThreshold() == 5);
|
||||
BEAST_EXPECT(ce.entropyGateThreshold() == 5);
|
||||
}
|
||||
|
||||
void
|
||||
testProposalProofRoundTrip()
|
||||
{
|
||||
@@ -2956,6 +3018,7 @@ public:
|
||||
testOnPreBuildInjectsZeroEntropyFallback();
|
||||
testOnPreBuildInjectsEntropySetEntropy();
|
||||
testOnPreBuildTier2ParticipantAligned();
|
||||
testTier2ThresholdAnchorsToOriginalView();
|
||||
testProposalProofRoundTrip();
|
||||
testHarvestRngDataReplacementAndRejection();
|
||||
testExportSidecarBuildFetchAndMerge();
|
||||
|
||||
@@ -244,6 +244,8 @@ ConsensusExtensions::tier2Threshold() const
|
||||
// originalViewSize, not size(): nUNL can shrink the effective view while
|
||||
// leaving faulty nodes in it, so a fraction of the effective view could
|
||||
// exceed the Byzantine fraction (which is bounded over the original UNL).
|
||||
// Regressing this to size() is a consensus fork under nUNL and is pinned by
|
||||
// ConsensusExtensions_test::testTier2ThresholdAnchorsToOriginalView.
|
||||
auto const base = activeValidatorView()->originalViewSize;
|
||||
if (base == 0)
|
||||
return 1; // safety: need at least one aligned participant
|
||||
|
||||
@@ -171,13 +171,20 @@ If no entropy hash reaches the entropy gate threshold before the bounded
|
||||
deadline, the round must fall back to the Tier 1 consensus-bound digest. This
|
||||
is the safe degradation path, not a consensus failure.
|
||||
|
||||
Examples with five active validators, validator_quorum threshold four, and
|
||||
participant_aligned threshold four:
|
||||
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):
|
||||
|
||||
- Four honest validators align on one entropy hash and one validator advertises
|
||||
- Five honest validators align on one entropy hash and one validator advertises
|
||||
a bogus hash: proceed with validator_quorum entropy for the honest quorum.
|
||||
- Two validators advertise different bogus hashes and only three align on the
|
||||
honest hash: fall back to the Tier 1 digest.
|
||||
- Four validators align on the honest hash and two advertise different bogus
|
||||
hashes: proceed with participant_aligned (Tier 2) entropy — the aligned
|
||||
cohort is below the 80% quorum but at or above the intersection-safe floor,
|
||||
and its overlap (2*4 - 6 = 2 > floor(6/5) = 1) still shares an honest
|
||||
validator between any two such cohorts.
|
||||
- Three validators align on the honest hash and three fail to align: fall back
|
||||
to the Tier 1 digest.
|
||||
- No peer entropy hash is observed in time: fall back to the Tier 1 digest.
|
||||
|
||||
The fallback pseudo-transaction is deterministic — every node derives the same
|
||||
|
||||
Reference in New Issue
Block a user