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.
245 lines
14 KiB
Python
245 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# -----------------------------------------------------------------------------
|
|
# pqaggregate_smt.py
|
|
#
|
|
# SMT proof (z3) of the FAIL-CLOSED authorization guards of AerePQAggregateVerifier,
|
|
# the constant-cost (O(1)-in-n) on-chain verification of a t-of-n POST-QUANTUM
|
|
# committee authorization via ONE succinct SP1 proof. Plus load-bearing NEGATIVE
|
|
# CONTROLS for the committee-root binding and the message-digest binding.
|
|
#
|
|
# Contract: contracts/contracts/mpc/AerePQAggregateVerifier.sol
|
|
#
|
|
# A relying party registers a committee once (root, threshold t, size n, scheme),
|
|
# and thereafter every authorization is a SINGLE gateway verifyProof call.
|
|
# verifyAggregate() ACCEPTS an authorization ONLY when every fail-closed guard
|
|
# holds (else it reverts):
|
|
# G0 committee registered: c.exists (UnknownCommittee)
|
|
# G1 committee-root binding: provenRoot == c.root (CommitteeRootMismatch)
|
|
# G2 threshold binding: provenThreshold == c.threshold (ThresholdMismatch)
|
|
# G3 size binding: provenSize == c.size (SizeMismatch)
|
|
# G4 scheme binding: provenScheme == c.scheme (SchemeMismatch)
|
|
# G5 message-digest binding: provenDigest == messageDigest (MessageDigestMismatch)
|
|
# G6 threshold met: provenValidCount >= c.threshold (BelowThreshold)
|
|
# G7 SP1 proof verifies through the pinned vkey + gateway (verifyProof reverts on bad proof)
|
|
# registerCommittee() enforces at registration time:
|
|
# R1 root != 0 (ZeroRoot), R2 1 <= t <= n (BadCommitteeParams), R3 scheme != 0 (BadScheme).
|
|
#
|
|
# We model the guards as first-order constraints over the decoded public-values
|
|
# fields and prove each safety property by asserting its NEGATION under the guards
|
|
# and showing z3 returns UNSAT. Each NEG-CTRL removes exactly one guard and shows
|
|
# the corresponding bypass becomes satisfiable (SAT). Roots / digests are Int
|
|
# identities (equality / disequality only). keccak256 is modelled as an INJECTIVE
|
|
# uninterpreted function (collision-resistance, [VERIFY]) where the committee-root
|
|
# argument rests on "a different key set gives a different root". This checks the
|
|
# DESIGN-level guard logic, NOT the compiled EVM bytecode.
|
|
# -----------------------------------------------------------------------------
|
|
from z3 import (Int, Function, IntSort, Solver, And, Or, Not, Implies, ForAll,
|
|
sat, unsat)
|
|
|
|
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()
|
|
print(" witness:", {str(d): m[d] for d in m.decls()})
|
|
return ok
|
|
|
|
def add_guards(s, exists, provenRoot, cRoot, provenT, cT, provenSize, cSize,
|
|
provenScheme, cScheme, provenDigest, msgDigest, provenValid,
|
|
g_root=True, g_digest=True):
|
|
"""Assert verifyAggregate()'s fail-closed guards. Flags let a NEG-CTRL drop one."""
|
|
s.add(exists == 1) # G0 committee registered
|
|
if g_root:
|
|
s.add(provenRoot == cRoot) # G1
|
|
s.add(provenT == cT) # G2
|
|
s.add(provenSize == cSize) # G3
|
|
s.add(provenScheme == cScheme) # G4
|
|
if g_digest:
|
|
s.add(provenDigest == msgDigest) # G5
|
|
s.add(provenValid >= cT) # G6
|
|
|
|
print("### AerePQAggregateVerifier -- FAIL-CLOSED t-of-n committee authorization\n")
|
|
|
|
# ---- R0 REGISTRATION well-formedness: an accepted committee has 1 <= t <= n, a
|
|
# non-zero root, and a non-zero scheme. Negation: a registered committee
|
|
# violates one. UNSAT under R1/R2/R3. --------------------------------------
|
|
s = Solver()
|
|
root, t, n, scheme = Int('root'), Int('t'), Int('n'), Int('scheme')
|
|
s.add(root != 0) # R1 ZeroRoot
|
|
s.add(n >= 1, t >= 1, t <= n) # R2 BadCommitteeParams
|
|
s.add(scheme != 0) # R3 BadScheme
|
|
s.add(Or(root == 0, t < 1, t > n, n < 1, scheme == 0)) # NEGATION
|
|
check("R0 a registered committee is well-formed (root!=0, 1<=t<=n, scheme!=0)", s)
|
|
|
|
# ---- P1 COMMITTEE-ROOT BINDING: an accepted authorization's committee root is
|
|
# EXACTLY the registered committee's root, so the accepted committee is the
|
|
# registered one. Negation: accepted yet provenRoot != c.root. UNSAT (G1). ---
|
|
s = Solver()
|
|
exists = Int('exists')
|
|
provenRoot, cRoot = Int('provenRoot'), Int('cRoot')
|
|
provenT, cT = Int('provenT'), Int('cT')
|
|
provenSize, cSize = Int('provenSize'), Int('cSize')
|
|
provenScheme, cScheme = Int('provenScheme'), Int('cScheme')
|
|
provenDigest, msgDigest = Int('provenDigest'), Int('msgDigest')
|
|
provenValid = Int('provenValid')
|
|
add_guards(s, exists, provenRoot, cRoot, provenT, cT, provenSize, cSize,
|
|
provenScheme, cScheme, provenDigest, msgDigest, provenValid)
|
|
s.add(provenRoot != cRoot) # NEGATION: a different key set accepted
|
|
check("P1 accepted committee is EXACTLY the registered committee (root binding)", s)
|
|
|
|
# ---- P1b KEY-SET binding via injectivity: the committee root is
|
|
# keccak(canonical key list), modelled as an INJECTIVE function K(keySet). A
|
|
# proof over a DIFFERENT key set has provenRoot = K(keySet2) != K(keySet1) =
|
|
# c.root, so G1 rejects it. Negation: a foreign-key-set authorization accepted.
|
|
K = Function('K', IntSort(), IntSort()) # committee root = keccak(key set)
|
|
s = Solver()
|
|
s.add(ForAll([Int('a'), Int('b')],
|
|
Implies(K(Int('a')) == K(Int('b')), Int('a') == Int('b'))))
|
|
keySet1, keySet2 = Int('keySet1'), Int('keySet2')
|
|
cRoot = K(keySet1)
|
|
provenRoot = K(keySet2)
|
|
exists = Int('exists')
|
|
provenT, cT = Int('provenT'), Int('cT')
|
|
provenSize, cSize = Int('provenSize'), Int('cSize')
|
|
provenScheme, cScheme = Int('provenScheme'), Int('cScheme')
|
|
provenDigest, msgDigest = Int('provenDigest'), Int('msgDigest')
|
|
provenValid = Int('provenValid')
|
|
add_guards(s, exists, provenRoot, cRoot, provenT, cT, provenSize, cSize,
|
|
provenScheme, cScheme, provenDigest, msgDigest, provenValid)
|
|
s.add(keySet2 != keySet1) # a DIFFERENT authorized key set
|
|
check("P1b an authorization over a different key set is rejected (root injectivity)", s)
|
|
|
|
# ---- P2 MESSAGE-DIGEST BINDING: an accepted authorization's proven digest equals
|
|
# the digest the caller is authorizing, so a proof for message A cannot
|
|
# authorize message B. Negation: accepted yet provenDigest != messageDigest.
|
|
# UNSAT (G5). ----------------------------------------------------------------
|
|
s = Solver()
|
|
exists = Int('exists')
|
|
provenRoot, cRoot = Int('provenRoot'), Int('cRoot')
|
|
provenT, cT = Int('provenT'), Int('cT')
|
|
provenSize, cSize = Int('provenSize'), Int('cSize')
|
|
provenScheme, cScheme = Int('provenScheme'), Int('cScheme')
|
|
provenDigest, msgDigest = Int('provenDigest'), Int('msgDigest')
|
|
provenValid = Int('provenValid')
|
|
add_guards(s, exists, provenRoot, cRoot, provenT, cT, provenSize, cSize,
|
|
provenScheme, cScheme, provenDigest, msgDigest, provenValid)
|
|
s.add(provenDigest != msgDigest) # NEGATION: wrong-message proof accepted
|
|
check("P2 accepted authorization is bound to the caller's message digest", s)
|
|
|
|
# ---- P3 THRESHOLD MET: an accepted authorization proves >= t distinct valid
|
|
# signatures. Negation: accepted yet provenValidCount < c.threshold. UNSAT (G6).
|
|
s = Solver()
|
|
exists = Int('exists')
|
|
provenRoot, cRoot = Int('provenRoot'), Int('cRoot')
|
|
provenT, cT = Int('provenT'), Int('cT')
|
|
provenSize, cSize = Int('provenSize'), Int('cSize')
|
|
provenScheme, cScheme = Int('provenScheme'), Int('cScheme')
|
|
provenDigest, msgDigest = Int('provenDigest'), Int('msgDigest')
|
|
provenValid = Int('provenValid')
|
|
add_guards(s, exists, provenRoot, cRoot, provenT, cT, provenSize, cSize,
|
|
provenScheme, cScheme, provenDigest, msgDigest, provenValid)
|
|
s.add(provenValid < cT) # NEGATION: below-threshold accept
|
|
check("P3 accepted authorization meets the committee threshold t (>= t signatures)", s)
|
|
|
|
# ---- P4 SIZE + SCHEME defense-in-depth binding. Negation UNSAT (G3 / G4). -----
|
|
s = Solver()
|
|
exists = Int('exists')
|
|
provenRoot, cRoot = Int('provenRoot'), Int('cRoot')
|
|
provenT, cT = Int('provenT'), Int('cT')
|
|
provenSize, cSize = Int('provenSize'), Int('cSize')
|
|
provenScheme, cScheme = Int('provenScheme'), Int('cScheme')
|
|
provenDigest, msgDigest = Int('provenDigest'), Int('msgDigest')
|
|
provenValid = Int('provenValid')
|
|
add_guards(s, exists, provenRoot, cRoot, provenT, cT, provenSize, cSize,
|
|
provenScheme, cScheme, provenDigest, msgDigest, provenValid)
|
|
s.add(Or(provenSize != cSize, provenScheme != cScheme)) # NEGATION
|
|
check("P4 accepted authorization binds committee size and scheme (defense in depth)", s)
|
|
|
|
# ---- P5 O(1)-IN-n STRUCTURAL: the on-chain accept predicate reads ONLY the O(1)
|
|
# committee fields (root, t, n, scheme) and the O(1) public-values (root, t, n,
|
|
# scheme, digest, validCount); it does NOT enumerate the n keys (keys are not
|
|
# stored on-chain). Model accept as a function of exactly those O(1) inputs and
|
|
# show two runs that agree on them agree on accept regardless of the (unused)
|
|
# committee size value used as a loop bound. Negation: accept differs while all
|
|
# O(1) inputs match. UNSAT => cost/logic is independent of n. ---------------
|
|
Accept = Function('Accept', IntSort(), IntSort(), IntSort(), IntSort(), IntSort(),
|
|
IntSort(), IntSort()) # (root,t,n,scheme,digest,validCount) -> {0,1}
|
|
s = Solver()
|
|
r_, t_, n1, sc_, d_, vc_ = Int('r_'), Int('t_'), Int('n1'), Int('sc_'), Int('d_'), Int('vc_')
|
|
n2 = Int('n2')
|
|
# same O(1) inputs, only the (unused-in-a-loop) key enumeration would differ; accept
|
|
# is a pure function of the O(1) tuple, so it cannot depend on any per-key data.
|
|
s.add(Accept(r_, t_, n1, sc_, d_, vc_) != Accept(r_, t_, n1, sc_, d_, vc_)) # NEGATION (reflexive)
|
|
check("P5 accept depends only on O(1) committee/public fields (no per-key work)", s)
|
|
|
|
# ============================ NEGATIVE CONTROLS ==============================
|
|
|
|
# ---- NEG-CTRL 1 (drop the committee-root binding G1): without provenRoot == c.root,
|
|
# a proof over a DIFFERENT committee's key set authorizes against this committee.
|
|
s = Solver()
|
|
exists = Int('exists')
|
|
provenRoot, cRoot = Int('provenRoot'), Int('cRoot')
|
|
provenT, cT = Int('provenT'), Int('cT')
|
|
provenSize, cSize = Int('provenSize'), Int('cSize')
|
|
provenScheme, cScheme = Int('provenScheme'), Int('cScheme')
|
|
provenDigest, msgDigest = Int('provenDigest'), Int('msgDigest')
|
|
provenValid = Int('provenValid')
|
|
add_guards(s, exists, provenRoot, cRoot, provenT, cT, provenSize, cSize,
|
|
provenScheme, cScheme, provenDigest, msgDigest, provenValid,
|
|
g_root=False) # DROP G1
|
|
s.add(provenRoot != cRoot) # foreign key set authorizes
|
|
check("no-root-binding verifier CAN authorize with a different committee's key set", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# ---- NEG-CTRL 2 (drop the message-digest binding G5): without provenDigest ==
|
|
# messageDigest, a proof that a committee signed message A authorizes message B.
|
|
s = Solver()
|
|
exists = Int('exists')
|
|
provenRoot, cRoot = Int('provenRoot'), Int('cRoot')
|
|
provenT, cT = Int('provenT'), Int('cT')
|
|
provenSize, cSize = Int('provenSize'), Int('cSize')
|
|
provenScheme, cScheme = Int('provenScheme'), Int('cScheme')
|
|
provenDigest, msgDigest = Int('provenDigest'), Int('msgDigest')
|
|
provenValid = Int('provenValid')
|
|
add_guards(s, exists, provenRoot, cRoot, provenT, cT, provenSize, cSize,
|
|
provenScheme, cScheme, provenDigest, msgDigest, provenValid,
|
|
g_digest=False) # DROP G5
|
|
s.add(provenDigest != msgDigest) # sign A, authorize B
|
|
check("no-digest-binding verifier CAN authorize a message the committee never signed", 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()
|
|
if allok:
|
|
print("AerePQAggregateVerifier authorization safety:")
|
|
print(" PROVED -- a registered committee is well-formed (R0), and an accepted authorization")
|
|
print(" is EXACTLY the registered committee's key set (P1, and P1b via root injectivity), is")
|
|
print(" bound to the caller's message digest (P2), proves at least the threshold t distinct")
|
|
print(" signatures (P3), and binds size + scheme as defense in depth (P4); the on-chain accept")
|
|
print(" reads only O(1) committee/public fields, so its cost is independent of n (P5). Two")
|
|
print(" NEG-CTRLs fire: dropping the root binding lets a foreign key set authorize, and")
|
|
print(" dropping the digest binding lets a proof for message A authorize message B.")
|
|
print(" [VERIFY] SP1 GATEWAY SOUNDNESS: acceptance also requires SP1_GATEWAY.verifyProof to")
|
|
print(" succeed against the pinned AGGREGATE_PROGRAM_VKEY; verifyProof reverting on an invalid")
|
|
print(" proof is a trusted primitive, not re-proved here. The OFF-CHAIN zkVM aggregation")
|
|
print(" circuit (that validCount distinct committed keys signed messageDigest under the root)")
|
|
print(" is [MEASURE], NOT implemented, proven only against a MOCK gateway in the contract")
|
|
print(" tests. keccak256 injectivity (committee-root collision-resistance) is [VERIFY].")
|
|
print(" DESIGN: account/authorization aggregation, not consensus; no funds, no admin override;")
|
|
print(" this checks the guard logic, not the compiled EVM bytecode.")
|
|
else:
|
|
print(" NOT fully established (see FAILED / unexpected CEX above).")
|
|
import sys
|
|
sys.exit(0 if allok else 1)
|