aere-research/formal-consensus/qbft_pqc_activation_smt.py
Aere Network 4a0b48588c Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:30 +03:00

338 lines
20 KiB
Python

#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# qbft_pqc_activation_smt.py (PQC ACTIVATION transition-safety, 2026-07-18)
#
# MACHINE-CHECKED (z3) safety of the AERE Falcon-512 quorum-certificate
# ACTIVATION TRANSITION: the fork-gated switch from LOG-ONLY (additive, never
# gates) to BLOCKING (a post-fork block also needs a Falcon-512 quorum), and the
# move of the validator index->FalconPubKey registry from a GENESIS-anchored
# manifest to a CONTRACT-anchored one. Design source of truth:
# consensus-pqc/QUORUM-DESIGN.md sec 3/5/6 (FalconSealValidationRule, the
# aere.falcon.forkBlock gate: block < fork = LOG-ONLY always-true;
# block >= fork = BLOCKING requires >= quorum distinct valid Falcon seals;
# ECDSA committed seals remain the decisive safety/liveness seal throughout),
# consensus-pqc/ONCHAIN-REGISTRY-DESIGN.md (fail-closed loader: the registry
# is used only if keccak256(manifest) == the on-chain-committed anchor hash;
# any mismatch leaves the registry EMPTY so a post-fork block is rejected).
#
# This model proves the three transition properties requested, each by UNSAT of
# its negation and each paired with a deliberately-broken NEGATIVE CONTROL that
# z3 must flag SAT, so the checks are demonstrably non-vacuous (they have teeth):
#
# (a) NO SPLIT ACROSS THE ACTIVATION HEIGHT. No block valid under the pre-fork
# rule and one valid under the post-fork rule both commit at the same
# height.
# A1 the gate is a pure function of height: a single chain-wide fork
# height cannot classify one block pre and another post at the SAME
# height (so there is no mixed-rule region at the boundary).
# A2 both rules keep the DECISIVE ECDSA committed-seal quorum (neither
# regime ever waives it), and two conflicting blocks cannot both
# carry an ECDSA quorum under <= f Byzantine (bounded N=7), so
# split-freedom carries across the boundary unchanged.
#
# (b) THE TRANSITION ADDS NO CLASSICAL-ONLY HALT (pre-fork is a STRICT no-op).
# For every height below the fork the fork-gated rule is byte-for-byte the
# classical ECDSA-only rule, so no block that would commit on the pre-fork
# (ECDSA-only) chain is ever rejected by adding the Falcon layer.
#
# (c) CONTRACT-ANCHORED registry read is SAFETY-EQUIVALENT to GENESIS-anchored.
# Under injective keccak (assumption A2, shared with the other AERE models),
# the on-chain anchor pins a UNIQUE manifest (C0); the verifier outcome is a
# function of (manifest, anchor) ALONE and does not depend on WHERE the
# anchor is read from -- genesis stateRoot slot or contract storage slot
# (C1); an immutable contract anchor committing the same hash yields an
# IDENTICAL committable set (C2); and a manifest that does not match the
# committed anchor is fail-closed under either source (C3). The equivalence
# is CONDITIONAL on the contract anchor being immutable / fail-closed, and
# the two firing controls (a MUTABLE contract slot; a fail-OPEN loader) show
# both conditions are load-bearing.
#
# ASSUMPTION A2 (as in falcon_logonly_noop_smt.py): keccak256 is injective on the
# manifests in play, so equal hashes imply equal manifests. Modeled as an
# uninterpreted function with an injectivity constraint over the terms used.
#
# HONEST BOUNDARY: this is the certificate/registry DESIGN combinatorics of the
# activation transition, NOT the Besu Java bytecode, and NOT mainnet. Chain 2800
# runs classical ECDSA QBFT (N=7, f=2, quorum 5); the Falcon layer there is
# LOG-ONLY, non-gating, and carries no certificate. Activating BLOCKING and the
# contract-anchored registry is isolated-testnet R&D and remains audit-gated and
# founder-gated. A2's per-N conflict check is bounded (N=7); the fork-gate and
# registry lemmas are unbounded over the modeled variables. Cryptographic
# soundness of Falcon-512 / keccak256 is out of scope (NIST KAT + precompile).
# -----------------------------------------------------------------------------
from z3 import (Int, Bool, Function, IntSort, BoolSort, Solver,
And, Or, Not, Implies, If, Sum, sat, unsat)
def quorum(n): return (2 * n + 2) // 3 # ceil(2N/3)
def faultbound(n): return (n - 1) // 3 # floor((N-1)/3)
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
N = 7
q = quorum(N); f = faultbound(N)
print("### AERE PQC ACTIVATION transition -- log-only->blocking + contract-anchored Falcon registry\n")
print(f" live/target set N={N}: quorum q={q}, fault bound f={f} (chain 2800 = classical ECDSA QBFT)\n")
# =============================================================================
# GROUP A -- NO SPLIT ACROSS THE ACTIVATION HEIGHT.
# =============================================================================
# The rule in force at a block is a pure function of the block's height h and the
# single chain-wide activation height 'fork': post-fork iff h >= fork.
def ruleIsPost(h, fork): return h >= fork
# ---- A1 a single shared fork height cannot mix regimes at one height ----------
# Negate the property: block A judged by the PRE rule and block B judged by the
# POST rule, both at the SAME height, under the SAME (single) fork height.
s = Solver()
hA, hB, fork = Int('hA'), Int('hB'), Int('fork')
s.add(hA == hB) # same height
s.add(Not(ruleIsPost(hA, fork))) # A classified PRE (hA < fork)
s.add(ruleIsPost(hB, fork)) # B classified POST (hB >= fork)
check("A1 single chain-wide fork height cannot classify one block pre and another "
"post at the SAME height (no mixed-rule region at the boundary)", s)
# ---- NEG-CTRL A1: per-node DIVERGENT fork heights (misconfiguration) -----------
# If two nodes disagree on the activation height, the boundary CAN split: at some
# height A is pre for one node and B is post for the other. This is the real
# operational requirement -- the activation height must be one identical constant
# on every node -- made load-bearing.
s = Solver()
hA, hB = Int('hA'), Int('hB')
forkA, forkB = Int('forkA'), Int('forkB')
s.add(hA == hB)
s.add(forkA != forkB) # nodes disagree on the activation block
s.add(Not(ruleIsPost(hA, forkA))) # node using forkA: A is pre
s.add(ruleIsPost(hB, forkB)) # node using forkB: B is post
check("NEG-CTRL A1 DIVERGENT per-node fork heights let the SAME height be pre for one "
"node and post for another (proves a single chain-wide fork constant is required)",
s, expect_unsat=False, kind="NEG-CTRL")
# ---- A2 both regimes keep the DECISIVE ECDSA quorum; ECDSA no-conflict carries -
# Rule predicates (Falcon gate returns true in log-only; adds a Falcon quorum in
# blocking). 'ecdsa'/'falcon' are the counts of distinct valid seals of each kind.
def pre_valid(ecdsa, falcon): return ecdsa >= q # LOG-ONLY regime
def post_valid(ecdsa, falcon): return And(ecdsa >= q, falcon >= q) # BLOCKING regime
# A2-struct-pre: the pre-fork rule never waives the ECDSA quorum.
s = Solver()
ec, fa = Int('ecdsa'), Int('falcon')
s.add(pre_valid(ec, fa)); s.add(Not(ec >= q)) # negate: valid without ECDSA quorum
check("A2 pre-fork (log-only) rule never waives the ECDSA committed-seal quorum", s)
# A2-struct-post: the post-fork rule never waives the ECDSA quorum either.
s = Solver()
ec, fa = Int('ecdsa'), Int('falcon')
s.add(post_valid(ec, fa)); s.add(Not(ec >= q)) # negate: valid without ECDSA quorum
check("A2 post-fork (blocking) rule never waives the ECDSA committed-seal quorum "
"(so the decisive safety seal is identical on both sides of the fork)", s)
# A2-conflict (bounded N=7): two conflicting blocks cannot both carry an ECDSA
# committed-seal quorum under <= f equivocating Byzantine. Since BOTH rules require
# this quorum, same-height split-freedom holds in either regime and hence across
# the boundary. (General treatment: qbft_safety_smt.py; kept here so (a) is self-contained.)
def ecdsa_conflict(N, qv, fv):
s = Solver()
honest = [Bool(f'honest_{i}') for i in range(N)]
sealA = [Bool(f'sealA_{i}') for i in range(N)]
sealB = [Bool(f'sealB_{i}') for i in range(N)]
s.add(Sum([If(Not(honest[i]), 1, 0) for i in range(N)]) <= fv) # <= f Byzantine
for i in range(N):
s.add(Implies(honest[i], Not(And(sealA[i], sealB[i])))) # honest: one block/height
s.add(Sum([If(sealA[i], 1, 0) for i in range(N)]) >= qv) # ECDSA quorum on A
s.add(Sum([If(sealB[i], 1, 0) for i in range(N)]) >= qv) # ECDSA quorum on B
return s
check(f"A2 ECDSA no-conflict N={N} (q={q},f={f}): two conflicting blocks cannot both carry "
f"an ECDSA committed-seal quorum under <= f equivocating (split-free in either regime)",
ecdsa_conflict(N, q, f))
# ---- NEG-CTRL A2: lower the quorum to ceil(N/2) -> conflicting ECDSA commits -----
def ceil_half(n): return (n + 1) // 2
check(f"NEG-CTRL A2 N={N} majority quorum ceil(N/2)={ceil_half(N)}: two conflicting blocks CAN "
f"both reach an ECDSA quorum (proves ceil(2N/3) is load-bearing for split-freedom)",
ecdsa_conflict(N, ceil_half(N), f), expect_unsat=False, kind="NEG-CTRL")
# =============================================================================
# GROUP B -- THE TRANSITION ADDS NO CLASSICAL-ONLY HALT (pre-fork strict no-op).
# =============================================================================
# The fork-gated commit rule vs the classical (pre-Falcon-layer) commit rule.
# Q is a concrete quorum; the argument is independent of its value (as in
# falcon_logonly_noop_smt.py).
Q = 5
def forkgated_valid(h, fork, ecdsa, falcon):
# block < fork: LOG-ONLY (Falcon gate returns true); block >= fork: BLOCKING.
return If(h < fork, ecdsa >= Q, And(ecdsa >= Q, falcon >= Q))
def classical_valid(ecdsa):
return ecdsa >= Q # the rule that existed before the Falcon layer
# ---- B1 below the fork the two rules are identical (strict no-op) --------------
s = Solver()
h, fork = Int('h'), Int('fork')
ecdsa, falcon = Int('ecdsa'), Int('falcon')
s.add(h < fork) # any pre-fork height
s.add(forkgated_valid(h, fork, ecdsa, falcon) != classical_valid(ecdsa)) # negate: they differ
check("B1 below the fork the fork-gated rule == the classical ECDSA-only rule "
"(the Falcon layer is a strict no-op pre-fork -> no block is newly halted)", s)
# ---- B1-sanity non-vacuous: some pre-fork block does commit ---------------------
s = Solver()
h, fork = Int('h'), Int('fork')
ecdsa, falcon = Int('ecdsa'), Int('falcon')
s.add(h < fork, ecdsa >= Q)
s.add(forkgated_valid(h, fork, ecdsa, falcon)) # reachable: a pre-fork block commits
check("B1-sanity a pre-fork block with an ECDSA quorum IS committable (non-vacuous)", s,
expect_unsat=False, kind="SANITY")
# ---- NEG-CTRL B1: a buggy rule that BLOCKS on Falcon even pre-fork ---------------
# (fork gate ignored -> always blocking). A classical block with an ECDSA quorum but
# no Falcon quorum is then rejected below the fork: a halt the transition must NOT add.
def buggy_always_blocking(h, fork, ecdsa, falcon):
return And(ecdsa >= Q, falcon >= Q) # BUG: no fork gate, blocks pre-fork too
s = Solver()
h, fork = Int('h'), Int('fork')
ecdsa, falcon = Int('ecdsa'), Int('falcon')
s.add(h < fork) # pre-fork
s.add(classical_valid(ecdsa)) # would commit on the classical chain
s.add(Not(buggy_always_blocking(h, fork, ecdsa, falcon))) # but the buggy rule rejects it
check("NEG-CTRL B1 a rule that BLOCKS on Falcon even pre-fork rejects a classical (ECDSA-"
"quorum) block -> an added halt (proves the fork gate = zero pre-fork halt risk)",
s, expect_unsat=False, kind="NEG-CTRL")
# =============================================================================
# GROUP C -- CONTRACT-ANCHORED registry read is SAFETY-EQUIVALENT to GENESIS.
# =============================================================================
# keccak256 as an injective uninterpreted function (assumption A2). The fail-closed
# loader uses a manifest only if keccak256(manifest) equals the on-chain anchor.
K = Function('keccak', IntSort(), IntSort())
def trusted(m, a): return K(m) == a # fail-closed: registry usable iff hash matches
# Qgood(m): the block's collected Falcon seals reach quorum under manifest m
# (uninterpreted; abstracts the per-block seal set / registry contents).
Qgood = Function('quorumUnderManifest', IntSort(), BoolSort())
def falconOK(m, a): return And(trusted(m, a), Qgood(m)) # post-fork Falcon-quorum gate outcome
# ---- C0 the on-chain anchor pins a UNIQUE manifest -----------------------------
# Under injective keccak, no two distinct manifests are both trusted by one anchor,
# so "which registry is used" is fixed by the anchor VALUE alone -- identically for
# a genesis stateRoot slot or a contract storage slot.
s = Solver()
m1, m2, a = Int('m1'), Int('m2'), Int('a')
s.add(Implies(m1 != m2, K(m1) != K(m2))) # keccak injective (A2)
s.add(m1 != m2, trusted(m1, a), trusted(m2, a)) # negate: two manifests trusted by one anchor
check("C0 the on-chain anchor pins a UNIQUE manifest (injective keccak: no two distinct "
"manifests share an anchor) -> the anchor VALUE alone fixes the registry", s)
# ---- C1 the verifier outcome is a function of (manifest, anchor) ALONE ----------
# It does NOT depend on WHERE the anchor is read from (genesis slot vs contract
# slot). Same manifest + same committed hash from either source => identical outcome.
s = Solver()
m_gen, a_gen = Int('m_gen'), Int('a_gen') # genesis-anchored read
m_ctr, a_ctr = Int('m_ctr'), Int('a_ctr') # contract-anchored read
s.add(m_gen == m_ctr, a_gen == a_ctr) # same manifest + same anchor value, different source
s.add(falconOK(m_gen, a_gen) != falconOK(m_ctr, a_ctr)) # negate: outcome depends on source
check("C1 the Falcon-verify outcome depends on (manifest, anchor) ONLY, not on the read "
"source (genesis stateRoot slot vs contract storage slot) -> safety-equivalent read", s)
# ---- C2 an IMMUTABLE contract anchor committing the same hash is identical -------
# Genesis: honest manifest M* anchored as A*=keccak(M*). Contract loader is fail-
# closed (K(m_ctr)==a_ctr) and the contract anchor is immutable-and-equal (a_ctr==A*).
# Injectivity then forces m_ctr==M*, so committability is identical to genesis.
s = Solver()
Mstar, Astar = Int('M_star'), Int('A_star')
m_ctr, a_ctr = Int('m_ctr'), Int('a_ctr')
s.add(K(Mstar) == Astar) # genesis: honest manifest anchored
s.add(K(m_ctr) == a_ctr) # contract loader fail-closed
s.add(a_ctr == Astar) # contract anchor: immutable, commits the SAME hash
s.add(Implies(m_ctr != Mstar, K(m_ctr) != K(Mstar))) # keccak injective (A2)
s.add(falconOK(m_ctr, a_ctr) != falconOK(Mstar, Astar)) # negate: committability diverges
check("C2 an IMMUTABLE contract anchor committing the same hash yields an IDENTICAL "
"committable set to genesis-anchoring (injectivity forces the same manifest)", s)
# ---- NEG-CTRL C2: a MUTABLE contract slot diverges from genesis -------------------
# An attacker rewrites the mutable contract anchor to a'=keccak(m') for a forged
# manifest m' (attacker keys). The contract-anchored verifier then accepts seals the
# genesis-anchored verifier rejects -> a block commits under contract-anchoring that
# genesis-anchoring would never accept. Proves the equivalence needs immutability.
s = Solver()
Mstar, Astar = Int('M_star'), Int('A_star')
mprime, aprime = Int('m_forged'), Int('a_mutated')
s.add(K(Mstar) == Astar) # honest genesis anchor
s.add(mprime != Mstar)
s.add(Implies(mprime != Mstar, K(mprime) != K(Mstar))) # keccak injective (A2)
s.add(aprime == K(mprime)) # attacker rewrote the MUTABLE contract slot
s.add(Qgood(mprime)) # forged manifest admits an attacker seal-quorum
s.add(Not(Qgood(Mstar))) # honest genesis manifest would NOT accept those seals
s.add(falconOK(mprime, aprime)) # contract-anchored: ACCEPTS
s.add(Not(falconOK(Mstar, Astar))) # genesis-anchored: REJECTS
check("NEG-CTRL C2 a MUTABLE contract anchor can be rewritten to a forged manifest so a "
"block commits under contract-anchoring that genesis-anchoring rejects (proves the "
"contract anchor must be immutable / re-anchored under the same integrity)",
s, expect_unsat=False, kind="NEG-CTRL")
# ---- C3 fail-closed: a manifest not matching the committed anchor is unusable -----
# Holds identically for genesis and contract sources (both call the same verify).
s = Solver()
m, a = Int('m_tampered'), Int('a_anchor')
s.add(K(m) != a) # manifest does not match the committed anchor
s.add(falconOK(m, a)) # negate: claim it still verifies
check("C3 fail-closed: a manifest whose keccak != the committed anchor can never yield a "
"Falcon-OK (post-fork block rejected) -- identical for genesis or contract source", s)
# ---- NEG-CTRL C3: a fail-OPEN loader accepts a tampered registry ------------------
def falconOK_failopen(m, a): return Qgood(m) # BUG: ignores the anchor check entirely
s = Solver()
m, a = Int('m_tampered'), Int('a_anchor')
s.add(K(m) != a) # mismatched / tampered manifest
s.add(Qgood(m)) # forged manifest admits a quorum
s.add(falconOK_failopen(m, a)) # buggy loader accepts it anyway
check("NEG-CTRL C3 a fail-OPEN loader (ignores the anchor check) accepts a tampered manifest "
"(proves fail-closed is load-bearing for the registry safety-equivalence)",
s, expect_unsat=False, kind="NEG-CTRL")
# =============================================================================
print("\n=== SUMMARY (PQC ACTIVATION transition 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 (under A2 keccak-injectivity): the LOG-ONLY -> BLOCKING Falcon activation")
print(" transition is SPLIT-FREE at the boundary -- a single chain-wide fork height admits")
print(" no mixed-rule region (A1), and both regimes keep the decisive ECDSA committed-seal")
print(" quorum so two conflicting blocks cannot both commit at one height (A2). Below the")
print(" fork the fork-gated rule is byte-for-byte the classical ECDSA-only rule, so the")
print(" transition adds NO classical-only halt (B1, a strict pre-fork no-op). And a")
print(" CONTRACT-anchored registry read is SAFETY-EQUIVALENT to a GENESIS-anchored one:")
print(" the anchor pins a unique manifest (C0), the verify outcome depends on (manifest,")
print(" anchor) alone and not on the read location (C1), an immutable equal contract anchor")
print(" gives an identical committable set (C2), and both sources are fail-closed (C3).")
print(" Every negative control FIRES: divergent per-node fork heights split the boundary;")
print(" a majority quorum breaks split-freedom; a pre-fork blocking rule adds a halt; a")
print(" MUTABLE contract anchor and a fail-OPEN loader each break registry equivalence --")
print(" so each guard (single fork constant, ceil(2N/3), fork-gating, immutability, fail-")
print(" closed) is load-bearing.")
print(" BOUNDARY: DESIGN combinatorics of the transition, NOT Besu bytecode, NOT mainnet.")
print(" Chain 2800 = classical ECDSA QBFT (N=7,f=2,q=5); the Falcon layer there is LOG-ONLY")
print(" and non-gating. Blocking + contract-anchored registry stay isolated-testnet R&D,")
print(" audit-gated and founder-gated.")
else:
print(" NOT fully established (see FAILED / unexpected result above).")
import sys
sys.exit(0 if allok else 1)