test(testnet): assert zero entropy in degraded ledgers

This commit is contained in:
Nicholas Dudfield
2026-04-10 12:04:46 +07:00
parent d94079d762
commit 04077c1a55

View File

@@ -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")