Files
zeekayandHanzo Dev a413de56af lean: the scheme IDs the deployed gate reads, and forgery relative to the adversary
StrictE2EPQ/Profile.lean modelled a scheme-ID table no Go package declares:
ML-DSA at 0x40/0x41/0x42, Pulsar at 0x50/0x51, an SLH-DSA-256s at 0x80. The
production identity scheme, ML-DSA-65, is 0x42 and was labelled devnet-only;
0x52 is Pulsar-M-65, and the SLH-DSA family is 0x61-0x63, all f variants and
on the wallet and contract-auth axes rather than the signature axis. A gate
proof over those identifiers says nothing about the gate.

Every enum now carries a `byte` function holding the value its Go constant
table assigns, so the constants are checked by the build rather than asserted
in a comment. luxStrictPQ_wire_bytes pins the eight production code points
against config/profiles.go, luxStrictPQ_blocks_disjoint states the raw-ML-DSA
/ Pulsar-M block split ChainSecurityProfile.Validate requires, and
sigSchemeID_byte_inj states that one byte names one scheme. Each enum, each
predicate and each profile field cites the Go declaration it mirrors by line.

Three consensus packages declare overlapping tables and they disagree.
pkg/wire/candidate.go:346-352 gives Pulsar-M the 0x41/0x42/0x43 bytes
config/pq_mode.go:303-305 gives raw ML-DSA, while candidate.go:342 requires the
two to be identical; byte 0x10 is ContractAuthZChainProof, post-quantum, at
config/pq_mode.go:868 and ContractAuthECDSASecp256k1Legacy, classical, at
protocol/auth/scheme_ids.go:201. The model follows config and protocol/auth,
records each divergence on the enum it affects, and where the two disagree
about admissibility takes the intersection, so IsE2EPQ never admits a scheme
either Go verifier refuses.

Quasar/Consensus.lean defined AdvForge as "the three verification bits are
true" and ignored its adversary argument. QuasarCert is a free record, so
⟨0,0,true,true,true,0,0⟩ satisfied it and quasar_triple_defense returned
AdvBreak adv a for every adversary and every assumption. That refutes the
hypotheses of single_assumption_prevents_forgery, assumption_independence and
two_break_independence, which made all three vacuously true, and made
quasar_triple_defense a way to mint the breaks it claims to require.

Forgery is now relative to provenance. AdvProduced pairs "this adversary
produced the certificate" with the EUF-CMA freshness condition, "no honest
quorum did"; both predicates are opaque, because a forgery and an honestly
finalized certificate carry identical fields and provenance is a protocol
event rather than a function of them. The three unforgeability axioms take
AdvProduced as a premise. protocol_correctness supplies the honest
certificate, without which security over an empty set of valid certificates
would say nothing. honest_cert_not_forgery and valid_cert_not_universally_forged
state what the old definition denied, and forge_not_implied_by_validity proves
that three set bits are not sufficient for AdvForge — so the definition cannot
be weakened back to a predicate over the fields without failing to compile.

The corpus: 760 theorems, 504 axioms, 0 sorry. vacuity.py is unchanged at 75;
neither file contributes an entry. LLM.md's three headline numbers and the
Quasar row were stale and are now measured.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 11:56:31 -07:00

166 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Report what every theorem in the corpus actually rests on.
A green build says each proof follows from what it was given. It does not say
what that was. `theorem safety` compiles whether it is proved from arithmetic or
handed to itself as an axiom two lines above, and only `#print axioms` tells
them apart.
Lean's own three axioms — propext, Classical.choice, Quot.sound — are the
ambient logic and are not assumptions about our systems. A theorem depending on
those alone is proved. A theorem depending on anything else is conditional on
it, and the axiom it names is load-bearing: if that axiom is false, the theorem
says nothing.
This generates a query file for every theorem in the corpus, runs it through
Lean, and reports the split, plus which of our axioms carry the most weight.
python3 axioms.py . report
python3 axioms.py . --strict exit 1 if any theorem rests on a vacuous axiom
Requires a built corpus: run `lake build` first.
"""
import re
import subprocess
import sys
from pathlib import Path
# The ambient logic of Lean plus Mathlib, not assumptions about our systems.
FOUNDATION = {"propext", "Classical.choice", "Quot.sound"}
DECL = re.compile(
r"^(?:@\[[^\]]*\]\s*)*"
r"(?:private |protected |noncomputable |partial |unsafe |scoped |local |nonrec )*"
r"(theorem|lemma)\s+([A-Za-z_][A-Za-z0-9_'!?]*)"
)
NAMESPACE = re.compile(r"^namespace\s+([A-Za-z_][A-Za-z0-9_.']*)")
END = re.compile(r"^end\s+([A-Za-z_][A-Za-z0-9_.']*)")
BLOCK = re.compile(r"/-.*?-/", re.S)
def module(path: Path, root: Path) -> str:
return str(path.relative_to(root).with_suffix("")).replace("/", ".")
def theorems(path: Path, root: Path):
"""Yield (module, fully-qualified name) for each theorem in the file."""
text = BLOCK.sub(lambda m: "\n" * m.group().count("\n"), path.read_text(
encoding="utf-8", errors="replace"))
scope, mod = [], module(path, root)
for line in text.splitlines():
line = re.sub(r"--.*$", "", line)
if m := NAMESPACE.match(line):
scope.append(m.group(1))
elif m := END.match(line):
if scope and scope[-1] == m.group(1):
scope.pop()
elif m := DECL.match(line):
yield mod, ".".join(scope + [m.group(2)])
def built(root: Path) -> set:
"""Modules the build actually produced, by their .olean.
A module in no library has no .olean, and importing one aborts the whole
query file — so the modules this cannot report on are exactly the ones
nothing checks. `lake build` names them; they are counted, not imported.
"""
lib = root / ".lake" / "build" / "lib"
return {str(p.relative_to(lib).with_suffix("")).replace("/", ".")
for p in lib.rglob("*.olean")} if lib.is_dir() else set()
def query(root: Path, found) -> str:
"""A Lean file importing every module and printing each theorem's axioms."""
mods = sorted({m for m, _ in found})
lines = [f"import {m}" for m in mods]
lines += [f"#print axioms {name}" for _, name in found]
return "\n".join(lines) + "\n"
def parse(out: str):
"""Map theorem -> set of axioms it depends on."""
deps = {}
for m in re.finditer(
r"'([^']+)' (?:depends on axioms: \[([^\]]*)\]|does not depend on any axioms)",
out):
name, axs = m.group(1), m.group(2)
deps[name] = {a.strip() for a in axs.split(",") if a.strip()} if axs else set()
return deps
def main() -> int:
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
strict = "--strict" in sys.argv
all_found = [(m, n) for p in sorted(root.rglob("*.lean"))
if p.name not in ("lakefile.lean",) and ".lake" not in p.parts
for m, n in theorems(p, root)]
have = built(root)
found = [(m, n) for m, n in all_found if m in have]
skipped = sorted({m for m, _ in all_found if m not in have})
if not found:
print("no built theorems found; run `lake build` first", file=sys.stderr)
return 1
if skipped:
print(f"not queried, module never compiled ({len(skipped)}): "
+ ", ".join(skipped) + "\n")
path = root / "axioms_query.lean"
path.write_text(query(root, found))
try:
run = subprocess.run(["lake", "env", "lean", str(path)],
cwd=root, capture_output=True, text=True, timeout=1800)
finally:
path.unlink(missing_ok=True)
deps = parse(run.stdout)
if not deps:
# On stdout, not stderr: a runner that drops stderr turns a precise
# error into "it failed", and the usual cause here is precise — two
# modules declaring the same name cannot be imported together, so the
# corpus does not load as a whole.
print("no axiom output. Lean said:\n")
print((run.stdout + run.stderr)[-3000:])
return 1
# A theorem is proved when it rests on the ambient logic alone.
proved = {n for n, a in deps.items() if a <= FOUNDATION}
conditional = {n: a - FOUNDATION for n, a in deps.items() if not a <= FOUNDATION}
weight = {}
for axs in conditional.values():
for a in axs:
weight[a] = weight.get(a, 0) + 1
for name in sorted(conditional):
print(f"{name} rests on {', '.join(sorted(conditional[name]))}")
total = len(deps)
print(f"\ntheorems queried: {total}")
print(f"proved outright: {len(proved)}")
print(f"resting on axioms: {len(conditional)}")
if weight:
print("\nload-bearing axioms, by number of theorems resting on them:")
for a, n in sorted(weight.items(), key=lambda kv: (-kv[1], kv[0]))[:20]:
print(f" {n:>4} {a}")
if strict:
# An axiom stating `True` carries no information, so a theorem resting
# on one has no more content than the axiom does.
vac = subprocess.run([sys.executable, str(root / "vacuity.py"), str(root)],
capture_output=True, text=True)
empty = {m.group(1) for m in re.finditer(r"axiom (\S+) states True", vac.stdout)}
bad = {n: a & empty for n, a in conditional.items() if a & empty}
if bad:
print(f"\n{len(bad)} theorems rest on an axiom that states `True`:")
for n in sorted(bad):
print(f" {n} <- {', '.join(sorted(bad[n]))}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())