aere-research/formal-consensus/cryptoregistry_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

164 lines
7.6 KiB
Python

#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# cryptoregistry_smt.py
#
# SMT RE-CONFIRMATION (z3) of the AereCryptoRegistry governance/routing invariants,
# the LIVE (mainnet chain 2800) crypto-agility registry at
# 0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5
#
# Contract: contracts/contracts/pqc/AereCryptoRegistry.sol
#
# These are the SAME properties the halmos bytecode suite proves
# (contracts/test/formal/AereCryptoRegistry.symbolic.t.sol, R1-R3), re-confirmed here
# at the DESIGN level with z3 so the whole PQC set has one runnable, dependency-light
# proof (halmos/forge are not installed in this environment; z3 4.16 is). This does NOT
# replace the bytecode-level halmos check -- it complements it, exactly like the other
# formal-consensus/*.py models.
#
# TARGET INVARIANTS:
# R1 FAIL-CLOSED: verify() returns false for an UNKNOWN or REVOKED id (and for a
# hash-only row, isSignature==false), BEFORE routing to any verifier -- for any
# pubkey/message/signature, regardless of what the precompile would return.
# R3 resolveActive returns ONLY an ACTIVE id and TERMINATES within the bounded walk
# (returns ACTIVE or reverts, even on a cycle).
#
# NEG-CTRLs fire for each. ASSUMPTIONS: A2 (status gate is evaluated before the
# staticcall -- true in the source), design model not bytecode (A3).
# -----------------------------------------------------------------------------
from z3 import (Int, Bool, Array, IntSort, Select, Solver, And, Or, Not, If,
Distinct, sat, unsat)
UNKNOWN, ACTIVE, DEPRECATED, REVOKED = 0, 1, 2, 3
N = 6
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:", wit)
return ok
print("### AereCryptoRegistry -- re-confirm R1 fail-closed + R3 resolveActive (z3 design layer)\n")
# verify() gate, faithful to the source order:
# if !exists return false
# if !isSignature return false
# if status in {UNKNOWN, REVOKED} return false (ACTIVE / DEPRECATED proceed)
# ... length checks ... then staticcall -> precompile bool
def verify_model(exists, is_sig, status, lengths_ok, precompile_says):
return And(exists, is_sig,
Not(Or(status == UNKNOWN, status == REVOKED)),
lengths_ok, precompile_says)
# ---- R1a : a REVOKED id never verifies (any inputs / any precompile output) -------
s = Solver()
is_sig, lengths_ok, precompile = Bool('isSig'), Bool('lengthsOk'), Bool('precompileSaysValid')
s.add(verify_model(True, is_sig, REVOKED, lengths_ok, precompile))
check("R1a REVOKED id never verifies (fail-closed before routing)", s)
# ---- R1b : an UNKNOWN id never verifies ------------------------------------------
s = Solver()
is_sig, lengths_ok, precompile = Bool('isSig'), Bool('lengthsOk'), Bool('precompileSaysValid')
s.add(verify_model(False, is_sig, UNKNOWN, lengths_ok, precompile)) # !exists
check("R1b UNKNOWN / non-existent id never verifies", s)
# ---- R1c : a hash-only row (isSignature==false, e.g. SHAKE256) never verifies -----
s = Solver()
lengths_ok, precompile = Bool('lengthsOk'), Bool('precompileSaysValid')
s.add(verify_model(True, False, ACTIVE, lengths_ok, precompile)) # isSignature == false
check("R1c hash-only ACTIVE row (SHAKE256) never verifies as a signature", s)
# sanity: an ACTIVE signature row CAN verify when the precompile accepts (not vacuous)
s = Solver()
s.add(verify_model(True, True, ACTIVE, True, True))
check("R1-sanity an ACTIVE signature row verifies when lengths ok + precompile accepts", s,
expect_unsat=False, kind="SANITY")
# ---- NEG-CTRL R1: a verify that routes to the precompile BEFORE the status gate
# would let a REVOKED row verify. ---------------------------------------------
s = Solver()
precompile = Bool('precompileSaysValid')
buggy = precompile # BUG: no status gate
s.add(buggy, precompile == True) # revoked row, precompile happens to accept
check("NEG-CTRL a verify() that skips the status gate lets a REVOKED row verify", s,
expect_unsat=False, kind="NEG-CTRL")
# ---- R3 : resolveActive returns ONLY an ACTIVE id, and terminates -----------------
status = Array('status', IntSort(), IntSort())
successor = Array('successor', IntSort(), IntSort())
exists = Array('exists', IntSort(), IntSort())
def _walk(id_expr, hop):
if hop > N:
return (True, id_expr) # SuccessorCycle revert
ex = Select(exists, id_expr); st = Select(status, id_expr); nx = Select(successor, id_expr)
adv_rev, adv_id = _walk(nx, hop + 1)
rev = If(Not(ex == 1), True, If(st == ACTIVE, False, If(nx == 0, True, adv_rev)))
rid = If(Not(ex == 1), id_expr, If(st == ACTIVE, id_expr, If(nx == 0, id_expr, adv_id)))
return (rev, rid)
s = Solver()
startId = Int('startId')
rev, resolved = _walk(startId, 0)
s.add(Not(rev)) # returned a value
s.add(Select(status, resolved) != ACTIVE) # claim it is non-ACTIVE
check("R3a resolveActive returns ONLY an ACTIVE id", s)
s = Solver()
startId = Int('startId2')
rev, resolved = _walk(startId, 0)
s.add(Not(Or(rev, Not(rev)))) # totality: always halts
check("R3b resolveActive is total within the loop bound (always ACTIVE or revert)", s)
# pigeonhole: no acyclic non-ACTIVE walk of length _count+1 (cycle revert is sound)
s = Solver()
vs = [Int(f'v{i}') for i in range(N + 1)]
for v in vs:
s.add(v >= 1, v <= N)
s.add(Distinct(*vs))
check("R3c pigeonhole: no acyclic successor walk of length _count+1 over _count rows", s)
# ---- NEG-CTRL R3: a fixed too-small loop bound (K < _count) can return before ACTIVE
# or miss a reachable ACTIVE row -> unsound. Demonstrate a 1-hop resolver that
# returns a non-ACTIVE id. -----------------------------------------------------
s = Solver()
st0 = Int('status0')
s.add(st0 != ACTIVE)
buggy_resolved_status = st0 # BUG: returns row 0 without walking
s.add(buggy_resolved_status != ACTIVE)
check("NEG-CTRL a resolver that returns the start row unconditionally yields a non-ACTIVE id", s,
expect_unsat=False, kind="NEG-CTRL")
# ------------------------------------------------------------------------------
print("\n=== SUMMARY ===")
allok = True
for name, tag, ok, kind in results:
print(f" {tag:9} [{kind}] {name}")
allok = allok and ok
print()
print("AereCryptoRegistry (design-level re-confirmation of the halmos R1/R3 suite):")
if allok:
print(" RE-CONFIRMED: verify() is fail-closed for UNKNOWN / REVOKED / hash-only rows")
print(" before any routing (R1a/b/c, any precompile output); resolveActive returns")
print(" ONLY an ACTIVE id and always terminates within the _count-bounded walk")
print(" (R3a/b/c pigeonhole). NEG-CTRLs confirm the status gate and the bounded walk")
print(" are load-bearing. The authoritative BYTECODE proof remains the halmos suite")
print(" in contracts/test/formal/AereCryptoRegistry.symbolic.t.sol (R1-R3).")
else:
print(" NOT fully established (see FAILED / unexpected result above).")
import sys
sys.exit(0 if allok else 1)