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.
164 lines
7.6 KiB
Python
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)
|