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.
176 lines
10 KiB
Python
176 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
# -----------------------------------------------------------------------------
|
|
# reputation_gate_smt.py
|
|
#
|
|
# SMT proof (z3) of the M1-FIX authorization + delta-clamp invariant of
|
|
# AereReputationRegistry8004, the ERC-8004 Reputation adapter over the LIVE
|
|
# AereAIReputation. Plus load-bearing NEGATIVE CONTROLS for the author gate (the
|
|
# M1 bug: anyone could drive a score) and for the delta clamp.
|
|
#
|
|
# Contract: contracts/contracts/erc8004/AereReputationRegistry8004.sol
|
|
#
|
|
# Because every caller of this adapter collapses into ONE on-chain attestor identity
|
|
# (the adapter) on the live AereAIReputation, an UNGATED write path (the pre-fix M1
|
|
# state) would let ANY address inflate or tank any agent's reputation. The M1 fix
|
|
# gates giveFeedback to an owner-curated allowlist AND keeps the existing delta clamp,
|
|
# so a forwarded attestation is authorized AND a bounded unit step, never an arbitrary
|
|
# value set. giveFeedback fail-closes on each guard (else revert, forwards nothing):
|
|
# G0 author gate (M1 fix): isAuthorizedFeedbackAuthor[msg.sender] (NotAuthorizedFeedbackAuthor)
|
|
# G1 agent registered: IDENTITY.isRegistered(agentId) (AgentNotRegistered)
|
|
# G2 delta clamp: -1 <= delta <= 1 (InvalidDelta)
|
|
# G3 adapter is attestor: REPUTATION.isAttestor(this) (AdapterNotAttestor)
|
|
# then forwards REPUTATION.attest(operator, key, delta, ...).
|
|
#
|
|
# FORWARD-SAFETY INVARIANT (the M1 target):
|
|
# a forwarded attestation happens => (author is authorized) AND (delta in {-1,0,+1})
|
|
#
|
|
# So the ONLY way this adapter moves an agent's live reputation is an authorized
|
|
# author submitting a clamped unit delta; the owner curates WHO may write but can
|
|
# neither set a reputation value directly nor exceed the clamp every author is held
|
|
# to, and moves no funds.
|
|
#
|
|
# Method: model the fail-closed guards as first-order constraints and prove each
|
|
# property by asserting its NEGATION under the guards (UNSAT). Each NEG-CTRL removes
|
|
# exactly one guard and shows the corresponding attack becomes SAT.
|
|
#
|
|
# HONEST SCOPE / OVERLAP. The authorization half of this property is also exercised
|
|
# directly by the Hardhat test "gates giveFeedback to owner-authorized feedback
|
|
# authors (M1)" in test/erc8004-adapters.test.js. The z3 value-add here is (a) proving
|
|
# the COMPOSITE invariant (authorized AND unit-clamped) is the only way a forward can
|
|
# occur, and (b) the firing negative control that reproduces the exact M1 "anyone
|
|
# drives the score" bug, showing the gate is load-bearing. The live AereAIReputation
|
|
# computes the actual score FROM the delta; this model bounds only what the adapter
|
|
# forwards (an authorized, clamped delta), not the live contract's internal score
|
|
# formula. Application-layer, no funds, consensus classical ECDSA. DESIGN-level logic,
|
|
# NOT the compiled EVM bytecode.
|
|
# -----------------------------------------------------------------------------
|
|
from z3 import Int, Bool, Solver, And, Or, Not, Implies, 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 clamped(delta):
|
|
"""InvalidDelta guard: delta in {-1, 0, +1}."""
|
|
return And(delta >= -1, delta <= 1)
|
|
|
|
def forward_ok(authorized, registered, delta, adapterIsAttestor):
|
|
"""giveFeedback's fail-closed guards G0..G3. A forwarded attest happens iff all hold."""
|
|
return And(authorized, registered, clamped(delta), adapterIsAttestor)
|
|
|
|
print("### AereReputationRegistry8004 -- M1 AUTHOR GATE + DELTA CLAMP (forward-safety)\n")
|
|
|
|
# ---- G0 AUTHORIZATION REQUIRED (M1 fix): a forwarded attestation requires an
|
|
# authorized author. Negation: a forward happens with an unauthorized caller. UNSAT.
|
|
s = Solver()
|
|
authorized, registered, adapterIsAttestor = Bool('authorized'), Bool('registered'), Bool('adapterIsAttestor')
|
|
delta = Int('delta')
|
|
s.add(forward_ok(authorized, registered, delta, adapterIsAttestor))
|
|
s.add(Not(authorized)) # NEGATION: unauthorized caller moved a score
|
|
check("G0 only an authorized feedback author can move a score (M1 gate)", s)
|
|
|
|
# ---- G2 DELTA CLAMP: a forwarded attestation carries delta in {-1,0,+1}, so no
|
|
# author (nor the owner) can forge an arbitrary reputation move. Negation: a
|
|
# forward happens with an out-of-range delta. UNSAT.
|
|
s = Solver()
|
|
authorized, registered, adapterIsAttestor = Bool('authorized'), Bool('registered'), Bool('adapterIsAttestor')
|
|
delta = Int('delta')
|
|
s.add(forward_ok(authorized, registered, delta, adapterIsAttestor))
|
|
s.add(Or(delta < -1, delta > 1)) # NEGATION: arbitrary delta forwarded
|
|
check("G2 a forwarded delta is clamped to {-1,0,+1} (no arbitrary value set)", s)
|
|
|
|
# ---- M1 COMPOSITE forward-safety: any score move through the adapter is BOTH by an
|
|
# authorized author AND a clamped unit step. Negation: a forward happens yet the
|
|
# author is unauthorized OR the delta is out of range. UNSAT.
|
|
s = Solver()
|
|
authorized, registered, adapterIsAttestor = Bool('authorized'), Bool('registered'), Bool('adapterIsAttestor')
|
|
delta = Int('delta')
|
|
s.add(forward_ok(authorized, registered, delta, adapterIsAttestor))
|
|
s.add(Or(Not(authorized), delta < -1, delta > 1)) # NEGATION
|
|
check("M1 a score move requires (authorized author) AND (clamped unit delta)", s)
|
|
|
|
# ---- G1 REGISTERED AGENT required (fail-closed): a forward requires a registered
|
|
# agentId. Negation: a forward about an unregistered agent. UNSAT.
|
|
s = Solver()
|
|
authorized, registered, adapterIsAttestor = Bool('authorized'), Bool('registered'), Bool('adapterIsAttestor')
|
|
delta = Int('delta')
|
|
s.add(forward_ok(authorized, registered, delta, adapterIsAttestor))
|
|
s.add(Not(registered)) # NEGATION: feedback about an unknown agent
|
|
check("G1 feedback requires a registered agent (fail-closed)", s)
|
|
|
|
# ---- G3 ADAPTER-IS-ATTESTOR required (fail-closed): a forward requires this adapter
|
|
# to be an authorized attestor on the live reputation. Negation: forward without
|
|
# the adapter being an attestor. UNSAT.
|
|
s = Solver()
|
|
authorized, registered, adapterIsAttestor = Bool('authorized'), Bool('registered'), Bool('adapterIsAttestor')
|
|
delta = Int('delta')
|
|
s.add(forward_ok(authorized, registered, delta, adapterIsAttestor))
|
|
s.add(Not(adapterIsAttestor)) # NEGATION: writes through a non-attestor adapter
|
|
check("G3 forwarding requires the adapter to be a live attestor (fail-closed)", s)
|
|
|
|
# ============================ NEGATIVE CONTROLS ==============================
|
|
|
|
# ---- NEG-CTRL 1 (drop the author gate -> the M1 bug): an UNGATED giveFeedback (the
|
|
# pre-fix state) lets ANY caller drive an agent's score. Since every caller
|
|
# collapses into the single adapter attestor, an unauthorized address writes.
|
|
s = Solver()
|
|
authorized, registered, adapterIsAttestor = Bool('authorized'), Bool('registered'), Bool('adapterIsAttestor')
|
|
delta = Int('delta')
|
|
# BUG: the G0 author gate is removed; a forward needs only registered + clamp + attestor.
|
|
buggy_forward = And(registered, clamped(delta), adapterIsAttestor)
|
|
s.add(buggy_forward)
|
|
s.add(Not(authorized)) # an UNauthorized caller still drives the score
|
|
check("no-author-gate adapter CAN let anyone drive an agent's score (reproduces M1)", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# ---- NEG-CTRL 2 (drop the delta clamp): a BUGGY adapter without the {-1,0,+1} clamp
|
|
# forwards an arbitrary delta, letting even an authorized author forge a large
|
|
# reputation swing in one call. z3 finds the out-of-range delta.
|
|
s = Solver()
|
|
authorized, registered, adapterIsAttestor = Bool('authorized'), Bool('registered'), Bool('adapterIsAttestor')
|
|
delta = Int('delta')
|
|
# BUG: the G2 clamp is removed; a forward needs only author + registered + attestor.
|
|
buggy_forward = And(authorized, registered, adapterIsAttestor)
|
|
s.add(buggy_forward)
|
|
s.add(delta > 1) # an arbitrary large delta is forwarded
|
|
check("no-delta-clamp adapter CAN forward an arbitrary reputation delta", 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("AereReputationRegistry8004 M1 forward-safety:")
|
|
print(" PROVED -- a forwarded attestation requires an authorized feedback author (G0, the M1")
|
|
print(" fix), a delta clamped to {-1,0,+1} (G2, no arbitrary value set), a registered agent")
|
|
print(" (G1) and the adapter being a live attestor (G3); the composite M1 property holds: the")
|
|
print(" ONLY way to move an agent's score through the adapter is an authorized author with a")
|
|
print(" clamped unit delta. Two NEG-CTRLs fire: dropping the author gate reproduces the exact")
|
|
print(" M1 bug (anyone drives the score), and dropping the clamp lets an arbitrary delta")
|
|
print(" through, so both guards are load-bearing.")
|
|
print(" OVERLAP / SCOPE (honest): the authorization half is also covered directly by the")
|
|
print(" Hardhat test 'gates giveFeedback to owner-authorized feedback authors (M1)'; the z3")
|
|
print(" value-add is the composite invariant and the firing non-vacuity control. The owner")
|
|
print(" curates WHO may write but cannot set a reputation value or exceed the clamp. The live")
|
|
print(" AereAIReputation computes the score FROM the delta; this model bounds what the adapter")
|
|
print(" forwards (authorized + clamped), not the live contract's score formula. Application-")
|
|
print(" layer, no funds, consensus classical ECDSA. DESIGN-level guard logic, not the bytecode.")
|
|
else:
|
|
print(" NOT fully established (see FAILED / unexpected CEX above).")
|
|
import sys
|
|
sys.exit(0 if allok else 1)
|