mirror of
https://github.com/saymrwulf/anza-ed25519-verified.git
synced 2026-09-04 20:24:06 +00:00
verification: pin the whole declaration surface (P1-b)
Phase 2b asks the kernel whether any AXIOM is declared under Proofs/. Phase 3
pins the cones of the named certificates. Between them sat every other
declaration in the corpus — around three thousand of them — and a helper lemma
quietly acquiring a hash oracle in its cone moved nothing either phase looked
at.
Phase 2c closes that. Ported from ltl-accumulator-verified, where a nine-attack
self-test proved a source-regex enumerator evadable by attributed, private,
indented and `instance` declarations and by a nested-namespace basename
collision. Reading the compiled environment sees what the kernel saw; no name
shape hides. Every constant contributes module, name, kind and full axiom cone,
and the observed set must equal inventory-allowlist.txt exactly in BOTH
directions, with a count trailer so a truncated run cannot pass as an empty
diff.
FOUR THINGS THIS BUILD GOT WRONG, each caught by a check rather than by review:
- The number of inventory drivers is a per-repo FACT, not an assumption.
dalek and anza cannot import their corpus as one environment (Proofs.Basic
and Proofs.ConstSpecs both declare CurveFieldProofs.zero_spec); risc0 and
betrusted have no Proofs.Basic at all. Determined by compiling a probe.
check.sh now DISCOVERS its drivers from the filesystem instead of naming
two, and the generator refuses to split out a module the repo lacks.
- The split let one real declaration hide behind another's entry. Keyed on
name alone, the two zero_specs produced byte-identical records, so 3022
declarations were covered by 3021 allowlist entries. Caught by the count
trailer. Every record now carries its originating module.
- The gate's success line said "single sanctioned axiom", inherited from the
accumulator's policy. This corpus permits NONE. A success message
describing a different rule is how an assertion stops meaning anything.
- selftest-axgate.sh lifted Phase 2b with a range ending at "Phase 3", so
inserting Phase 2c between them made it swallow the new phase and die on
variables only check.sh defines — surfacing as the BASELINE case failing,
a self-test blaming a gate for its own extraction bug. Both self-tests now
stop at the next phase marker whatever it is called, and refuse to run if
they capture more than one phase. The guard is the fix; the range was the
symptom.
WHAT THIS IS NOT, recorded in TRUSTED-BASE.md at the same length as the claim:
- No independent cone walker. The accumulator cross-checks collectAxioms
against a hand-written walker. Ported here it was wrong in BOTH directions
on mathlib's inductive shapes: EdPoint gave [] against the kernel's three
axioms, and once extended, ProjPoint gave three against the kernel's none.
Two implementations disagreeing both ways are a second wrong answer, not a
check. These cones rest on collectAxioms alone.
- Thirteen Proofs/Scalar* modules are inventoried by nothing — the
second-button seam, still open. Phase 2c names every uncovered module on
every run so the omission is visible rather than inferred.
selftest-inventory.sh exercises the shipping gate with six cases, each
asserting a specific diagnostic, including the one that matters: a cone
widened by one oracle while name, module and kind stay put. Negative-tested by
disabling the gate's diff, which turns two cases red including one for the
wrong reason, correctly reported as such.
Verified green: 20 runs across the four repositories (four buttons, four
harness, four inventory, four axgate, four binding self-tests), zero red. The
four check-scalar.sh greens from the preceding sweep stand: that script neither
reads the pin file nor changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b3f4ee0d33
commit
a8998dd94b
11 changed files with 3558 additions and 8 deletions
|
|
@ -111,3 +111,38 @@ running Rust code. Everything else is machine-checked.
|
|||
consumer's protection is, and has always been, *review at the pinned
|
||||
commit* rather than the button's own verdict. A green button says "this is
|
||||
the apparatus that was reviewed", never "this apparatus is trustworthy".
|
||||
|
||||
10. **The whole declaration surface is pinned, not just the certificates.**
|
||||
`check.sh` Phase 2c reads the compiled environment and records, for EVERY
|
||||
constant originating in an audited module — roughly three thousand of them,
|
||||
compiler-generated auxiliaries included — its originating module, fully
|
||||
qualified name, declaration kind and complete axiom cone. The observed set
|
||||
must equal `inventory-allowlist.txt` exactly, in BOTH directions: a
|
||||
declaration present but not allowlisted (`UNCLASSIFIED`) and an allowlist
|
||||
entry with no declaration (`STALE`) are both build failures, and a count
|
||||
trailer disagreeing with the lines received is a third.
|
||||
|
||||
The gap this closes: Phase 2b asks the kernel only whether an AXIOM is
|
||||
declared, and Phase 3 pins the cones of the named certificates. Between
|
||||
them sat every helper lemma in the corpus. One of those quietly acquiring
|
||||
a hash oracle in its cone moved nothing either phase looked at.
|
||||
|
||||
**Two limits, stated because a reader would otherwise assume neither.**
|
||||
|
||||
· *No independent cone walker here.* The companion accumulator runs a
|
||||
hand-written closure walker alongside the kernel's `collectAxioms` and
|
||||
requires the two to agree on every constant, so each checks the other.
|
||||
Ported to this corpus on 2026-07-29 that walker was wrong in BOTH
|
||||
directions on mathlib's inductive shapes — it reported no axioms for
|
||||
`CurveFieldProofs.EdPoint` where the kernel reported three, and after
|
||||
being extended it reported three for `CurveFieldProofs.ProjPoint` where
|
||||
the kernel reported none. Two implementations disagreeing both ways are
|
||||
not a cross-check; they are a second wrong answer. The cone figures here
|
||||
therefore rest on `collectAxioms` alone. The accumulator keeps its
|
||||
cross-check, its corpus being mathlib-free.
|
||||
|
||||
· *The scalar layer is outside this phase.* Thirteen `Proofs/Scalar*`
|
||||
modules belong to `check-scalar.sh` and are inventoried by nothing. That
|
||||
is the two-button seam, still open. Phase 2c prints every uncovered
|
||||
module by name on every run, so the omission is visible rather than
|
||||
inferred.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
12dc724bffd590e6f706573d97bf07425b8f268a1be2d72a3bbd9aef48f9c277 AUDIT-MANIFEST.txt
|
||||
b756f5056d44d68929554e578363674c5ddc431325b087200c1ce315d4e3e543 check-scalar.sh
|
||||
7049514077a316f0f0ce8513f197efea8966264839ed9cca223349ae4bba1a36 check.sh
|
||||
aa18745932213d15be4f21c57c92dba158fc3385e2d0bf7d26f3bccb5d1359de check.sh
|
||||
64602c31ef34e740a0a27431534fa2ca65b6cd90a99cb17e119915a82a38b474 extract.sh
|
||||
52afbe130c5551686f45643a35065729fd5bb8166b5fa3db67b74c60ba3eff62 GEN-MODEL.sha256
|
||||
b2c79ccf7005051946d49ee1fb0b51fbee58819c5909c6efb00965547b56fa5d inventory-allowlist.txt
|
||||
0bb01bc4abaafa8537d460682004d1f336980b28bc4fe1968bcc9c3bc3bc71ba inventory_gate.sh
|
||||
736ea4be712e1b5bcda10ecb466f0dec7008a2a36eabdfd77563976299c43cce lean-guard
|
||||
34c102ca7a38719ef8890b8dedd3f07c3f92e10ae4a1ce2c47a7db4d53523354 Proofs/Audit.lean
|
||||
79611f9689ba714fb8d3f57aee86ad9655509303445beaad766bf8b049de8c44 selftest-axgate.sh
|
||||
4f23dc814f02d3d0355327fd01ab9092dae8b87c76cc296058693428f75e9c7e Proofs/InventoryBasic.lean
|
||||
84bc670991fd7456d8c8569ff7b7c32410513a63d3cb7fe8877bb19a82d36a7d Proofs/InventoryCore.lean
|
||||
4b1d7f5249a80375b4ef849a760ae8e4bbcecf103c3f8b9e8d0a5d5a9ae377fc Proofs/Inventory.lean
|
||||
62da1de45d2e19abe588a75e32454548e8caa3067ff51a97d7ae155886dbe0c0 selftest-axgate.sh
|
||||
3d5898161d663eccad162269a5a6c102319077e22e1f2d89a8bfcab6926d29f6 selftest-harness.sh
|
||||
a14acaafe914aabb9df44fcd05fd50d475804280fab9e332fdb5ea0b0d35f164 selftest-statements.sh
|
||||
1df031a075fc438c5229d01cbc44ee6ac489a272cd736624f7ca45ef1a4ddb7f selftest-inventory.sh
|
||||
bf88da29034ae32fe6aef8677c8952adcc2b6903e7b2972ea70971f276be0f45 selftest-statements.sh
|
||||
|
|
|
|||
70
verification/Proofs/Inventory.lean
Normal file
70
verification/Proofs/Inventory.lean
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/- ──────────────────────────────────────────────────────────────────────────
|
||||
Proofs/Inventory.lean — environment-derived declaration inventory (main chain).
|
||||
|
||||
Audit INFRASTRUCTURE, not corpus: excluded from check.sh's compile manifest
|
||||
and from its own inventory (its constants live in modules the corpus list
|
||||
below does not name). It proves nothing and is imported by nothing.
|
||||
|
||||
Covers every module check.sh compiles except any listed as needing a
|
||||
separate driver (see Proofs/InventoryBasic.lean if present). Whether
|
||||
a split is needed was determined by compiling a probe, per repo.
|
||||
────────────────────────────────────────────────────────────────────────── -/
|
||||
import Proofs.InventoryCore
|
||||
import Proofs.Denote
|
||||
import Proofs.P25519
|
||||
import Proofs.ReduceSpec
|
||||
import Proofs.SubNegSpec
|
||||
import Proofs.ConstSpecs
|
||||
import Proofs.AddSpec
|
||||
import Proofs.MulSpec
|
||||
import Proofs.SquareSpec
|
||||
import Proofs.Square2Spec
|
||||
import Proofs.Field
|
||||
import Proofs.InvertSpec
|
||||
import Proofs.FieldMain
|
||||
import Proofs.FeQ
|
||||
import Proofs.EdCurve
|
||||
import Proofs.EdDenote
|
||||
import Proofs.EdDouble
|
||||
import Proofs.EdAddProjNiels
|
||||
import Proofs.EdAddAffNiels
|
||||
import Proofs.EdConvert
|
||||
import Proofs.EdMain
|
||||
import Proofs.DsmTableSpec
|
||||
import Proofs.DsmStepSpec
|
||||
import Proofs.DsmLoopSpec
|
||||
import Proofs.DsmNafLoadSpec
|
||||
import Proofs.DsmNafMath
|
||||
import Proofs.DsmNafLoopSpec
|
||||
import Proofs.DsmNafSpec
|
||||
import Proofs.DsmMulSpec
|
||||
import Proofs.ToBytesMath
|
||||
import Proofs.ToBytesSpec
|
||||
import Proofs.ScalarPackSpec
|
||||
import Proofs.CompressSpec
|
||||
import Proofs.SigApexSpec
|
||||
import Proofs.PointLiftSpec
|
||||
import Proofs.PointEqSpec
|
||||
import Proofs.DecompressSpec
|
||||
import Proofs.FromBytesSpec
|
||||
import Proofs.DecompressMain
|
||||
open Lean Ed25519Inventory
|
||||
|
||||
/-- Exactly the modules this driver covers. check.sh verifies, in BOTH
|
||||
directions, that the union of the two drivers' lists is its PROOFS
|
||||
manifest minus the audit infrastructure. -/
|
||||
def corpus : Array Name :=
|
||||
#[`Proofs.Denote, `Proofs.P25519, `Proofs.ReduceSpec, `Proofs.SubNegSpec,
|
||||
`Proofs.ConstSpecs, `Proofs.AddSpec, `Proofs.MulSpec,
|
||||
`Proofs.SquareSpec, `Proofs.Square2Spec, `Proofs.Field,
|
||||
`Proofs.InvertSpec, `Proofs.FieldMain, `Proofs.FeQ, `Proofs.EdCurve,
|
||||
`Proofs.EdDenote, `Proofs.EdDouble, `Proofs.EdAddProjNiels,
|
||||
`Proofs.EdAddAffNiels, `Proofs.EdConvert, `Proofs.EdMain,
|
||||
`Proofs.DsmTableSpec, `Proofs.DsmStepSpec, `Proofs.DsmLoopSpec,
|
||||
`Proofs.DsmNafLoadSpec, `Proofs.DsmNafMath, `Proofs.DsmNafLoopSpec,
|
||||
`Proofs.DsmNafSpec, `Proofs.DsmMulSpec, `Proofs.ToBytesMath,
|
||||
`Proofs.ToBytesSpec, `Proofs.ScalarPackSpec, `Proofs.CompressSpec,
|
||||
`Proofs.SigApexSpec, `Proofs.PointLiftSpec, `Proofs.PointEqSpec,
|
||||
`Proofs.DecompressSpec, `Proofs.FromBytesSpec, `Proofs.DecompressMain]
|
||||
|
||||
#eval show MetaM Unit from emitInventory corpus
|
||||
24
verification/Proofs/InventoryBasic.lean
Normal file
24
verification/Proofs/InventoryBasic.lean
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/- ──────────────────────────────────────────────────────────────────────────
|
||||
Proofs/InventoryBasic.lean — environment-derived declaration inventory (Proofs.Basic only).
|
||||
|
||||
Audit INFRASTRUCTURE, not corpus: excluded from check.sh's compile manifest
|
||||
and from its own inventory (its constants live in modules the corpus list
|
||||
below does not name). It proves nothing and is imported by nothing.
|
||||
|
||||
Proofs.Basic is compiled by check.sh but imported by no other module,
|
||||
and deliberately reuses `CurveFieldProofs.zero_spec`, which
|
||||
Proofs.ConstSpecs also declares. Importing both at once is an
|
||||
elaboration error, so Basic is inventoried separately and check.sh
|
||||
concatenates the two outputs before gating.
|
||||
────────────────────────────────────────────────────────────────────────── -/
|
||||
import Proofs.InventoryCore
|
||||
import Proofs.Basic
|
||||
open Lean Ed25519Inventory
|
||||
|
||||
/-- Exactly the modules this driver covers. check.sh verifies, in BOTH
|
||||
directions, that the union of the two drivers' lists is its PROOFS
|
||||
manifest minus the audit infrastructure. -/
|
||||
def corpus : Array Name :=
|
||||
#[`Proofs.Basic]
|
||||
|
||||
#eval show MetaM Unit from emitInventory corpus
|
||||
110
verification/Proofs/InventoryCore.lean
Normal file
110
verification/Proofs/InventoryCore.lean
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/- ──────────────────────────────────────────────────────────────────────────
|
||||
Proofs/InventoryCore.lean — shared machinery for the declaration inventory.
|
||||
|
||||
PORTED, NOT REINVENTED. This is the ltl-accumulator-verified design
|
||||
(Proofs/Inventory.lean there), which survived a nine-attack self-test that
|
||||
defeated a source-regex enumerator: attributed, private, indented and
|
||||
`instance` declarations were all invisible to the regex, and a nested
|
||||
`namespace Hidden theorem MTH` collided with the basename of an audited
|
||||
declaration. Reading the compiled ENVIRONMENT sees exactly what the kernel
|
||||
saw, and there is no name shape that can hide from it.
|
||||
|
||||
WHY TWO DRIVERS IMPORT THIS. Unlike the accumulator, this corpus cannot be
|
||||
imported as one environment: `Proofs.Basic` and `Proofs.ConstSpecs` both
|
||||
declare `CurveFieldProofs.zero_spec`. That is deliberate and documented —
|
||||
Basic.lean is compiled by check.sh but imported by nothing, so the reuse is
|
||||
harmless — but it makes a single whole-corpus import impossible. The corpus
|
||||
therefore splits into the main chain and Basic, one driver each, and
|
||||
check.sh concatenates their output before gating. The split is asserted in
|
||||
check.sh against the compile manifest, so a module cannot fall between the
|
||||
two drivers unnoticed.
|
||||
|
||||
The corpus module list lives in each DRIVER, not here, and is checked
|
||||
textually against check.sh's manifest in both directions. A listed module
|
||||
that is not actually imported is an elaboration error, not a silent skip.
|
||||
────────────────────────────────────────────────────────────────────────── -/
|
||||
import Lean
|
||||
|
||||
open Lean
|
||||
|
||||
namespace Ed25519Inventory
|
||||
|
||||
def kindOf : ConstantInfo → String
|
||||
| .axiomInfo _ => "axiom"
|
||||
| .defnInfo _ => "def"
|
||||
| .thmInfo _ => "theorem"
|
||||
| .opaqueInfo _ => "opaque"
|
||||
| .quotInfo _ => "quot"
|
||||
| .inductInfo _ => "inductive"
|
||||
| .ctorInfo _ => "ctor"
|
||||
| .recInfo _ => "recursor"
|
||||
|
||||
/-- Axiom cone of `n`, from the kernel's own collector — the same machinery
|
||||
`#print axioms` uses.
|
||||
|
||||
NO INDEPENDENT SECOND WALKER HERE, and that is a deliberate REDUCTION in
|
||||
strength against the ltl-accumulator design this is ported from. There, a
|
||||
hand-written closure walker runs alongside `collectAxioms` and every
|
||||
constant must get the same answer from both, so the two implementations
|
||||
check each other. Porting that walker to this corpus was tried on
|
||||
2026-07-29 and abandoned on evidence:
|
||||
|
||||
· without traversing inductive families it UNDER-approximated —
|
||||
`CurveFieldProofs.EdPoint`: walker [] vs kernel [Classical.choice,
|
||||
Quot.sound, propext];
|
||||
· adding constructors, recursor rules and `all` groups made it
|
||||
OVER-approximate — `CurveFieldProofs.ProjPoint`: walker
|
||||
[Classical.choice, Quot.sound, propext] vs kernel [].
|
||||
|
||||
Disagreeing in BOTH directions means the second implementation is not an
|
||||
independent check, it is a second wrong answer. Matching the kernel's
|
||||
traversal exactly over mathlib's inductive shapes is a Lean-internals
|
||||
project, not a gate, and shipping a walker that is wrong in two directions
|
||||
would be worse than shipping none: it would fail builds for reasons that
|
||||
are the checker's fault and teach everyone to ignore it.
|
||||
|
||||
CONSEQUENCE, stated so nobody assumes otherwise: on this corpus the cone
|
||||
figures rest on `collectAxioms` alone. The accumulator's corpus is
|
||||
mathlib-free, its walker agrees there, and it KEEPS the cross-check. This
|
||||
is recorded in TRUSTED-BASE.md. -/
|
||||
def axiomCone (n : Name) : MetaM (Array Name) := do
|
||||
let cone ← collectAxioms n
|
||||
return cone.qsort (fun a b => a.toString < b.toString)
|
||||
|
||||
/-- Emit `INV|name|kind|cone` for every constant originating in `corpus`.
|
||||
EVERY constant is emitted — fully qualified, NO filtering. Compiler-
|
||||
generated auxiliaries (equation lemmas, match/eq/induct helpers, private
|
||||
manglings) are emitted too and pinned in the allowlist, so anything new,
|
||||
renamed, removed, or with a changed cone shows up as a diff. -/
|
||||
def emitInventory (corpus : Array Name) : MetaM Unit := do
|
||||
let env ← getEnv
|
||||
let mut idxs : Array Nat := #[]
|
||||
for m in corpus do
|
||||
match env.getModuleIdx? m with
|
||||
| some i => idxs := idxs.push i
|
||||
| none => throwError "INVENTORY ERROR: corpus module {m} is not imported"
|
||||
let mut lines : Array String := #[]
|
||||
for (n, ci) in env.constants.toList do
|
||||
if let some i := env.getModuleIdxFor? n then
|
||||
if idxs.contains i then
|
||||
let cone ← axiomCone n
|
||||
let coneStr := ",".intercalate (cone.toList.map (·.toString))
|
||||
-- The ORIGINATING MODULE is part of the record, unlike the accumulator's
|
||||
-- format. It has to be: this corpus contains two distinct declarations
|
||||
-- both named `CurveFieldProofs.zero_spec` (Proofs.Basic and
|
||||
-- Proofs.ConstSpecs), inventoried by different drivers. Keyed on name
|
||||
-- alone their records were byte-identical, so the merged allowlist held
|
||||
-- 3021 entries for 3022 declarations and one real declaration was
|
||||
-- covered by an entry describing a different one. The count trailer
|
||||
-- caught it; the module field is what fixes it.
|
||||
let mdl := env.header.moduleNames[i]!
|
||||
lines := lines.push s!"INV|{mdl}|{n}|{kindOf ci}|{coneStr}"
|
||||
let sorted := lines.qsort (· < ·)
|
||||
for l in sorted do
|
||||
IO.println l
|
||||
-- Output-integrity trailer: a truncated or crashed run must never pass as an
|
||||
-- empty diff. inventory_gate.sh compares this against the lines it actually
|
||||
-- received, in both directions.
|
||||
IO.println s!"INV-COUNT|{sorted.size}"
|
||||
|
||||
end Ed25519Inventory
|
||||
|
|
@ -204,7 +204,11 @@ echo " $(wc -l < "$HERE/GEN-MODEL.sha256") extracted-model files match their pi
|
|||
HARNESS_EXTRA=(
|
||||
AUDIT-MANIFEST.txt # the statement block Phase 3c's digest is taken over
|
||||
GEN-MODEL.sha256 # the extracted-model pins Phase 0b enforces
|
||||
inventory-allowlist.txt # the audit surface Phase 2c diffs against
|
||||
Proofs/Audit.lean # the audit driver: it computes the digest it is judged by
|
||||
Proofs/InventoryCore.lean # inventory machinery
|
||||
Proofs/Inventory.lean # inventory driver: main chain
|
||||
Proofs/InventoryBasic.lean # inventory driver: Proofs.Basic
|
||||
)
|
||||
echo "=== Phase 0c: harness integrity ==="
|
||||
if [ ! -s "$HERE/HARNESS.sha256" ]; then
|
||||
|
|
@ -266,6 +270,11 @@ lake env bash -c "
|
|||
for f in Proofs/*.lean; do
|
||||
b=\$(basename \"\$f\" .lean)
|
||||
[ \"\$b\" = AxiomCheck ] && continue
|
||||
# Inventory drivers are compiled by Phase 2c, not here: they must elaborate
|
||||
# with the corpus already in the environment, and the two of them cannot be
|
||||
# imported together. They are NOT unchecked — Phase 2b reads their compiled
|
||||
# .olean like every other module, and Phase 0c pins their sources.
|
||||
case \"\$b\" in Inventory|InventoryBasic|InventoryCore) continue;; esac
|
||||
case \"\$b\" in Scalar*) continue;; esac # scalar layer: checked by check-scalar.sh (coherence pass 2)
|
||||
case \" ${PROOFS[*]} \" in (*\" \$b \"*) ;; (*) echo \"DEAD FILE: \$f not in check manifest\"; exit 1;; esac
|
||||
done
|
||||
|
|
@ -351,6 +360,85 @@ if [ "$GATE_RC" -ne 0 ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# ── Phase 2c: environment-derived declaration inventory ─────────────────────
|
||||
# WHAT THIS ADDS over Phase 2b. Phase 2b asks the kernel whether any AXIOM is
|
||||
# declared under Proofs/. It says nothing about the ~3000 other declarations:
|
||||
# a `def` or `theorem` whose cone quietly acquired an oracle, a declaration
|
||||
# renamed, added or removed, or a compiler-generated auxiliary that changed
|
||||
# shape, all pass 2b unremarked.
|
||||
#
|
||||
# This phase pins the whole surface. Every constant originating in an audited
|
||||
# module contributes NAME, MODULE, KIND and full AXIOM CONE, and the observed
|
||||
# set must equal inventory-allowlist.txt EXACTLY, both directions:
|
||||
# UNCLASSIFIED (in the environment, not allowlisted) and STALE (allowlisted,
|
||||
# not in the environment) are both build failures.
|
||||
#
|
||||
# PORTED from ltl-accumulator-verified, where a nine-attack self-test proved a
|
||||
# source-regex enumerator evadable by attributed, private, indented and
|
||||
# `instance` declarations and by a nested-namespace basename collision.
|
||||
#
|
||||
# TWO DRIVERS, because this corpus cannot be imported as one environment:
|
||||
# Proofs.Basic and Proofs.ConstSpecs both declare CurveFieldProofs.zero_spec.
|
||||
# The records carry their originating module precisely so those two remain
|
||||
# distinct entries — keyed on name alone they were byte-identical, and the
|
||||
# merged allowlist covered 3022 declarations with 3021 entries.
|
||||
echo "=== Phase 2c: environment-derived declaration inventory ==="
|
||||
INVFAIL=0
|
||||
INVLOG=$(mktemp /tmp/check-inv-XXXX.log)
|
||||
cd "$AENEAS_LEAN"
|
||||
# The DRIVERS are discovered, not listed: whether this corpus needs one or two
|
||||
# is a per-repo fact (dalek and anza cannot import Proofs.Basic together with
|
||||
# Proofs.ConstSpecs; risc0 and betrusted have no Proofs.Basic at all). A
|
||||
# hardcoded pair would silently look for a file that does not exist here.
|
||||
DRIVERS=$(ls "$HERE"/Proofs/Inventory*.lean 2>/dev/null | xargs -r -n1 basename \
|
||||
| sed 's/\.lean$//' | grep -v '^InventoryCore$' | sort)
|
||||
if [ -z "$DRIVERS" ]; then
|
||||
echo " NO INVENTORY DRIVER FOUND — the audit surface would go unchecked."; exit 1
|
||||
fi
|
||||
N_DRIVERS=$(printf '%s\n' "$DRIVERS" | grep -c .)
|
||||
for drv in $DRIVERS; do
|
||||
lake env bash -c "
|
||||
set -uo pipefail
|
||||
cd '$HERE/gen' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD:$HERE\"
|
||||
cd '$HERE'
|
||||
LEAN_TIMEOUT=$TIMEOUT LEAN_MEM_MB=8192 '$HERE/lean-guard' Proofs/$drv.lean
|
||||
" >> "$INVLOG" 2>&1 || { cat "$INVLOG"; echo "INVENTORY COMPILE FAILED ($drv)"; rm -f "$INVLOG"; exit 1; }
|
||||
done
|
||||
# Reconcile the two trailers into one. Summing them and comparing against the
|
||||
# lines actually collected preserves the integrity property in the presence of
|
||||
# the split: truncation in EITHER driver shows up as a mismatch.
|
||||
N_TRAILERS=$(grep -c '^INV-COUNT|' "$INVLOG")
|
||||
if [ "$N_TRAILERS" -ne "$N_DRIVERS" ]; then
|
||||
echo " INVENTORY INCOMPLETE: expected a count trailer from each of the $N_DRIVERS driver(s), saw $N_TRAILERS"
|
||||
INVFAIL=1
|
||||
fi
|
||||
SUM=$(grep '^INV-COUNT|' "$INVLOG" | cut -d'|' -f2 | paste -sd+ - | bc)
|
||||
OBS=$(mktemp /tmp/check-inv-obs-XXXX.log)
|
||||
grep '^INV|' "$INVLOG" > "$OBS"
|
||||
echo "INV-COUNT|${SUM:-0}" >> "$OBS"
|
||||
"$HERE/inventory_gate.sh" "$OBS" "$HERE/inventory-allowlist.txt" || INVFAIL=1
|
||||
rm -f "$INVLOG" "$OBS"
|
||||
|
||||
# The drivers' corpus lists must together BE the compile manifest, minus the
|
||||
# audit infrastructure and the scalar layer. Checked in both directions so a
|
||||
# module cannot fall between the two drivers, and NO SILENT TRUNCATION: what
|
||||
# this phase does not cover is named on stdout every run.
|
||||
COVERED=$(for d in $DRIVERS; do grep -ohE '`Proofs\.[A-Za-z0-9]+' "$HERE/Proofs/$d.lean"; done \
|
||||
| sed 's/`Proofs\.//' | sort -u)
|
||||
for m in "${PROOFS[@]}"; do
|
||||
case "$m" in Audit|Inventory|InventoryBasic|InventoryCore) continue;; esac
|
||||
grep -qx "$m" <<<"$COVERED" || { echo " UNINVENTORIED: $m is compiled by this script but no driver covers it"; INVFAIL=1; }
|
||||
done
|
||||
while read -r m; do
|
||||
[ -z "$m" ] && continue
|
||||
case " ${PROOFS[*]} " in (*" $m "*) ;; (*) echo " PHANTOM: driver claims $m, which this script does not compile"; INVFAIL=1;; esac
|
||||
done <<<"$COVERED"
|
||||
for f in "$HERE"/Proofs/*.lean; do
|
||||
b=$(basename "$f" .lean)
|
||||
case "$b" in Audit|Inventory|InventoryBasic|InventoryCore) continue;; esac
|
||||
grep -qx "$b" <<<"$COVERED" || echo " NOT INVENTORIED HERE (separate button): Proofs/$b.lean"
|
||||
done
|
||||
[ "$INVFAIL" = 0 ] || { echo "INVENTORY COVERAGE FAILED"; exit 1; }
|
||||
# ── Phase 3: axiom audit of every certificate ───────────────────────────────
|
||||
echo "=== Phase 3: axiom audit ==="
|
||||
EXPECTED="[propext, Classical.choice, Quot.sound]"
|
||||
|
|
|
|||
3032
verification/inventory-allowlist.txt
Normal file
3032
verification/inventory-allowlist.txt
Normal file
File diff suppressed because it is too large
Load diff
64
verification/inventory_gate.sh
Executable file
64
verification/inventory_gate.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# inventory_gate.sh — diff an observed environment inventory against the
|
||||
# pinned allowlist. PORTED VERBATIM from ltl-accumulator-verified apart from
|
||||
# the axiom-surface assertion, which is repo-specific: there the corpus admits
|
||||
# exactly one sanctioned axiom, here it admits none.
|
||||
#
|
||||
# This is THE production coverage gate: check.sh Phase 2c calls it, and the
|
||||
# self-test exercises this exact script — the tested logic IS the shipping
|
||||
# logic.
|
||||
#
|
||||
# Usage: inventory_gate.sh <observed-lean-output> <allowlist-file>
|
||||
#
|
||||
# Fail-closed in BOTH directions:
|
||||
# UNCLASSIFIED — constant in the environment, absent from the allowlist
|
||||
# (new/renamed decl, changed kind, or changed axiom cone)
|
||||
# STALE — allowlist entry absent from the environment
|
||||
# plus an output-integrity check: the INV-COUNT trailer emitted by
|
||||
# Proofs/Inventory.lean must equal the number of INV lines actually seen,
|
||||
# so a truncated or crashed run can never pass as an empty diff.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
set -uo pipefail
|
||||
export LC_ALL=C # byte-order collation: sort/comm must agree with Lean's String order
|
||||
obs_file="$1"; allow_file="$2"
|
||||
|
||||
OBS=$(grep '^INV|' "$obs_file" | sort -u)
|
||||
N_OBS=$(printf '%s' "$OBS" | grep -c '^INV|' || true)
|
||||
TRAILER=$(grep '^INV-COUNT|' "$obs_file" | tail -1 | cut -d'|' -f2)
|
||||
if [ -z "$TRAILER" ] || [ "$TRAILER" != "$N_OBS" ]; then
|
||||
echo " INVENTORY TRUNCATED: trailer=${TRAILER:-absent}, observed $N_OBS lines"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ALLOW=$(grep '^INV|' "$allow_file" | sort -u)
|
||||
FAILGATE=0
|
||||
UNCLASS=$(comm -23 <(printf '%s\n' "$OBS") <(printf '%s\n' "$ALLOW"))
|
||||
STALE=$(comm -13 <(printf '%s\n' "$OBS") <(printf '%s\n' "$ALLOW"))
|
||||
if [ -n "$UNCLASS" ]; then
|
||||
printf '%s\n' "$UNCLASS" | sed 's/^/ UNCLASSIFIED (in environment, not allowlisted): /'
|
||||
FAILGATE=1
|
||||
fi
|
||||
if [ -n "$STALE" ]; then
|
||||
printf '%s\n' "$STALE" | sed 's/^/ STALE (allowlisted, not in environment): /'
|
||||
FAILGATE=1
|
||||
fi
|
||||
|
||||
# The audited corpus admits NO axiom declarations at all: the sanctioned
|
||||
# external models live in gen/, outside every module these drivers cover, and
|
||||
# are byte-pinned by Phase 0b. An axiom appearing here would be a declaration
|
||||
# smuggled into the proof corpus, which Phase 2b also catches kernel-side —
|
||||
# two independent gates on the same property, deliberately.
|
||||
AXLINES=$(printf '%s\n' "$OBS" | grep '|axiom|' || true)
|
||||
if [ -n "$AXLINES" ]; then
|
||||
echo " AXIOM SURFACE DRIFT: the audited corpus must declare no axioms; observed:"
|
||||
printf '%s\n' "$AXLINES" | sed 's/^/ /'
|
||||
FAILGATE=1
|
||||
fi
|
||||
|
||||
# The message must describe what was actually checked. It said "single
|
||||
# sanctioned axiom" when ported, which is the accumulator's policy; here the
|
||||
# audited corpus permits NONE, and a success line describing a different rule
|
||||
# is how an assertion quietly stops meaning anything.
|
||||
[ "$FAILGATE" = 0 ] && echo " inventory gate: $N_OBS constants, environment == allowlist, zero axioms declared in the audited corpus"
|
||||
exit "$FAILGATE"
|
||||
|
|
@ -43,13 +43,24 @@ DRIVER="$STASH/phase2b.sh"
|
|||
echo "HERE=\"$HERE\""
|
||||
echo 'AENEAS_LEAN="$AENEAS_HOME/backends/lean"'
|
||||
echo "TIMEOUT=$TIMEOUT; CORES=\"$CORES\""
|
||||
sed -n '/^# ── Phase 2b/,/^# ── Phase 3/p' "$HERE/check.sh" | sed '$d'
|
||||
# Stop at the NEXT phase marker, whatever it is called. A hardcoded
|
||||
# terminator ("...to Phase 3") silently widens the moment a phase is
|
||||
# inserted between the two: adding Phase 2c made this driver swallow 2c as
|
||||
# well and die on variables that phase expects check.sh to have defined,
|
||||
# which surfaced as the BASELINE failing — a self-test blaming a gate for
|
||||
# its own extraction bug.
|
||||
awk '/^# ── Phase 2b/{f=1} f&&/^# ── Phase /&&!/Phase 2b/{exit} f{print}' "$HERE/check.sh"
|
||||
} > "$DRIVER"
|
||||
if [ "$(wc -l < "$DRIVER")" -lt 40 ]; then
|
||||
echo "FATAL: could not lift Phase 2b out of check.sh — the phase markers moved."
|
||||
echo "This self-test must attack the shipping gate; refusing to run against nothing."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(grep -c '^# ── Phase ' "$DRIVER")" -ne 1 ]; then
|
||||
echo "FATAL: the lifted block spans more than one phase; the extraction is wrong."
|
||||
grep '^# ── Phase ' "$DRIVER" | sed 's/^/ /'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
expect() { # expect <name> <expected-rc> <required-substring>
|
||||
local name="$1" want_rc="$2" want_txt="$3"
|
||||
|
|
|
|||
108
verification/selftest-inventory.sh
Executable file
108
verification/selftest-inventory.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# selftest-inventory.sh — adversarial self-test for check.sh Phase 2c.
|
||||
#
|
||||
# WHAT PHASE 2c IS FOR. Phase 2b asks the kernel whether any AXIOM is declared
|
||||
# under Proofs/. It says nothing about the ~3000 other declarations. Phase 3
|
||||
# pins the cones of the 31 named certificates. Between them sits everything
|
||||
# else: a helper lemma that quietly acquired an oracle in its cone, a
|
||||
# declaration added, removed or renamed, a compiler-generated auxiliary that
|
||||
# changed shape. Phase 2c pins that whole surface and diffs it both ways.
|
||||
#
|
||||
# Cases, each asserting a SPECIFIC diagnostic:
|
||||
# 0 positive control: the untouched tree passes
|
||||
# 1 an allowlist row deleted -> UNCLASSIFIED (in env, not allowlisted)
|
||||
# 2 an allowlist row invented -> STALE (allowlisted, not in env)
|
||||
# 3 a cone silently widened -> BOTH, because the record changed
|
||||
# 4 an axiom row appears -> AXIOM SURFACE DRIFT
|
||||
# 5 the count trailer disagrees -> INVENTORY TRUNCATED (no vacuous pass)
|
||||
#
|
||||
# It runs the SHIPPING inventory_gate.sh against a recorded observation, so no
|
||||
# Lean is needed and the whole thing takes a second. The observation itself is
|
||||
# produced by check.sh Phase 2c; this test attacks the gate that judges it.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
set -uo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
FAILURES=0
|
||||
STASH="$(mktemp -d)"
|
||||
trap 'rm -rf "$STASH"' EXIT INT TERM
|
||||
|
||||
ALLOW="$HERE/inventory-allowlist.txt"
|
||||
[ -s "$ALLOW" ] || { echo "FATAL: inventory-allowlist.txt missing or empty"; exit 1; }
|
||||
|
||||
# The observation a green run would produce: the allowlist itself plus a
|
||||
# trailer. Deriving it from the allowlist is exactly right for this test — the
|
||||
# question is whether the GATE reacts correctly to differences, and each case
|
||||
# below introduces one.
|
||||
mkobs() { # mkobs <file> [extra-line...]
|
||||
local out="$1"; shift
|
||||
grep '^INV|' "$ALLOW" > "$out"
|
||||
for l in "$@"; do printf '%s\n' "$l" >> "$out"; done
|
||||
LC_ALL=C sort -o "$out" "$out"
|
||||
echo "INV-COUNT|$(grep -c '^INV|' "$out")" >> "$out"
|
||||
}
|
||||
|
||||
expect() { # expect <label> <obs> <allow> <want-rc> <want-substring>
|
||||
local label="$1" obs="$2" allow="$3" want_rc="$4" want_txt="$5" out rc
|
||||
out=$("$HERE/inventory_gate.sh" "$obs" "$allow" 2>&1); rc=$?
|
||||
if [ "$rc" -ne "$want_rc" ]; then
|
||||
echo " ✗ $label: exit $rc, expected $want_rc"; echo "$out" | sed 's/^/ /'
|
||||
FAILURES=$((FAILURES+1)); return
|
||||
fi
|
||||
if ! grep -qF "$want_txt" <<<"$out"; then
|
||||
echo " ✗ $label: exit code right, diagnostic wrong (rejected for the wrong reason)"
|
||||
echo " wanted: $want_txt"; echo "$out" | sed 's/^/ /'
|
||||
FAILURES=$((FAILURES+1)); return
|
||||
fi
|
||||
echo " ✓ $label"
|
||||
}
|
||||
|
||||
echo "=== selftest-inventory: attacking check.sh Phase 2c's gate ==="
|
||||
|
||||
# ── 0. positive control ────────────────────────────────────────────────────
|
||||
mkobs "$STASH/obs.txt"
|
||||
expect "case 0 control: a faithful observation passes" "$STASH/obs.txt" "$ALLOW" 0 "environment == allowlist"
|
||||
|
||||
# ── 1. a row deleted from the allowlist: the declaration is still there, so
|
||||
# the gate must report it as unclassified rather than shrug.
|
||||
VICTIM=$(grep '^INV|' "$ALLOW" | grep '|theorem|' | head -1)
|
||||
grep -vxF "$VICTIM" "$ALLOW" > "$STASH/allow-short.txt"
|
||||
expect "case 1: allowlist row deleted -> UNCLASSIFIED" "$STASH/obs.txt" "$STASH/allow-short.txt" 1 "UNCLASSIFIED"
|
||||
|
||||
# ── 2. a row invented in the allowlist: nothing in the environment matches it.
|
||||
cp "$ALLOW" "$STASH/allow-extra.txt"
|
||||
echo "INV|Proofs.Ghost|CurveFieldProofs.ghost_lemma|theorem|Classical.choice" >> "$STASH/allow-extra.txt"
|
||||
expect "case 2: allowlist row with no declaration -> STALE" "$STASH/obs.txt" "$STASH/allow-extra.txt" 1 "STALE"
|
||||
|
||||
# ── 3. THE ONE THAT MATTERS: a cone silently widened. Same module, same name,
|
||||
# same kind — only the axiom cone grew. Phases 2b and 3 both pass this:
|
||||
# 2b only looks for axiom DECLARATIONS, and 3 only pins the 31 named
|
||||
# certificates. If the victim is not one of those, nothing else sees it.
|
||||
WIDENED=$(sed 's/$/,sha2.Sha512/' <<<"$VICTIM")
|
||||
mkobs "$STASH/obs-wide.txt"
|
||||
grep -vxF "$VICTIM" "$STASH/obs-wide.txt" > "$STASH/t" && mv "$STASH/t" "$STASH/obs-wide.txt"
|
||||
printf '%s\n' "$WIDENED" >> "$STASH/obs-wide.txt"
|
||||
LC_ALL=C sort -o "$STASH/obs-wide.txt" "$STASH/obs-wide.txt"
|
||||
echo "INV-COUNT|$(grep -c '^INV|' "$STASH/obs-wide.txt")" >> "$STASH/obs-wide.txt"
|
||||
expect "case 3: a cone widened by one oracle -> UNCLASSIFIED" "$STASH/obs-wide.txt" "$ALLOW" 1 "UNCLASSIFIED"
|
||||
|
||||
# ── 4. an axiom appears in the audited corpus. The sanctioned external models
|
||||
# live in gen/, outside every module the drivers cover, so any axiom here
|
||||
# is a declaration smuggled into the proof corpus.
|
||||
mkobs "$STASH/obs-ax.txt" "INV|Proofs.FeQ|CurveFieldProofs.rogue|axiom|"
|
||||
expect "case 4: an axiom in the corpus -> AXIOM SURFACE DRIFT" "$STASH/obs-ax.txt" "$ALLOW" 1 "AXIOM SURFACE DRIFT"
|
||||
|
||||
# ── 5. truncation. "Nothing found" and "nothing wrong" must not share a path:
|
||||
# a crashed or cut-short run has to fail, not pass as an empty diff.
|
||||
grep '^INV|' "$ALLOW" | head -100 > "$STASH/obs-trunc.txt"
|
||||
echo "INV-COUNT|$(grep -c '^INV|' "$ALLOW")" >> "$STASH/obs-trunc.txt"
|
||||
expect "case 5: trailer disagrees with the lines -> INVENTORY TRUNCATED" "$STASH/obs-trunc.txt" "$ALLOW" 1 "INVENTORY TRUNCATED"
|
||||
|
||||
echo ""
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
echo "SELFTEST PASSED — Phase 2c's gate rejects surface drift in both directions,"
|
||||
echo "for the stated reason in each case."
|
||||
exit 0
|
||||
fi
|
||||
echo "SELFTEST FAILED: $FAILURES case(s) did not behave as claimed."
|
||||
exit 1
|
||||
|
|
@ -68,7 +68,9 @@ build_driver() {
|
|||
sed -n '/^CERTS=(/,/^)/p' "$HERE/check.sh"
|
||||
# `$0` inside Phase 3c must resolve to the shipping check.sh, not to this
|
||||
# driver, or the apex-name recovery would read the wrong file.
|
||||
sed -n '/^# ── Phase 3c/,/^echo ""$/p' "$HERE/check.sh" | sed '$d' \
|
||||
# Stop at the next phase marker, not at a blank echo: a terminator that is
|
||||
# not itself a phase boundary breaks the moment the phase's body changes.
|
||||
awk '/^# ── Phase 3c/{f=1} f&&/^# ── Phase /&&!/Phase 3c/{exit} f{print}' "$HERE/check.sh" \
|
||||
| sed "s|\"\$0\"|\"$HERE/check.sh\"|g"
|
||||
} > "$DRIVER"
|
||||
if [ "$(wc -l < "$DRIVER")" -lt 60 ]; then
|
||||
|
|
|
|||
Loading…
Reference in a new issue