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.
107 lines
5.1 KiB
Python
107 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
# SMT proof (z3) of the DISTINCT-KEY THRESHOLD property for AereThresholdAccount +
|
|
# AereThresholdPQCRegistry, formalising the duplicate-key finding fixed 2026-07-15.
|
|
#
|
|
# THE BUG: authorization counts distinct signers by member INDEX (the `seen` bitmask
|
|
# in _countMem / the strict DuplicateMember check). Member indices are 0..n-1, so they
|
|
# are ALWAYS distinct -- the index dedup can never fail on a real committee. Distinctness
|
|
# of the actual signing KEYS was never checked at committee registration, so the same
|
|
# public key could occupy two indices and one keyholder could fill multiple "distinct
|
|
# member" slots and reach the threshold t alone, collapsing the t-of-n guarantee.
|
|
#
|
|
# THE FIX: reject duplicate committee keys at the one place a committee is set
|
|
# (initialize / registerCommittee). Modelled here as Distinct(key).
|
|
#
|
|
# MODEL: key[i] = the id of the KEYHOLDER controlling slot i. A single keyholder h
|
|
# occupies slot i iff key[i] == h; the number of slots it can validly sign for is
|
|
# count(i: key[i] == h). "One keyholder reaches threshold" := exists h with count >= t.
|
|
#
|
|
# METHOD: assert the property's NEGATION; UNSAT => the property holds.
|
|
|
|
from z3 import Int, Solver, Sum, If, Distinct, sat, unsat
|
|
|
|
results = []
|
|
|
|
def check(name, s, expect_unsat=True, kind="PROOF"):
|
|
r = s.check()
|
|
ok = (r == unsat) if expect_unsat else (r == sat)
|
|
tag = "PROVED" if (ok and expect_unsat) else ("CEX-FOUND" if (ok and not expect_unsat) else "FAILED")
|
|
results.append((name, tag, ok, kind))
|
|
print(f"[{tag}] ({kind}) {name}: z3={r}")
|
|
if r == sat and not expect_unsat:
|
|
m = s.model()
|
|
print(f" witness: {m}")
|
|
return ok
|
|
|
|
def keys(N):
|
|
return [Int(f"key_{i}") for i in range(N)]
|
|
|
|
def count_for(key, h):
|
|
return Sum([If(key[i] == h, 1, 0) for i in range(len(key))])
|
|
|
|
print("### AereThresholdAccount / PQC registry -- DISTINCT-KEY THRESHOLD (dup-key finding 2026-07-15)\n")
|
|
print(" Distinct signers are counted by member INDEX (0..n-1, always distinct), so the")
|
|
print(" index dedup never fires on a real committee. The guarantee that matters is that")
|
|
print(" the KEYS are distinct. Proved below; the pre-fix (no key-distinctness) is CEX'd.\n")
|
|
|
|
print("=" * 74)
|
|
print("P1 distinct committee keys => every keyholder fills at most 1 slot, so reaching")
|
|
print(" threshold t>=2 requires >= t DISTINCT keyholders (the t-of-n guarantee holds)")
|
|
print("=" * 74)
|
|
for (N, T) in [(5, 2), (5, 3), (9, 5), (7, 4), (21, 11)]:
|
|
s = Solver()
|
|
key = keys(N)
|
|
h = Int("h")
|
|
s.add(Distinct(key)) # THE FIX: committee keys are pairwise distinct
|
|
s.add(count_for(key, h) >= T) # NEGATION: some single keyholder h fills >= t slots
|
|
check(f"N={N} t={T}: distinct keys AND one keyholder fills >= t is impossible", s, expect_unsat=True)
|
|
|
|
print("\n" + "=" * 74)
|
|
print("P2 NEG-CONTROL: WITHOUT the distinct-key check, one keyholder CAN reach the")
|
|
print(" threshold alone (the exact pre-fix bug: a committee like [A,A,A,B,C], t=3)")
|
|
print("=" * 74)
|
|
for (N, T) in [(5, 3), (9, 5)]:
|
|
s = Solver()
|
|
key = keys(N)
|
|
h = Int("h")
|
|
# No Distinct(key): a keyholder may occupy multiple indices (duplicate keys allowed).
|
|
s.add(count_for(key, h) >= T)
|
|
check(f"N={N} t={T}: no key-distinctness => one keyholder filling >= t is POSSIBLE",
|
|
s, expect_unsat=False, kind="NEG-CONTROL")
|
|
|
|
print("\n" + "=" * 74)
|
|
print("P3 index-distinctness ALONE does not imply key-distinctness (why the seen-bitmask")
|
|
print(" dedup was insufficient): indices are trivially distinct, keys can still repeat")
|
|
print("=" * 74)
|
|
for (N, T) in [(5, 3)]:
|
|
s = Solver()
|
|
key = keys(N)
|
|
h = Int("h")
|
|
idx = [Int(f"idx_{i}") for i in range(N)]
|
|
s.add(Distinct(idx)) # the index dedup the contract DOES have
|
|
s.add([idx[i] == i for i in range(N)]) # indices are 0..n-1 (always satisfiable)
|
|
s.add(count_for(key, h) >= T) # yet one keyholder still fills >= t slots
|
|
check(f"N={N} t={T}: indices distinct yet one keyholder fills >= t (key dedup needed)",
|
|
s, expect_unsat=False, kind="NEG-CONTROL")
|
|
|
|
print("\n=== SUMMARY (distinct-key threshold, AereThresholdAccount / PQC registry) ===")
|
|
allok = True
|
|
for name, tag, ok, kind in results:
|
|
print(f" {tag:9} [{kind}] {name}")
|
|
allok = allok and ok
|
|
print()
|
|
if allok:
|
|
print(" ESTABLISHED: the 2026-07-15 fix (reject duplicate committee keys at registration)")
|
|
print(" is exactly what makes t-of-n sound. P1 PROVES that with distinct keys no single")
|
|
print(" keyholder can reach threshold t>=2, so t signatures require t distinct parties.")
|
|
print(" P2/P3 are load-bearing neg-controls: without key-distinctness (index dedup alone),")
|
|
print(" one keyholder reaches the threshold alone -- the collapsed-guarantee bug that was")
|
|
print(" found by manual review and fixed + redeployed (factory V2, PQC registry V2).")
|
|
print(" BOUNDARY: a combinatorial model of the counting logic, not the Solidity bytecode;")
|
|
print(" complements the on-chain proof that the live V2 reverts DuplicatePubKey.")
|
|
else:
|
|
print(" NOT fully established (see FAILED above).")
|
|
|
|
import sys
|
|
sys.exit(0 if allok else 1)
|