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.
338 lines
20 KiB
Python
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)
|