370 lines
19 KiB
Python
370 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
# -----------------------------------------------------------------------------
|
|
# anchor_blocking_quorum_smt.py
|
|
#
|
|
# MACHINE-CHECKED (z3) proof of ANCHOR-THRESHOLD SAFETY for the AERE post-quantum
|
|
# anchor (every 32nd block, minimum K=3 seals since 2026-08-14).
|
|
#
|
|
# CORRECTION 2026-08-19: this file used to call the property "blocking-quorum safety
|
|
# once the blocking fork is active (height >= 14,050,000)". The fork property
|
|
# aere.falcon.forkBlock=14050000 arms a per-block rule that the shipped client
|
|
# retires at aere.pq.anchorBlock (13,014,000), so it changed no enforcement; the
|
|
# invariant below never depended on it and is stated for what the chain enforces:
|
|
# K=3 distinct valid seals on anchor blocks (f+1 of 9, not a quorum), none elsewhere.
|
|
#
|
|
# INVARIANT (the task target):
|
|
# Since 2026-08-14, no ANCHOR block can be finalized with fewer
|
|
# than K=3 valid Falcon seals from DISTINCT signers of the registry ACTIVE
|
|
# at that height; and a NON-anchor block requires no seals at all.
|
|
#
|
|
# Finalization is modelled as a function of (a) the multiset of HEARD seals and
|
|
# (b) the height SCHEDULE. Chain facts taken as LAW (chain 2800):
|
|
# N=9 validators, QBFT quorum ceil(2N/3) = 6
|
|
# anchor every 32 blocks starting at height 13,014,000
|
|
# min-seals schedule on heights: 13014000:0, 13034000:3 (so K=3 at the fork)
|
|
# producer write cap maxSeals = 5
|
|
# emergency minSealsCeiling can only LOWER the effective K, never raise it
|
|
# (the fork height 14,050,000 changed no enforcement; see the correction above)
|
|
# registry rotation on heights: 13014000 -> 7 keys, 13600000 -> 9 keys,
|
|
# every height covered by EXACTLY one registry
|
|
#
|
|
# Convention (same as computemarket_smt.py / falcon_blocking_smt.py): every
|
|
# property is a function that builds constraints; UNSAT of the negation means
|
|
# the property HOLDS; and every proof is paired with a NEGATIVE CONTROL that
|
|
# plants the violation and must come back SAT, proving the check can fire.
|
|
# The mandated negative control here: a planted rule that PERMITS 2 seals must
|
|
# make "finalized with only 2 distinct seals" SAT, while the original rule is
|
|
# UNSAT on the same scenario.
|
|
#
|
|
# HONEST BOUNDARY: this checks the DESIGN combinatorics of the finalization
|
|
# predicate (seal counting, distinctness, registry eligibility, schedule,
|
|
# ceiling, write cap), not the Besu Java code and not the live chain. The
|
|
# height 14,050,000 fork is modelled as given by the operator decision of
|
|
# 2026-08-15; registry key sets are modelled as index sets (7-key set is a
|
|
# prefix of the 9-key set; a 10th index stands for a key in NO registry).
|
|
# -----------------------------------------------------------------------------
|
|
from z3 import (Int, Bool, BoolVal, Solver, And, Or, Not, Implies, If, Sum,
|
|
sat, unsat)
|
|
|
|
# ---- chain facts (LAW) ------------------------------------------------------
|
|
FORK_BLOCKING = 14050000 # blocking fork height
|
|
ANCHOR_START = 13014000 # first anchor height
|
|
ANCHOR_INTERVAL = 32 # anchor every 32nd block
|
|
SCHED_K3_FROM = 13034000 # min-seals schedule: 13014000:0, 13034000:3
|
|
K_TARGET = 3 # the K in force at and after the blocking fork
|
|
MAX_SEALS = 5 # producer write cap
|
|
QBFT_N, QBFT_Q = 9, 6 # validator count, QBFT commit quorum ceil(2N/3)
|
|
REG1_START, REG1_KEYS = 13014000, 7 # first registry: 7 Falcon keys
|
|
REG2_START, REG2_KEYS = 13600000, 9 # second registry: 9 Falcon keys
|
|
NKEYS = 10 # key slots 0..8 = registry keys; slot 9 = a key
|
|
# that is in NO registry (foreign / rotated-out)
|
|
|
|
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
|
|
|
|
# ---- the finalization predicate, as a function of heard seals + schedule ----
|
|
def is_anchor(h):
|
|
"""Anchor heights: every 32nd block from 13,014,000."""
|
|
return And(h >= ANCHOR_START, (h - ANCHOR_START) % ANCHOR_INTERVAL == 0)
|
|
|
|
def sched_K(h):
|
|
"""The min-seals schedule on heights: 13014000:0, 13034000:3."""
|
|
return If(h >= SCHED_K3_FROM, K_TARGET, 0)
|
|
|
|
def eligible(i, h):
|
|
"""Is key index i in the registry ACTIVE at height h? Exactly one registry
|
|
covers each height: [13014000,13600000) -> 7 keys, [13600000,oo) -> 9.
|
|
Index 9 is in NO registry, ever."""
|
|
return If(h >= REG2_START, BoolVal(i < REG2_KEYS),
|
|
And(h >= REG1_START, BoolVal(i < REG1_KEYS)))
|
|
|
|
class Block:
|
|
"""Symbolic state of one block: height, the multiset of heard seals
|
|
(per-key multiplicity + validity), the emergency ceiling, and the
|
|
QBFT commit count."""
|
|
def __init__(self):
|
|
self.h = Int('h')
|
|
self.heard = [Int(f'heard_{i}') for i in range(NKEYS)] # seals heard from key i
|
|
self.valid = [Bool(f'valid_{i}') for i in range(NKEYS)] # key i's seal verifies
|
|
self.ceil_set = Bool('ceil_set') # emergency ceiling engaged?
|
|
self.ceil_val = Int('ceil_val') # its value (>= 0)
|
|
self.qbft = Int('qbft_commits') # ECDSA commit seals
|
|
|
|
def wf(self):
|
|
return [self.h >= ANCHOR_START] \
|
|
+ [self.heard[i] >= 0 for i in range(NKEYS)] \
|
|
+ [self.qbft >= 0, self.qbft <= QBFT_N, self.ceil_val >= 0]
|
|
|
|
def distinct_eligible_valid(self):
|
|
"""The rule the code enforces: count DISTINCT signers (multiplicity
|
|
collapses to 1) whose seal VERIFIES and who are in the ACTIVE
|
|
registry at this height."""
|
|
return Sum([If(And(self.heard[i] >= 1, self.valid[i], eligible(i, self.h)), 1, 0)
|
|
for i in range(NKEYS)])
|
|
|
|
def K_eff(self):
|
|
"""Effective K: the schedule value, LOWERED (never raised) by the
|
|
emergency ceiling when engaged."""
|
|
K = sched_K(self.h)
|
|
return If(self.ceil_set, If(self.ceil_val <= K, self.ceil_val, K), K)
|
|
|
|
def finalized(self):
|
|
"""Post-fork blocking finalization: QBFT commit quorum ALWAYS, plus the
|
|
Falcon quorum ON ANCHOR HEIGHTS ONLY. Non-anchor blocks carry no
|
|
Falcon requirement at all."""
|
|
falcon_ok = If(is_anchor(self.h), self.distinct_eligible_valid() >= self.K_eff(),
|
|
BoolVal(True))
|
|
return And(self.qbft >= QBFT_Q, falcon_ok)
|
|
|
|
def finalized_planted_K2(self):
|
|
"""PLANTED VIOLATION for the mandated negative control: same rule but
|
|
the anchor threshold guard is weakened to permit 2 seals."""
|
|
falcon_ok = If(is_anchor(self.h), self.distinct_eligible_valid() >= 2,
|
|
BoolVal(True))
|
|
return And(self.qbft >= QBFT_Q, falcon_ok)
|
|
|
|
def finalized_multiplicity_bug(self):
|
|
"""PLANTED VIOLATION: counts seal MESSAGES instead of distinct signers
|
|
(a replayed seal counts every time)."""
|
|
multi = Sum([If(And(self.valid[i], eligible(i, self.h)), self.heard[i], 0)
|
|
for i in range(NKEYS)])
|
|
falcon_ok = If(is_anchor(self.h), multi >= self.K_eff(), BoolVal(True))
|
|
return And(self.qbft >= QBFT_Q, falcon_ok)
|
|
|
|
def finalized_no_registry_check(self):
|
|
"""PLANTED VIOLATION: counts ANY valid seal, ignoring the active
|
|
registry (a foreign key's seal counts)."""
|
|
anykey = Sum([If(And(self.heard[i] >= 1, self.valid[i]), 1, 0)
|
|
for i in range(NKEYS)])
|
|
falcon_ok = If(is_anchor(self.h), anykey >= self.K_eff(), BoolVal(True))
|
|
return And(self.qbft >= QBFT_Q, falcon_ok)
|
|
|
|
print("### AERE anchor BLOCKING-QUORUM SAFETY -- fork at 14,050,000, K=3, N=9, q=6\n")
|
|
print(f" anchor grid: every {ANCHOR_INTERVAL} blocks from {ANCHOR_START}; "
|
|
f"fork {FORK_BLOCKING} is {'ON' if (FORK_BLOCKING-ANCHOR_START)%ANCHOR_INTERVAL==0 else 'OFF'} the grid "
|
|
f"((14050000-13014000)/32 = {(FORK_BLOCKING-ANCHOR_START)//ANCHOR_INTERVAL})\n")
|
|
|
|
# =============================================================================
|
|
# S0 CONFIG -- sanity of the constants the invariant leans on.
|
|
# =============================================================================
|
|
s = Solver()
|
|
s.add(Int('r') == (FORK_BLOCKING - ANCHOR_START) % ANCHOR_INTERVAL, Int('r') != 0)
|
|
check("S0a CONFIG: the blocking-fork height 14,050,000 lies ON the anchor grid "
|
|
"(it is itself an anchor height)", s)
|
|
|
|
s = Solver()
|
|
h = Int('h')
|
|
s.add(h >= FORK_BLOCKING, sched_K(h) != K_TARGET)
|
|
check("S0b SCHEDULE (all h >= fork): the min-seals schedule pins K == 3 at every "
|
|
"blocking-era height (13034000:3 step is below the fork)", s)
|
|
|
|
s = Solver()
|
|
h = Int('h')
|
|
r1 = And(h >= REG1_START, h < REG2_START)
|
|
r2 = (h >= REG2_START)
|
|
s.add(h >= ANCHOR_START, Not(Or(And(r1, Not(r2)), And(r2, Not(r1)))))
|
|
check("S0c REGISTRY PARTITION (all h): every height from 13,014,000 on is covered "
|
|
"by EXACTLY one registry (7-key window then 9-key window, no gap, no overlap)", s)
|
|
|
|
s = Solver()
|
|
h = Int('h')
|
|
s.add(h >= FORK_BLOCKING, Not(h >= REG2_START))
|
|
check("S0d REGISTRY AT FORK (all h >= fork): the active registry in the blocking "
|
|
"era is the 9-key registry (fork 14,050,000 > rotation 13,600,000)", s)
|
|
|
|
# =============================================================================
|
|
# S1 -- THE INVARIANT (positive result). An anchor block at h >= fork, with the
|
|
# emergency ceiling NOT engaged, can NEVER be finalized with <= 2 distinct
|
|
# valid registry-eligible Falcon seals. UNSAT = the property HOLDS.
|
|
# =============================================================================
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, is_anchor(b.h), Not(b.ceil_set))
|
|
s.add(b.finalized())
|
|
s.add(b.distinct_eligible_valid() <= 2)
|
|
check("S1 BLOCKING-QUORUM SAFETY: post-fork ANCHOR block finalized with <= 2 "
|
|
"distinct valid eligible Falcon seals is IMPOSSIBLE (K=3 floor holds)", s)
|
|
|
|
# same, with the ceiling engaged but at/above K: still no hole below 3
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, is_anchor(b.h), b.ceil_set, b.ceil_val >= K_TARGET)
|
|
s.add(b.finalized())
|
|
s.add(b.distinct_eligible_valid() <= 2)
|
|
check("S1b CEILING AT/ABOVE K: with minSealsCeiling engaged at >= 3 the K=3 floor "
|
|
"still holds (a ceiling >= K opens no hole)", s)
|
|
|
|
# =============================================================================
|
|
# S2 -- MANDATED NEGATIVE CONTROL: permit 2 seals and demand finalization.
|
|
# The planted rule (threshold guard weakened to 2) MUST be SAT on exactly the
|
|
# scenario S1 just proved impossible: the violation IS expressible, so S1's
|
|
# UNSAT is earned, not vacuous.
|
|
# =============================================================================
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, is_anchor(b.h), Not(b.ceil_set))
|
|
s.add(b.finalized_planted_K2())
|
|
s.add(b.distinct_eligible_valid() == 2)
|
|
check("S2 NEG-CTRL (mandated): PLANT a rule that permits 2 seals -> a post-fork "
|
|
"anchor block finalizes with exactly 2 distinct valid seals (violation is "
|
|
"expressible; the K>=3 guard in S1 is load-bearing)", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# =============================================================================
|
|
# S3 -- NON-ANCHOR blocks require NO seals (both directions).
|
|
# =============================================================================
|
|
# (a) zero heard seals can never block a non-anchor block: UNSAT of "QBFT quorum
|
|
# met, zero seals, NOT finalized".
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, Not(is_anchor(b.h)), b.qbft >= QBFT_Q)
|
|
s.add(*[b.heard[i] == 0 for i in range(NKEYS)])
|
|
s.add(Not(b.finalized()))
|
|
check("S3a NON-ANCHOR NEEDS NO SEALS (all h >= fork): a non-anchor block with the "
|
|
"QBFT quorum and ZERO heard seals is ALWAYS finalized (seals never required)", s)
|
|
|
|
# (b) non-vacuity witness: such a block exists (SAT with a concrete height).
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, Not(is_anchor(b.h)), b.qbft >= QBFT_Q)
|
|
s.add(*[b.heard[i] == 0 for i in range(NKEYS)])
|
|
s.add(b.finalized())
|
|
check("S3b CONFIG witness: a concrete post-fork non-anchor height finalizes with "
|
|
"zero seals (the S3a implication is not vacuous)", s,
|
|
expect_unsat=False, kind="CONFIG")
|
|
|
|
# =============================================================================
|
|
# S4 -- DISTINCTNESS is load-bearing: replayed seals collapse to one signer.
|
|
# =============================================================================
|
|
# (a) correct rule: 3+ seal MESSAGES from <= 2 distinct signers do NOT finalize.
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, is_anchor(b.h), Not(b.ceil_set))
|
|
s.add(Sum([b.heard[i] for i in range(NKEYS)]) >= 3) # >= 3 raw seal messages
|
|
s.add(b.distinct_eligible_valid() <= 2) # but <= 2 distinct signers
|
|
s.add(b.finalized())
|
|
check("S4a DISTINCT SIGNERS: >= 3 heard seal MESSAGES from <= 2 distinct eligible "
|
|
"signers can never finalize a post-fork anchor block (replay collapses)", s)
|
|
|
|
# (b) NEG-CTRL: a rule counting messages (multiplicity) accepts ONE signer
|
|
# replayed three times.
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, is_anchor(b.h), Not(b.ceil_set))
|
|
s.add(b.distinct_eligible_valid() <= 1) # a single distinct signer
|
|
s.add(b.finalized_multiplicity_bug())
|
|
check("S4b NEG-CTRL: PLANT multiplicity counting -> one signer replayed 3x "
|
|
"finalizes the anchor block (distinctness is load-bearing)", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# =============================================================================
|
|
# S5 -- REGISTRY ELIGIBILITY is load-bearing: a key outside the ACTIVE registry
|
|
# can never be the third seal.
|
|
# =============================================================================
|
|
# (a) correct rule: 2 eligible + 1 foreign (index 9, in NO registry) do NOT
|
|
# finalize, even though 3 distinct VALID seals were heard.
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, is_anchor(b.h), Not(b.ceil_set))
|
|
s.add(b.heard[9] >= 1, b.valid[9]) # a valid FOREIGN seal heard
|
|
s.add(b.distinct_eligible_valid() == 2) # only 2 eligible signers
|
|
s.add(b.finalized())
|
|
check("S5a REGISTRY ELIGIBILITY: 2 eligible + 1 valid seal from a key in NO "
|
|
"registry can never finalize a post-fork anchor block", s)
|
|
|
|
# (b) NEG-CTRL: a rule that skips the registry check accepts the foreign key as
|
|
# the third seal.
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, is_anchor(b.h), Not(b.ceil_set))
|
|
s.add(b.heard[9] >= 1, b.valid[9])
|
|
s.add(b.distinct_eligible_valid() == 2)
|
|
s.add(b.finalized_no_registry_check())
|
|
check("S5b NEG-CTRL: PLANT a rule without the registry check -> the foreign key "
|
|
"counts as the third seal and the block finalizes (eligibility is "
|
|
"load-bearing)", s, expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# =============================================================================
|
|
# S6 -- the emergency ceiling can only LOWER K, never raise it.
|
|
# =============================================================================
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.K_eff() > sched_K(b.h))
|
|
check("S6a CEILING ONLY LOWERS (all h, all ceiling values): effective K can never "
|
|
"exceed the schedule K (minSealsCeiling cannot raise the threshold)", s)
|
|
|
|
# CONFIG: the engaged ceiling is the ONLY legal path below 3 -- with ceiling 1 a
|
|
# 1-seal anchor block finalizes. This documents WHY S1 assumes the ceiling is
|
|
# not engaged; it is the designed emergency lowering, not a bug.
|
|
b = Block()
|
|
s = Solver(); s.add(b.wf())
|
|
s.add(b.h >= FORK_BLOCKING, is_anchor(b.h), b.ceil_set, b.ceil_val == 1)
|
|
s.add(b.distinct_eligible_valid() == 1)
|
|
s.add(b.finalized())
|
|
check("S6b CONFIG: with the emergency ceiling engaged at 1, a 1-seal anchor block "
|
|
"finalizes (the ceiling is the ONLY legal path below K=3, by design)", s,
|
|
expect_unsat=False, kind="CONFIG")
|
|
|
|
# =============================================================================
|
|
# S7 -- the write cap maxSeals=5 never starves the certificate below K=3.
|
|
# =============================================================================
|
|
c, w = Int('c'), Int('w')
|
|
s = Solver()
|
|
s.add(c >= 0, w == If(c <= MAX_SEALS, c, MAX_SEALS)) # producer writes min(c, 5)
|
|
s.add(c >= K_TARGET, w < K_TARGET) # negate: cap drops below K
|
|
check("S7a WRITE CAP (all counts): with maxSeals=5 >= K=3, capping the written "
|
|
"certificate can never drop a >= 3-seal certificate below K", s)
|
|
|
|
# NEG-CTRL: a cap BELOW K (2) starves the certificate -- this is exactly why the
|
|
# config loader refuses maxSeals below the largest schedule K.
|
|
c, w = Int('c'), Int('w')
|
|
s = Solver()
|
|
s.add(c >= 0, w == If(c <= 2, c, 2)) # planted cap 2 < K
|
|
s.add(c >= K_TARGET, w < K_TARGET)
|
|
check("S7b NEG-CTRL: PLANT a cap of 2 < K -> a 3-seal certificate is written short "
|
|
"of the quorum (why maxSeals below the schedule K is refused at load)", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# =============================================================================
|
|
print("\n=== SUMMARY (anchor BLOCKING-QUORUM SAFETY) ===")
|
|
allok = True
|
|
for name, tag, ok, kind in results:
|
|
print(f" {tag:9} [{kind}] {name}")
|
|
allok = allok and ok
|
|
print()
|
|
if allok:
|
|
print(" PROVED: with the blocking fork active (h >= 14,050,000) no anchor block")
|
|
print(" can be finalized with fewer than K=3 valid Falcon seals from DISTINCT")
|
|
print(" signers of the registry active at that height (the 9-key registry, since")
|
|
print(" the fork lies above the 13,600,000 rotation), and non-anchor blocks")
|
|
print(" require no seals at all (proved in both directions). The mandated")
|
|
print(" negative control FIRES: a planted rule permitting 2 seals finalizes an")
|
|
print(" anchor block with exactly 2 seals (SAT), so the K>=3 guard is load-")
|
|
print(" bearing and the S1 UNSAT is non-vacuous. Distinctness (replay collapses)")
|
|
print(" and registry eligibility (foreign keys never count) are each load-")
|
|
print(" bearing, with firing controls. The emergency ceiling can only LOWER K")
|
|
print(" (and is the single designed path below 3); the maxSeals=5 write cap can")
|
|
print(" never starve a certificate below K, and a planted cap of 2 does.")
|
|
print(" BOUNDARY: design combinatorics of the finalization predicate, not Besu")
|
|
print(" bytecode, not the live chain; registries modelled as index sets.")
|
|
else:
|
|
print(" NOT fully established (see FAILED / unexpected result above).")
|
|
import sys
|
|
sys.exit(0 if allok else 1)
|