From 04077c1a5559cd56e49872afa8697595a4b76cca Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Fri, 10 Apr 2026 12:04:46 +0700 Subject: [PATCH] test(testnet): assert zero entropy in degraded ledgers --- .../entropy/quorum_degradation_smoke.py | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/.testnet/scenarios/entropy/quorum_degradation_smoke.py b/.testnet/scenarios/entropy/quorum_degradation_smoke.py index 53f455dab..0cd1442c1 100644 --- a/.testnet/scenarios/entropy/quorum_degradation_smoke.py +++ b/.testnet/scenarios/entropy/quorum_degradation_smoke.py @@ -1,4 +1,4 @@ -""":descr: 4/5 liveness, 3/5 stall, zero entropy (bootstrap skip), recovery""" +""":descr: 4/5 liveness, 3/5 zero-entropy fallback, recovery""" from __future__ import annotations @@ -20,21 +20,36 @@ async def scenario(ctx, log): # Snapshot validated seq before dropping to 3/5. val_before = ctx.validated_ledger_index(0) - # --- 3/5 validation stall --- + # --- 3/5 degraded window --- ctx.stop_node(3) await ctx.wait_for_nodes_down(nodes=[3], timeout=30) - # 10s ≈ 3 rounds at 3s cadence — enough to validate if quorum existed. + # 10s ≈ 3 rounds at 3s cadence. await ctx.sleep(10) val_after = ctx.validated_ledger_index(0) log(f"3/5: validated ledger {val_before} → {val_after}") - if val_after and val_before and val_after > val_before: - raise AssertionError( - f"Validated ledger advanced ({val_before}→{val_after}) " - f"with only 3/5 validators" - ) + # Accepted/built ledgers may still later appear as validated once the full + # network rejoins. For ConsensusEntropy the key invariant is that every + # ledger created during this sub-quorum window carries ZERO entropy. + degraded_zero = 0 + degraded_end = val_after or val_before + if val_before and degraded_end and degraded_end > val_before: + for seq in range(val_before + 1, degraded_end + 1): + ce, _ = get_entropy_tx(ctx, seq) + digest, entropy_count, is_zero = entropy_fields(ce) + + if not is_zero: + raise AssertionError( + f"Ledger {seq}: expected ZERO entropy during 3/5 window, " + f"got Digest={digest[:16]}... EntropyCount={entropy_count}" + ) + + degraded_zero += 1 + log(f" Degraded ledger {seq}: EntropyCount={entropy_count} ZERO") + + log(f"3/5 entropy summary: {degraded_zero} zero") # Log checks tied to actual transition mechanics: # - seq=1 proposals are emitted once commit-set phase is entered @@ -64,20 +79,21 @@ async def scenario(ctx, log): await ctx.wait_for_ledgers(1, node_id=0, timeout=120) val_recovered = ctx.validated_ledger_index(0) - log(f"Recovered: validated seq {val_before} → {val_recovered}") + pre_recovery = max(v for v in [val_before, val_after] if v is not None) + log(f"Recovered: validated seq {pre_recovery} → {val_recovered}") - if not val_recovered or not val_before or val_recovered <= val_before: + if not val_recovered or val_recovered <= pre_recovery: raise AssertionError( f"Validated ledger did not advance after recovery " - f"({val_before} → {val_recovered})" + f"({pre_recovery} → {val_recovered})" ) - # Inspect recovery ledgers: during the stall, entropy falls to zero - # (bootstrap skip). The first ledger after val_before may still carry - # real entropy from an in-flight round before node 3 went down. + # Inspect post-recovery ledgers separately from the degraded window above. + # Once the network is back at quorum, non-zero entropy is valid again but + # must still be quorum-met. zero_count = 0 nonzero_count = 0 - for seq in range(val_before + 1, val_recovered + 1): + for seq in range(pre_recovery + 1, val_recovered + 1): ce, _ = get_entropy_tx(ctx, seq) digest, entropy_count, is_zero = entropy_fields(ce) @@ -98,13 +114,4 @@ async def scenario(ctx, log): log(f"Entropy summary: {zero_count} zero, {nonzero_count} non-zero") - # With quorum-gated entropy, sub-quorum reveals produce zero entropy. - # Allow at most 1 non-zero in case node 3 completes the full pipeline - # (commit + reveal) before dying — that's a valid quorum-met round. - if nonzero_count > 1: - raise AssertionError( - f"Expected at most 1 non-zero entropy ledger during stall, " - f"got {nonzero_count}" - ) - log("PASS")