#!/usr/bin/env python3 # ----------------------------------------------------------------------------- # pq_message_enforcement_smt.py # # MACHINE-CHECKED (z3) proof of the ENFORCEMENT RULES for the four post-quantum # sealed QBFT message layers of chain 2800 (SPEC.md section 2.6), written on # 2026-09-02, the day the fleet moved to the build that fixed D-311. # # WHAT IS MODELLED (design combinatorics, not the Java code and not the chain): # - four message TYPES with four DOMAINS (PROPOSAL, PREPARE, ROUND-CHANGE, COMMIT); # - a seal = (index, domain, height, round, digest, form) where `form` distinguishes # the COMMIT anchor form (signed when the anchor is armed at that height) from the # committed-seal digest form; # - a registry active at a height binding index -> author; # - the per-type enforcement height H_type (absent = never); # - the verifier's decision "the vote counts" as a function of the message, its # author, the seal it carries, the registry and the height. # # PROPERTIES (UNSAT of the negation = holds): # P1 ARMED-REQUIRES-VALID: at h >= H_type, a message counts only if it carries a # seal bound to its author, accepted by the registry, and verifying over the # pre-image of ITS OWN type at ITS OWN (height, round, digest). # P2 NO-CROSS-DOMAIN-REPLAY: a seal valid for type X never makes a message of type # Y != X count (domains differ, so pre-images differ). # P3 NO-CROSS-POSITION-REPLAY: a seal over (h, r, d) never makes a message at a # different (h', r', d') count. # P4 NO-AUTHOR-SWAP: a seal of index i counts only on a message whose author is # the address the registry binds to i. # P5 BELOW-FORK-UNCHANGED: at h < H_type an unsealed message still counts (the # upstream rule alone decides). # P6 COMMIT-FORM-AGREEMENT (D-311): with the anchor armed, an emitter signing the # anchor form and a verifier checking the anchor form agree (commits count); # the NEGATIVE CONTROL plants the pre-2026-09-02 verifier (digest form) and # shows that EVERY honest commit is refused, i.e. the liveness failure the # testnet observed at block 999. # # NEGATIVE CONTROLS: every property has one; each plants the violation and must # come back SAT, proving the check can fire. Convention as in the other *_smt.py. # # HONEST BOUNDARY: Falcon verification is modelled as equality of the signed # pre-image tuple (a perfect signature); registry rotation is modelled as a # height-indexed binding; nothing here says anything about the Java code paths # beyond the rules SPEC.md 2.6 states, nor about chain 2800's live enforcement # state (none is armed at the time of writing). # ----------------------------------------------------------------------------- from z3 import (Int, Bool, BoolVal, Solver, And, Or, Not, Implies, If, sat, unsat) PROPOSAL, PREPARE, ROUNDCHANGE, COMMIT = 0, 1, 2, 3 DIGEST_FORM, ANCHOR_FORM = 0, 1 N = 9 # validators / registry indices 0..8 ANCHOR_BLOCK = 13014000 # aere.pq.anchorBlock on 2800 results = [] def bound_author(index, height): """registry active at `height` binds index i to author i (modelled identity); indices outside 0..N-1 are refused. Rotation is modelled by the height guard: below the registry start nothing is bound (the model uses one registry; the anchor model covers rotation).""" return If(And(index >= 0, index < N), index, -1) def anchor_armed(height): return height + 1 >= ANCHOR_BLOCK def expected_form(mtype, height): """the message form a COMMIT seal must sign at this height (SPEC 2.6, D8); other types always sign their own domain pre-image (modelled as DIGEST_FORM of their own domain).""" return If(And(mtype == COMMIT, anchor_armed(height)), ANCHOR_FORM, DIGEST_FORM) def counts(mtype, height, round_, digest, author, has_seal, s_index, s_domain, s_h, s_r, s_d, s_form, H_type, verifier_form=None): """the vote-counting predicate of SPEC 2.6. `verifier_form` overrides the form the verifier expects for COMMIT (used by the D-311 negative control); None = the fixed verifier.""" armed = height >= H_type exp_form = expected_form(mtype, height) if verifier_form is None else verifier_form seal_ok = And(has_seal, bound_author(s_index, height) == author, # rule 2 + 3 s_domain == mtype, # rule 4: own domain s_h == height, s_r == round_, s_d == digest, # rule 4: own position s_form == exp_form) # rule 4: commit form return Or(Not(armed), seal_ok) def fresh(prefix): return (Int(prefix + '_type'), Int(prefix + '_h'), Int(prefix + '_r'), Int(prefix + '_d'), Int(prefix + '_author'), Bool(prefix + '_has'), Int(prefix + '_si'), Int(prefix + '_sdom'), Int(prefix + '_sh'), Int(prefix + '_sr'), Int(prefix + '_sd'), Int(prefix + '_sform')) def domain_bounds(t, sdom): return And(t >= 0, t <= 3, sdom >= 0, sdom <= 3) def check(name, negation_constraints, negative_control_constraints): s = Solver(); s.add(*negation_constraints); r = s.check() holds = (r == unsat) s2 = Solver(); s2.add(*negative_control_constraints); r2 = s2.check() fires = (r2 == sat) results.append((name, holds, fires)) print(f"{name:28s} property={'HOLDS' if holds else 'FAILS'} ({r}) negative-control={'FIRES' if fires else 'DOES NOT FIRE'} ({r2})") return holds and fires # ---------------------------------------------------------------- P1 armed requires valid t, h, r, d, a, has, si, sdom, sh, sr, sd, sform = fresh('p1'); H = Int('p1_H') base = [domain_bounds(t, sdom), h >= H, a >= 0, a < N] # negation: armed, and the message counts, yet the seal is absent or not the author's or wrong position bad_seal = Or(Not(has), bound_author(si, h) != a, sdom != t, sh != h, sr != r, sd != d, sform != expected_form(t, h)) neg = base + [counts(t, h, r, d, a, has, si, sdom, sh, sr, sd, sform, H), bad_seal] # negative control: a planted rule that accepts an unsealed message while armed planted = base + [Not(has), Or(Not(h >= H), True)] # "counts" replaced by True: the planted rule ignores the seal check('P1 armed-requires-valid', neg, planted) # ---------------------------------------------------------------- P2 no cross-domain replay t, h, r, d, a, has, si, sdom, sh, sr, sd, sform = fresh('p2'); H = Int('p2_H') neg = [domain_bounds(t, sdom), h >= H, a >= 0, a < N, has, sdom != t, counts(t, h, r, d, a, has, si, sdom, sh, sr, sd, sform, H)] # planted: a domain-blind verifier (drops the sdom == t conjunct) blind = And(has, bound_author(si, h) == a, sh == h, sr == r, sd == d, sform == expected_form(t, h)) planted = [domain_bounds(t, sdom), h >= H, a >= 0, a < N, has, sdom != t, blind] check('P2 no-cross-domain-replay', neg, planted) # ---------------------------------------------------------------- P3 no cross-position replay t, h, r, d, a, has, si, sdom, sh, sr, sd, sform = fresh('p3'); H = Int('p3_H') neg = [domain_bounds(t, sdom), h >= H, a >= 0, a < N, has, Or(sh != h, sr != r, sd != d), counts(t, h, r, d, a, has, si, sdom, sh, sr, sd, sform, H)] blind = And(has, bound_author(si, h) == a, sdom == t, sform == expected_form(t, h)) # position-blind planted = [domain_bounds(t, sdom), h >= H, a >= 0, a < N, has, Or(sh != h, sr != r, sd != d), blind] check('P3 no-cross-position-replay', neg, planted) # ---------------------------------------------------------------- P4 no author swap t, h, r, d, a, has, si, sdom, sh, sr, sd, sform = fresh('p4'); H = Int('p4_H') neg = [domain_bounds(t, sdom), h >= H, a >= 0, a < N, has, bound_author(si, h) != a, counts(t, h, r, d, a, has, si, sdom, sh, sr, sd, sform, H)] blind = And(has, sdom == t, sh == h, sr == r, sd == d, sform == expected_form(t, h)) # author-blind planted = [domain_bounds(t, sdom), h >= H, a >= 0, a < N, has, bound_author(si, h) != a, blind] check('P4 no-author-swap', neg, planted) # ---------------------------------------------------------------- P5 below fork unchanged t, h, r, d, a, has, si, sdom, sh, sr, sd, sform = fresh('p5'); H = Int('p5_H') neg = [domain_bounds(t, sdom), h < H, Not(has), a >= 0, a < N, Not(counts(t, h, r, d, a, has, si, sdom, sh, sr, sd, sform, H))] # planted: an enforcement that ignores its own height (always armed) planted = [domain_bounds(t, sdom), h < H, Not(has), a >= 0, a < N, Not(Or(Not(BoolVal(True)), has))] check('P5 below-fork-unchanged', neg, planted) # ---------------------------------------------------------------- P6 commit form agreement (D-311) h = Int('p6_h'); r = Int('p6_r'); d = Int('p6_d'); a = Int('p6_a'); si = Int('p6_si'); H = Int('p6_H') honest = [h >= H, h + 1 >= ANCHOR_BLOCK, a >= 0, a < N, si == a] # honest emitter: seal of its own index emitted_form = ANCHOR_FORM # what QbftRound.pqSealMessageFor signs when the anchor is armed # negation: an honest commit under the FIXED verifier does not count neg = honest + [Not(counts(COMMIT, h, r, d, a, True, si, COMMIT, h, r, d, emitted_form, H))] # negative control: the pre-D-311 verifier (digest form) refuses the same honest commit planted = honest + [Not(counts(COMMIT, h, r, d, a, True, si, COMMIT, h, r, d, emitted_form, H, verifier_form=DIGEST_FORM))] check('P6 commit-form-agreement', neg, planted) # ---------------------------------------------------------------- P6b every honest commit refused under the old verifier # strengthening of the negative control: under the old verifier there is NO honest commit that counts s = Solver() s.add(honest[0], honest[1], honest[2], honest[3], honest[4], counts(COMMIT, h, r, d, a, True, si, COMMIT, h, r, d, emitted_form, H, verifier_form=DIGEST_FORM)) r_all = s.check() all_refused = (r_all == unsat) results.append(('P6b old-verifier-refuses-all', all_refused, all_refused)) print(f"{'P6b old-verifier-refuses-all':28s} {'HOLDS' if all_refused else 'FAILS'}: under the digest-form verifier with the anchor armed, no honest commit counts ({r_all}) - the block-999 stop of testnet 28001") ok = all(hold and fire for _, hold, fire in results) print(f"\n{'ALL PROPERTIES HOLD AND ALL NEGATIVE CONTROLS FIRE' if ok else 'SOMETHING DID NOT HOLD OR A CONTROL DID NOT FIRE'}: {sum(1 for _,hh,ff in results if hh and ff)}/{len(results)}") raise SystemExit(0 if ok else 1)