aere-research/formal-consensus/registry_rotation_coverage_smt.py
Aere Network 4ce1928d9e Three formal models for the blocking post-quantum consensus, each with its negative control
Written and run against z3 4.16.0, exit 0, on 2026-08-15, the day the blocking
fork went live:
- anchor_blocking_quorum: past block 14,050,000 no anchor block finalizes with
  fewer than K=3 distinct valid Falcon seals; the negative control that permits
  2 seals is SAT (the violation is expressible), the property itself UNSAT.
- pqanchor_ceiling_monotonicity: the emergency ceiling can only LOWER the
  effective threshold, never raise it, and K_eff always fits under the write
  cap; a planted max()-instead-of-min() ceiling is SAT.
- registry_rotation_coverage: the registry schedule covers every anchor height
  with exactly one registry, no gap and no overlap, and a seal is checked
  against the registry active at the ANCHOR height; a planted schedule with a
  gap is SAT.
These extend the existing SMT corpus toward end-to-end verifiability of the
consensus, one of the pieces the roadmap calls distinctive.
2026-08-15 21:05:47 +03:00

324 lines
15 KiB
Python

#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# registry_rotation_coverage_smt.py
#
# MACHINE-CHECKED (z3) ROTATION COVERAGE for the AERE PQ anchor key registries
# (chain 2800). The Falcon seal registry rotates on block height:
#
# R1 activates at 13,014,000 with 7 validator keys
# R2 activates at 13,600,000 with 9 validator keys
#
# INVARIANT (the task target):
# (C1) every anchor height H >= 13,014,000 (anchors every 32 blocks from
# 13,014,000) is covered by EXACTLY one active registry: no height with
# zero registries (gap) and no height with two (overlap);
# (C2) seal validation selects the registry active at the ANCHOR height,
# never at the validator's CURRENT head height. A historical seal's
# verdict therefore never changes as the chain advances past a rotation
# boundary (replay stability), and a key that joined in R2 can never be
# used to seal an R1-era anchor (no new-key backdating).
#
# Chain facts modelled as law (measured on the live chain, 2026-08):
# N = 9 validators, QBFT quorum = ceil(2N/3) = 6
# anchor every 32 blocks from 13,014,000
# K schedule 13,014,000:0 then 13,034,000:3 (K=3 minimum Falcon seals)
# write cap maxSeals = 5; emergency minSealsCeiling can only LOWER K
# blocking fork at 14,050,000: no block finalizes without the Falcon quorum
# registry rotation 13,014,000 -> 7 keys, 13,600,000 -> 9 keys
#
# Schedule semantics (the design being proved): a registry is active from its
# own activation height up to, but excluding, the next registry's activation
# height; the last registry stays active forever. Coverage is by INTERVAL, so
# a rotation boundary that is not itself anchor-aligned loses no anchor (and
# 13,600,000 is NOT anchor-aligned; proved below, with the interval argument
# closing the hole).
#
# Method: each property is a function that builds the constraints; the
# property's NEGATION is asserted, so UNSAT means the property HOLDS. Each
# load-bearing check is paired with a NEGATIVE CONTROL that plants a violation
# (a gapped schedule, an overlapping schedule, a current-height lookup) and
# MUST come back SAT with a concrete witness; if a negative control did not
# fire, the model would be vacuous.
#
# ASSUMPTIONS (bound every PROVED below):
# A1. R2 retains R1's 7 keys (indices 0..6) and adds two (indices 7..8),
# matching the fleet growth from 7 to 9 validators. Only the DIFFERENCE
# (indices 7..8 exist solely in R2) is load-bearing for the backdating
# and divergence results.
# A2. Heights are unbounded nonnegative integers (LIA); "anchor height" means
# H = 13,014,000 + 32k for some integer k >= 0.
#
# HONEST BOUNDARY: this proves the SCHEDULE ARITHMETIC and the lookup-keying
# design, unbounded in height over LIA. It does not re-prove Falcon signature
# soundness (falcon_*_smt.py) nor QBFT agreement (qbft_safety_smt.py), and it
# checks the design, not the Besu Java bytecode.
# -----------------------------------------------------------------------------
from z3 import Int, Bool, Solver, And, Or, Not, Implies, If, sat, unsat
# ---- chain constants (the law) ----------------------------------------------
N_VALIDATORS = 9
QBFT_QUORUM = 6 # ceil(2*9/3)
ANCHOR0 = 13_014_000 # first anchor height and R1 activation
ANCHOR_STEP = 32
K_STEP_HEIGHT = 13_034_000 # K schedule: 13014000:0, 13034000:3
K_MIN_SEALS = 3
MAX_SEALS = 5 # write cap
FORK_BLOCKING = 14_050_000 # no finality without the Falcon quorum from here
REG1_HEIGHT = 13_014_000
REG1_KEYS = 7
REG2_HEIGHT = 13_600_000
REG2_KEYS = 9
NONE, R1, R2 = 0, 1, 2
# ---- schedule semantics: active-from-own-height-until-next ------------------
def active_r1(h):
return And(h >= REG1_HEIGHT, h < REG2_HEIGHT)
def active_r2(h):
return h >= REG2_HEIGHT
def coverage_count(h):
return If(active_r1(h), 1, 0) + If(active_r2(h), 1, 0)
def lookup(h):
# registry selected for height h, derived from the SAME active predicates
return If(active_r2(h), R2, If(active_r1(h), R1, NONE))
def reg_size(reg):
return If(reg == R1, REG1_KEYS, If(reg == R2, REG2_KEYS, 0))
def member(reg, kx):
# A1: R2 = R1's keys plus indices 7..8
return Or(And(reg == R1, kx >= 0, kx <= REG1_KEYS - 1),
And(reg == R2, kx >= 0, kx <= REG2_KEYS - 1))
def add_anchor(s, h, kname):
# h is an anchor height: h = ANCHOR0 + 32k, k >= 0
k = Int(kname)
s.add(h == ANCHOR0 + ANCHOR_STEP * k, k >= 0)
return k
results = []
def check(name, s, expect_unsat=True, kind="PROOF"):
r = s.check()
if expect_unsat:
ok = (r == unsat); tag = "PROVED" if ok else "FAILED"
else:
ok = (r == sat); tag = "CEX-FOUND" if ok else "FAILED"
results.append((name, tag, ok, kind))
print(f"[{tag:9}] ({kind}) {name}: z3={r} (expected {'unsat' if expect_unsat else 'sat'})")
if r == sat:
m = s.model()
wit = {}
for d in m.decls():
try: wit[str(d)] = m[d].as_long()
except Exception:
try: wit[str(d)] = bool(m[d])
except Exception: wit[str(d)] = "?"
print(" witness:", {k: wit[k] for k in sorted(wit)})
return ok
print("### AERE PQ anchor -- REGISTRY ROTATION COVERAGE"
" (R1@13,014,000 x7 keys, R2@13,600,000 x9 keys)\n")
# =============================================================================
# C1a NO GAP: every anchor height >= 13,014,000 has at least one registry
# -----------------------------------------------------------------------------
s = Solver()
h = Int('h')
add_anchor(s, h, 'k')
s.add(coverage_count(h) == 0) # negate: an anchor with NO registry
check("C1a no gap: every anchor height >= 13,014,000 has an active registry "
"(unbounded in height)", s)
# =============================================================================
# C1b NO OVERLAP: no anchor height has two active registries
# -----------------------------------------------------------------------------
s = Solver()
h = Int('h')
add_anchor(s, h, 'k')
s.add(coverage_count(h) >= 2) # negate: an anchor with TWO registries
check("C1b no overlap: no anchor height has two active registries "
"(unbounded in height)", s)
# =============================================================================
# C1c EXACTLY ONE, stated directly (implied by C1a+C1b; kept as the invariant
# as worded)
# -----------------------------------------------------------------------------
s = Solver()
h = Int('h')
add_anchor(s, h, 'k')
s.add(coverage_count(h) != 1)
check("C1c exact coverage: every anchor height is covered by EXACTLY one "
"registry", s)
# =============================================================================
# BOUNDARY ARITHMETIC: the rotation boundary 13,600,000 is NOT anchor-aligned
# (13,600,000 - 13,014,000 = 586,000 = 32*18312 + 16), and interval coverage
# makes that harmless: C1a already proved no anchor is lost around it. The
# K-step height and the blocking-fork height ARE anchor-aligned.
# -----------------------------------------------------------------------------
s = Solver()
k = Int('k')
s.add(REG2_HEIGHT == ANCHOR0 + ANCHOR_STEP * k) # negate alignment: no such k
check("boundary: R2 activation 13,600,000 is NOT an anchor height "
"(no k with 13,600,000 = 13,014,000 + 32k); interval coverage closes "
"the hole (C1a)", s)
s = Solver()
k, r = Int('k'), Int('r')
s.add(K_STEP_HEIGHT == ANCHOR0 + ANCHOR_STEP * k + r, r >= 1, r <= 31)
check("boundary: K-step height 13,034,000 IS anchor-aligned "
"(remainder 1..31 is infeasible)", s)
s = Solver()
k, r = Int('k'), Int('r')
s.add(FORK_BLOCKING == ANCHOR0 + ANCHOR_STEP * k + r, r >= 1, r <= 31)
check("boundary: blocking fork 14,050,000 IS anchor-aligned "
"(remainder 1..31 is infeasible)", s)
# =============================================================================
# C2a REPLAY STABILITY: with the ANCHOR-keyed rule, a seal's verdict on an
# anchor at height a is identical at any two current head heights c1, c2.
# Holds by construction (the rule never reads the current height); the
# paired NEG-CTRL 3 shows the current-height rule breaks exactly this,
# so the keying choice is the load-bearing element.
# -----------------------------------------------------------------------------
s = Solver()
a, c1, c2, kx = Int('a'), Int('c1'), Int('c2'), Int('kx')
add_anchor(s, a, 'k')
s.add(c1 >= a, c2 >= a, kx >= 0, kx <= REG2_KEYS - 1)
v1 = member(lookup(a), kx) # verdict as computed at head c1
v2 = member(lookup(a), kx) # verdict as computed at head c2
s.add(v1 != v2) # negate: verdict changed with the head
check("C2a replay stability: anchor-keyed verdict is independent of the "
"current head height", s)
# =============================================================================
# C2b NO NEW-KEY BACKDATING: a key that exists only in R2 (index 7..8) can
# never validate a seal on an R1-era anchor, no matter how far past the
# rotation the validating node's head is.
# -----------------------------------------------------------------------------
s = Solver()
a, c, kx = Int('a'), Int('c'), Int('kx')
add_anchor(s, a, 'k')
s.add(a < REG2_HEIGHT) # R1-era anchor
s.add(kx >= REG1_KEYS, kx <= REG2_KEYS - 1) # key added in R2 only
s.add(c >= REG2_HEIGHT) # validator already lives in the R2 era
s.add(member(lookup(a), kx)) # negate: the seal is ACCEPTED
check("C2b no backdating: an R2-only key is rejected on every R1-era anchor "
"(anchor-keyed rule)", s)
# =============================================================================
# LAW CROSS-CHECKS: the blocking era is served entirely by R2, and the demanded
# seal count can always be supplied by the active registry.
# -----------------------------------------------------------------------------
# every anchor at/after the blocking fork is covered by R2 and ONLY R2
s = Solver()
h = Int('h')
add_anchor(s, h, 'k')
s.add(h >= FORK_BLOCKING)
s.add(Or(Not(active_r2(h)), active_r1(h))) # negate: not-R2 or still-R1
check("blocking era: every anchor >= 14,050,000 is covered by the 9-key R2 "
"and only by it", s)
# the active registry at any blocking-era anchor can supply the Falcon quorum
s = Solver()
h = Int('h')
add_anchor(s, h, 'k')
s.add(h >= FORK_BLOCKING)
s.add(reg_size(lookup(h)) < QBFT_QUORUM) # negate: registry too small
check("blocking era: active registry size (9) >= Falcon quorum (6) at every "
"blocking-era anchor", s)
# minSealsCeiling only LOWERS K; effective K never exceeds K=3, the write cap
# maxSeals=5, or the size of the registry active at any covered anchor
s = Solver()
h, ceil_ = Int('h'), Int('ceil')
add_anchor(s, h, 'k')
s.add(ceil_ >= 0)
k_eff = If(ceil_ < K_MIN_SEALS, ceil_, K_MIN_SEALS)
s.add(Or(k_eff > K_MIN_SEALS,
k_eff > MAX_SEALS,
k_eff > reg_size(lookup(h))))
check("K discipline: effective K (ceiling can only lower 3) never exceeds "
"K=3 <= maxSeals=5 <= active registry size, at every covered anchor", s)
# ============================ NEGATIVE CONTROLS ==============================
# ---- NEG-CTRL 1 (the task's required control): a GAPPED schedule, R1 ends at
# 13,500,000 but R2 only starts at 13,600,000, MUST make the search for an
# uncovered anchor SAT (z3 exhibits a concrete orphaned anchor height).
GAP_END = 13_500_000
def buggy_gap_r1(h): return And(h >= REG1_HEIGHT, h < GAP_END)
s = Solver()
h = Int('h')
add_anchor(s, h, 'k')
s.add(If(buggy_gap_r1(h), 1, 0) + If(active_r2(h), 1, 0) == 0)
check("NEG-CTRL 1 gapped schedule (R1 ends 13,500,000, R2 starts 13,600,000): "
"an UNCOVERED anchor EXISTS", s, expect_unsat=False, kind="NEG-CTRL")
# ---- NEG-CTRL 2: an OVERLAPPING schedule (R1's deactivation forgotten, active
# forever) MUST make the search for a doubly-covered anchor SAT.
def buggy_overlap_r1(h): return h >= REG1_HEIGHT
s = Solver()
h = Int('h')
add_anchor(s, h, 'k')
s.add(If(buggy_overlap_r1(h), 1, 0) + If(active_r2(h), 1, 0) >= 2)
check("NEG-CTRL 2 overlapping schedule (R1 never deactivated): a DOUBLY-"
"covered anchor EXISTS", s, expect_unsat=False, kind="NEG-CTRL")
# ---- NEG-CTRL 3: a BUGGY validator keyed on the CURRENT height makes the same
# historical seal flip verdict as the head crosses 13,600,000: two nodes
# at different heads DISAGREE on one seal, which is a chain split.
s = Solver()
a, c1, c2, kx = Int('a'), Int('c1'), Int('c2'), Int('kx')
add_anchor(s, a, 'k')
s.add(c1 >= a, c2 >= a, kx >= 0, kx <= REG2_KEYS - 1)
v1 = member(lookup(c1), kx) # BUG: registry chosen by head height
v2 = member(lookup(c2), kx)
s.add(v1 != v2)
check("NEG-CTRL 3 current-height lookup: one seal, two heads, two VERDICTS "
"(replay divergence exists)", s, expect_unsat=False, kind="NEG-CTRL")
# ---- NEG-CTRL 4: the same buggy rule ACCEPTS an R2-only key on an R1-era
# anchor once the head passes the rotation: history becomes forgeable by
# a key that did not exist when the anchor was sealed.
s = Solver()
a, c, kx = Int('a'), Int('c'), Int('kx')
add_anchor(s, a, 'k')
s.add(a < REG2_HEIGHT, kx >= REG1_KEYS, kx <= REG2_KEYS - 1, c >= REG2_HEIGHT)
s.add(member(lookup(c), kx)) # BUG accepts the backdated seal
check("NEG-CTRL 4 current-height lookup: an R2-only key CAN seal an R1-era "
"anchor (backdating accepted)", s, expect_unsat=False, kind="NEG-CTRL")
# ------------------------------------------------------------------------------
print("\n=== SUMMARY (REGISTRY ROTATION COVERAGE) ===")
allok = True
for name, tag, ok, kind in results:
print(f" {tag:9} [{kind}] {name}")
allok = allok and ok
print()
if allok:
print(" PROVED: the registry schedule (R1@13,014,000 x7, R2@13,600,000 x9,")
print(" active-until-next semantics) covers every anchor height >= 13,014,000")
print(" with EXACTLY one registry, unbounded in height: no gap, no overlap.")
print(" The rotation boundary 13,600,000 is not anchor-aligned and interval")
print(" coverage loses no anchor; the K-step and blocking-fork heights are")
print(" anchor-aligned. Anchor-keyed validation is replay-stable and rejects")
print(" R2-only keys on R1-era anchors; the blocking era >= 14,050,000 is")
print(" served entirely by the 9-key registry, which always covers the Falcon")
print(" quorum (6) and the effective K (<= 3 <= maxSeals 5). All four negative")
print(" controls FIRE: a gapped schedule orphans an anchor, a forgotten")
print(" deactivation double-covers one, and a current-height lookup both")
print(" splits verdicts across heads and accepts a backdated R2-only seal,")
print(" so the coverage checks and the anchor-height keying are load-bearing.")
print(" BOUNDARY: schedule arithmetic and lookup keying only (LIA, design")
print(" level); Falcon soundness and QBFT agreement are proved elsewhere in")
print(" this corpus, and this is not the Besu bytecode.")
else:
print(" NOT fully established (see FAILED / unexpected result above).")
import sys
sys.exit(0 if allok else 1)