mirror of
https://github.com/luxfi/proofs.git
synced 2026-10-30 15:21:05 +00:00
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]>
166 lines
6.3 KiB
Python
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())
|