aere-research/formal-consensus/agentdid_session_smt.py
Aere Network 6cb0140fae Republished from a clean root: the compiled artifact is gone from history, and the local line of work joins the sanitized public line
The public history carried kat/__pycache__/mlkem768_reference.cpython-314.pyc,
a compiled Python artifact embedding the operator's absolute local path. Text
secret scanners do not read compiled binaries, which is exactly how it slipped
through, and removing it from the tip would have left it reachable through the
old root commits. So this repository is republished from a single clean root.

This root also carries, from the previously unpublished line of work:
- corrected LICENSE year, LICENSING.md, VERIFY-POLICY.md, and
  CITATIONS-UNRESOLVED.md remeasured 2026-08-11 (101 paths, README aligned)
- O-018: run_consensus_verification.py ran 19 of 29 models and reported PASS;
  it now runs all 29, and computemarket_smt.py gains resolveByTimeout /
  reclaimUnsettled cases plus a negative control
- O-006: the word 'audited' removed from next to Bouncy Castle, twice, after a
  concurrent edit resurrected it
- O-014: prior art named and dated - Algorand's native falcon_verify shipped
  about ten months before AERE's precompiles; the primacy claim is withdrawn
  where it was implied
- bench/ scripts parametrized so they actually run for an outsider (the
  earlier textual sanitization left $STAGING unexpanded inside Python strings)
- AIP-2/AIP-3 errata with measured figures, spec remeasurements at 2026-08-01,
  and the spec-zk-stack retractions (owner is an operational key, not the
  Foundation; 'maximally sound' withdrawn; aggregator V1 deprecated)
The redacted bench-host environment files from the sanitized line are kept
exactly as published; the unredacted local variants are not carried.
2026-08-15 13:52:14 +03:00

168 lines
8.5 KiB
Python

#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# agentdid_session_smt.py
#
# SMT proof (z3) of the SESSION AUTHORIZATION invariants for AereAgentDID: the
# per-session cumulative SPEND CAP, the expiry / revocation / root-lifecycle
# gating, and the anti-replay action nonce.
#
# Contract: contracts/contracts/pqc/AereAgentDID.sol (authorize / issueSession /
# revokeSession / _isSessionLive)
#
# This is a hot-key authorization surface: a leaked secp256k1 session key must be
# contained by (a) a cumulative spend cap it can never exceed, (b) an expiry after
# which it stops working, (c) an on-chain revocation, (d) its root Falcon key still
# being ACTIVE, and (e) a strictly-increasing action nonce so a captured signature
# cannot be replayed. We model authorize() as an inductive transition system on
# (spent, actionNonce) with the exact Solidity guards.
#
# Per session:
# cap = spendCap (immutable at issuance)
# spent = cumulative spend recorded so far
# n = actionNonce
# live = exists AND NOT revoked AND now <= expiry AND rootActive AND rootFalcon
#
# authorize(amount) Solidity guards, then effects:
# require live (_isSessionLive)
# require scope == scopeHash (scope gate)
# require spent+amount >= spent AND spent+amount <= cap (overflow + CAP gate)
# require ecrecover(...) == sessionAddr (sig gate)
# s.actionNonce = n + 1; s.spent = spent + amount
#
# SPEND-CAP INVARIANT (task target): spent <= cap, ALWAYS.
# NONCE INVARIANT (anti-replay): actionNonce strictly increases on every
# successful authorize, so no two successful actions share a digest.
# GATING INVARIANT: a non-live session (revoked / expired / root-inactive) can
# NEVER record spend (authorize reverts, state unchanged).
#
# Method: PROVED = negation UNSAT under guards. NEG-CTRL = buggy variant's
# violation SAT (real counterexample => guard is load-bearing).
#
# ASSUMPTIONS (bound every PROVED):
# A1. ecrecover is a sound signature oracle: it returns sessionAddr ONLY for a
# genuine session-key signature over the exact digest. Its cryptographic
# soundness is out of scope (same class as the halmos precompile-oracle
# assumption). We model the sig gate as a boolean the adversary can only
# satisfy with a real signature; the cap/nonce/gating proofs hold for BOTH
# branches (sig ok / not ok), i.e. regardless of what ecrecover returns.
# A2. spendCap is fixed at issuance and never mutated (confirmed: no setter).
# A3. Design math (unbounded Ints). Solidity 0.8 checked add on spent+amount is
# modelled by the explicit overflow guard already present in the source.
# -----------------------------------------------------------------------------
from z3 import Int, Bool, Solver, And, Or, Not, If, 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()
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("### AereAgentDID -- session spend-cap / gating / anti-replay\n")
# ---- BASE CASE: a freshly issued session. issueSession sets spent=0, and
# InvalidSessionParams requires expiry > block.timestamp, so cap>=0 and
# spent(0) <= cap holds at birth. ------------------------------------------
s = Solver()
cap = Int('cap')
s.add(cap >= 0)
s.add(Not(0 <= cap))
check("base case (freshly issued session) satisfies spent<=cap", s)
# ---- C1 SPEND CAP inductive: authorize() preserves spent <= cap ---------------
# Guards: overflow guard (spent+amount does not wrap) AND spent+amount <= cap.
# The Solidity condition is: revert IF (spent+amount < spent) OR (spent+amount > cap).
# So the SUCCESS path requires: spent+amount >= spent AND spent+amount <= cap.
s = Solver()
cap, spent, amount = Int('cap'), Int('spent'), Int('amount')
s.add(spent >= 0, spent <= cap, amount >= 0) # INV(pre) + amount is a uint
s.add(spent + amount >= spent, spent + amount <= cap) # authorize success guard
spent2 = spent + amount
s.add(Not(spent2 <= cap)) # negate INV(post)
check("C1 authorize() preserves spend cap (spent <= cap inductive)", s)
# ---- C2 GATING: a non-live session can never record spend --------------------
# _isSessionLive must be TRUE to reach the effects. If the session is revoked OR
# expired OR its root is not ACTIVE Falcon, authorize reverts (no state change).
# We show: (NOT live) AND (reached effects) is infeasible -- the guard blocks it.
s = Solver()
revoked, expired, rootInactive, reachedEffects = Bool('revoked'), Bool('expired'), Bool('rootInactive'), Bool('reachedEffects')
live = And(Not(revoked), Not(expired), Not(rootInactive))
# reachedEffects can only be true when live is true (that is the guard).
s.add(reachedEffects == live)
s.add(Or(revoked, expired, rootInactive)) # session is NOT live
s.add(reachedEffects) # but we claim spend was recorded
check("C2 gating: revoked/expired/root-inactive session cannot record spend", s)
# ---- C3 ANTI-REPLAY: actionNonce strictly increases on each successful authorize
# (n -> n+1), so two successful actions never share the signed digest. ------
s = Solver()
n = Int('n')
s.add(n >= 0)
n2 = n + 1
s.add(Not(n2 > n)) # negate strict-increase
check("C3 anti-replay: actionNonce strictly increases (n+1 > n)", s)
# ---- C4 EXPIRY monotonic: once now > expiry the session is dead forever (expiry
# is immutable and now is non-decreasing across blocks). ------------------
s = Solver()
expiry, now1, now2 = Int('expiry'), Int('now1'), Int('now2')
s.add(now2 >= now1, now1 > expiry) # already expired at now1, time advances
s.add(Not(now2 > expiry)) # can it become un-expired later?
check("C4 expiry is terminal: now>expiry stays true as time advances", s)
# ---- NEG-CTRL 1: a BUGGY cap gate that checks the PER-ACTION amount against the
# cap (amount <= cap) instead of the CUMULATIVE spent+amount. Over several
# actions the cumulative spent blows past cap. z3 finds the overshoot. ------
s = Solver()
cap, spent, amount = Int('cap'), Int('spent'), Int('amount')
s.add(spent >= 0, spent <= cap, amount >= 0)
s.add(amount <= cap) # BUG: per-action check only
spent2 = spent + amount
s.add(spent2 > cap) # cumulative exceeds cap
check("NEG-CTRL per-action cap check CAN let cumulative spent exceed cap", s,
expect_unsat=False, kind="NEG-CTRL")
# ---- NEG-CTRL 2: a BUGGY gate that OMITS the liveness check (records spend even
# when revoked). z3 finds a revoked session recording spend. ---------------
s = Solver()
revoked = Bool('revoked')
reachedEffects = Bool('reachedEffects')
s.add(reachedEffects == True) # BUG: effects reached unconditionally
s.add(revoked == True) # session revoked
s.add(reachedEffects, revoked) # revoked session still spends
check("NEG-CTRL missing-liveness gate CAN let a revoked session record spend", 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("AereAgentDID session authorization:")
if allok:
print(" PROVED the spend cap is inductive (C1: spent<=cap for any sequence of actions),")
print(" non-live sessions cannot spend (C2: revoked/expired/root-inactive all blocked),")
print(" the action nonce strictly increases (C3: no signature replay), and expiry is")
print(" terminal (C4). Two NEG-CTRLs confirm the CUMULATIVE cap check and the liveness")
print(" gate are load-bearing. Signature soundness of ecrecover is assumed (A1).")
else:
print(" NOT fully established (see FAILED / unexpected CEX above).")
import sys
sys.exit(0 if allok else 1)