P2-c: classify and pin the extraction boundary
Aeneas emits a *_Template.lean naming everything the extracted code needs
from outside itself — the extraction's own statement of its boundary.
extract.sh has always said, in prose, "after regenerating, diff the template
against the hand-written file". Prose is not a gate, and the diff cannot be
one: the two files legitimately differ in almost every line, holes and
Aeneas comments against real definitions and modeling policy.
MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two
options — enforce the diff, or pin both files — and the answer turned out to
be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further
things stand here: the generated Funs.lean imports the model and CALLS these
externals, so the Lean compiler enforces their TYPES wherever the extracted
code uses them; and the per-certificate exact cones catch any external that
becomes, or stops being, an assumption anything depends on.
What none of those three sees is the CLASSIFICATION: for each name the
extraction asks for, whether this repository answers with an ASSUMPTION or
with a PROOF. That is the tier-A/B claim the documents make in prose — the
curve calls and the three curve types resolve to proven definitions rather
than axioms, because gen/CurveField/Funs.lean opens `namespace
curve25519_dalek` and so defines the very names Aeneas asks for. Nothing
checked it. A regeneration that renamed one, or a model that quietly
answered one with an axiom instead, would have left the documents claiming a
proof where the repository had an assumption.
Phase 0d recomputes the classification with model-correspondence.py
(namespace-aware, so a definition inside a namespace counts under its full
name) and requires equality with the committed MODEL-CORRESPONDENCE.txt.
UNRESOLVED — the extraction asking for something nothing here provides — is
a hard failure.
dalek 43 MODEL 8 PROVEN 3 EXTRA
anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate)
risc0 36 MODEL 8 PROVEN 4 EXTRA
betrusted 35 MODEL 8 PROVEN 4 EXTRA
selftest-correspondence.sh, five cases, negative-tested by disabling the
comparison. The case that matters is 2: a PROVEN external answered by an
axiom instead. No name changes anywhere, every byte pin still matches, and
it compiles, because the signature is unchanged — before Phase 0d nothing in
the button could tell.
Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows,
which was VACUOUS on anza, since anza has none — it removed nothing, the
table still matched, and the case passed while testing nothing. It now
deletes the first row whatever its verdict AND asserts the file changed.
extract.sh now points at the gate instead of asking a human to look.
Certified by a full sweep: both buttons, all four forks, purged trees,
machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# selftest-correspondence.sh — attacks check.sh Phase 0d.
|
|
|
|
|
#
|
|
|
|
|
# Phase 0d asserts HOW each external the extraction asks for is answered: with
|
|
|
|
|
# an assumption in the hand-written model, or with a proof already in the
|
|
|
|
|
# extracted corpus. The second class is the one the documents make a claim
|
|
|
|
|
# about — the curve calls and curve types are said to resolve to the proven
|
|
|
|
|
# model's own definitions rather than to axioms — and that claim was prose
|
|
|
|
|
# until this phase existed.
|
|
|
|
|
#
|
|
|
|
|
# 0 control: the committed table matches the files
|
|
|
|
|
# 1 the extraction asks for something NOTHING provides -> UNRESOLVED
|
|
|
|
|
# 2 a PROVEN external answered by an axiom in the model instead. This is the
|
|
|
|
|
# attack that matters: a proof silently downgraded to an assumption, in a
|
|
|
|
|
# name whose spelling does not change anywhere else.
|
|
|
|
|
# 3 a row deleted from the committed table -> drift
|
|
|
|
|
# 4 a row's verdict edited in the committed table -> drift
|
|
|
|
|
#
|
|
|
|
|
# No Lean: Phase 0d is pure text over gen/. Seconds, not minutes.
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
FAILURES=0
|
|
|
|
|
STASH="$(mktemp -d)"
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
|
[ -f "$STASH/corr" ] && cp "$STASH/corr" "$HERE/MODEL-CORRESPONDENCE.txt"
|
|
|
|
|
[ -f "$STASH/model" ] && cp "$STASH/model" "$HERE/$MODEL_REL"
|
|
|
|
|
[ -f "$STASH/tmpl" ] && cp "$STASH/tmpl" "$HERE/$TMPL_REL"
|
|
|
|
|
rm -rf "$STASH"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Derive the victims from this repo rather than naming them: the forks do not
|
|
|
|
|
# share a gen/ layout (anza has no CurveSig crate at all, so it has no PROVEN
|
|
|
|
|
# rows and case 2 does not apply there).
|
|
|
|
|
TMPL_REL=$(cd "$HERE" && ls gen/*/FunsExternal_Template.lean | head -1)
|
|
|
|
|
MODEL_REL="${TMPL_REL/_Template/}"
|
|
|
|
|
PROVEN_ROW=$(grep -m1 '|PROVEN$' "$HERE/MODEL-CORRESPONDENCE.txt" || true)
|
|
|
|
|
|
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
|
cp "$HERE/MODEL-CORRESPONDENCE.txt" "$STASH/corr"
|
|
|
|
|
cp "$HERE/$MODEL_REL" "$STASH/model"
|
|
|
|
|
cp "$HERE/$TMPL_REL" "$STASH/tmpl"
|
|
|
|
|
|
|
|
|
|
# Phase 0d lifted from the shipping button.
|
verification: derive lift dependencies instead of hand-keeping them
Round-9 swept all ten instruments per fork instead of only the two buttons,
and found selftest-axgate.sh red in all four. Root cause, identical to the
KERNLOG defect fixed the day before: a self-test lifts one phase out of
check.sh and supplies its variables from a HAND-WRITTEN preamble, and the
phase later grew a dependency nobody told the preamble about.
Phase 2b stopped globbing Proofs/*.olean and started reading $PROOFS by
membership. Bash does not error on an unset array under `set -u` — it
expands to nothing — so the gate silently received
expected := [".olean"]
one entry with an empty name, and rejected the baseline via its own
fail-closed absence check. Exit code right, reason wrong.
· lift-guard.sh (new, pinned): derives every variable the lifted payload
reads, subtracts what the driver defines, and fails AT LIFT TIME naming
the remainder. Wired into all five lifting self-tests; selftest-shapes.sh
drops its inline copy. One implementation, not five, since drifting out
of sync is the entire failure mode.
· selftest-axgate.sh: lifts PROOFS verbatim, and its case 2 now poisons a
MANIFESTED leaf module rather than adding a stray file. The stray-file
attack was a no-op against membership semantics — that is the dead-file
gate's job, proven by selftest-harness case 8 — and it is also the
weaker attack, since adding files to Proofs/ must clear a gate that
editing an already-manifested module does not.
· Phase 2c's accounting identity moves behind its own marker with its own
ACCTFAIL verdict, so the phase is liftable by construction rather than
by a self-test knowing where to stop. Truncating the lift range alone
would have dropped Phase 2c's own verdict and left the test unable to fail.
· The other four lifting self-tests assert on the PAYLOAD rather than the
assembled driver, so a marker in the preamble cannot satisfy a check
meant to prove the lift landed.
Certified: 8/8 self-tests green in all four forks (~212 assertions, none
failing); check.sh bytes unchanged and still matching the pin written before
the 3h08m round-9 sweep, whose 36 GREEN button rows therefore stand.
--audit-only re-run against the new HARNESS.sha256 in every fork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 11:07:28 +00:00
|
|
|
awk '/^# ── Phase 0d/{f=1} f&&/^# ── (Phase 1|Phases end)/{exit} f{print}' \
|
|
|
|
|
"$HERE/check.sh" > "$STASH/payload.sh"
|
P2-c: classify and pin the extraction boundary
Aeneas emits a *_Template.lean naming everything the extracted code needs
from outside itself — the extraction's own statement of its boundary.
extract.sh has always said, in prose, "after regenerating, diff the template
against the hand-written file". Prose is not a gate, and the diff cannot be
one: the two files legitimately differ in almost every line, holes and
Aeneas comments against real definitions and modeling policy.
MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two
options — enforce the diff, or pin both files — and the answer turned out to
be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further
things stand here: the generated Funs.lean imports the model and CALLS these
externals, so the Lean compiler enforces their TYPES wherever the extracted
code uses them; and the per-certificate exact cones catch any external that
becomes, or stops being, an assumption anything depends on.
What none of those three sees is the CLASSIFICATION: for each name the
extraction asks for, whether this repository answers with an ASSUMPTION or
with a PROOF. That is the tier-A/B claim the documents make in prose — the
curve calls and the three curve types resolve to proven definitions rather
than axioms, because gen/CurveField/Funs.lean opens `namespace
curve25519_dalek` and so defines the very names Aeneas asks for. Nothing
checked it. A regeneration that renamed one, or a model that quietly
answered one with an axiom instead, would have left the documents claiming a
proof where the repository had an assumption.
Phase 0d recomputes the classification with model-correspondence.py
(namespace-aware, so a definition inside a namespace counts under its full
name) and requires equality with the committed MODEL-CORRESPONDENCE.txt.
UNRESOLVED — the extraction asking for something nothing here provides — is
a hard failure.
dalek 43 MODEL 8 PROVEN 3 EXTRA
anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate)
risc0 36 MODEL 8 PROVEN 4 EXTRA
betrusted 35 MODEL 8 PROVEN 4 EXTRA
selftest-correspondence.sh, five cases, negative-tested by disabling the
comparison. The case that matters is 2: a PROVEN external answered by an
axiom instead. No name changes anywhere, every byte pin still matches, and
it compiles, because the signature is unchanged — before Phase 0d nothing in
the button could tell.
Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows,
which was VACUOUS on anza, since anza has none — it removed nothing, the
table still matched, and the case passed while testing nothing. It now
deletes the first row whatever its verdict AND asserts the file changed.
extract.sh now points at the gate instead of asking a human to look.
Certified by a full sweep: both buttons, all four forks, purged trees,
machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
|
|
|
{ echo 'set -euo pipefail'
|
|
|
|
|
echo "HERE=\"$HERE\""
|
verification: derive lift dependencies instead of hand-keeping them
Round-9 swept all ten instruments per fork instead of only the two buttons,
and found selftest-axgate.sh red in all four. Root cause, identical to the
KERNLOG defect fixed the day before: a self-test lifts one phase out of
check.sh and supplies its variables from a HAND-WRITTEN preamble, and the
phase later grew a dependency nobody told the preamble about.
Phase 2b stopped globbing Proofs/*.olean and started reading $PROOFS by
membership. Bash does not error on an unset array under `set -u` — it
expands to nothing — so the gate silently received
expected := [".olean"]
one entry with an empty name, and rejected the baseline via its own
fail-closed absence check. Exit code right, reason wrong.
· lift-guard.sh (new, pinned): derives every variable the lifted payload
reads, subtracts what the driver defines, and fails AT LIFT TIME naming
the remainder. Wired into all five lifting self-tests; selftest-shapes.sh
drops its inline copy. One implementation, not five, since drifting out
of sync is the entire failure mode.
· selftest-axgate.sh: lifts PROOFS verbatim, and its case 2 now poisons a
MANIFESTED leaf module rather than adding a stray file. The stray-file
attack was a no-op against membership semantics — that is the dead-file
gate's job, proven by selftest-harness case 8 — and it is also the
weaker attack, since adding files to Proofs/ must clear a gate that
editing an already-manifested module does not.
· Phase 2c's accounting identity moves behind its own marker with its own
ACCTFAIL verdict, so the phase is liftable by construction rather than
by a self-test knowing where to stop. Truncating the lift range alone
would have dropped Phase 2c's own verdict and left the test unable to fail.
· The other four lifting self-tests assert on the PAYLOAD rather than the
assembled driver, so a marker in the preamble cannot satisfy a check
meant to prove the lift landed.
Certified: 8/8 self-tests green in all four forks (~212 assertions, none
failing); check.sh bytes unchanged and still matching the pin written before
the 3h08m round-9 sweep, whose 36 GREEN button rows therefore stand.
--audit-only re-run against the new HARNESS.sha256 in every fork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 11:07:28 +00:00
|
|
|
cat "$STASH/payload.sh"
|
P2-c: classify and pin the extraction boundary
Aeneas emits a *_Template.lean naming everything the extracted code needs
from outside itself — the extraction's own statement of its boundary.
extract.sh has always said, in prose, "after regenerating, diff the template
against the hand-written file". Prose is not a gate, and the diff cannot be
one: the two files legitimately differ in almost every line, holes and
Aeneas comments against real definitions and modeling policy.
MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two
options — enforce the diff, or pin both files — and the answer turned out to
be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further
things stand here: the generated Funs.lean imports the model and CALLS these
externals, so the Lean compiler enforces their TYPES wherever the extracted
code uses them; and the per-certificate exact cones catch any external that
becomes, or stops being, an assumption anything depends on.
What none of those three sees is the CLASSIFICATION: for each name the
extraction asks for, whether this repository answers with an ASSUMPTION or
with a PROOF. That is the tier-A/B claim the documents make in prose — the
curve calls and the three curve types resolve to proven definitions rather
than axioms, because gen/CurveField/Funs.lean opens `namespace
curve25519_dalek` and so defines the very names Aeneas asks for. Nothing
checked it. A regeneration that renamed one, or a model that quietly
answered one with an axiom instead, would have left the documents claiming a
proof where the repository had an assumption.
Phase 0d recomputes the classification with model-correspondence.py
(namespace-aware, so a definition inside a namespace counts under its full
name) and requires equality with the committed MODEL-CORRESPONDENCE.txt.
UNRESOLVED — the extraction asking for something nothing here provides — is
a hard failure.
dalek 43 MODEL 8 PROVEN 3 EXTRA
anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate)
risc0 36 MODEL 8 PROVEN 4 EXTRA
betrusted 35 MODEL 8 PROVEN 4 EXTRA
selftest-correspondence.sh, five cases, negative-tested by disabling the
comparison. The case that matters is 2: a PROVEN external answered by an
axiom instead. No name changes anywhere, every byte pin still matches, and
it compiles, because the signature is unchanged — before Phase 0d nothing in
the button could tell.
Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows,
which was VACUOUS on anza, since anza has none — it removed nothing, the
table still matched, and the case passed while testing nothing. It now
deletes the first row whatever its verdict AND asserts the file changed.
extract.sh now points at the gate instead of asking a human to look.
Certified by a full sweep: both buttons, all four forks, purged trees,
machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
|
|
|
} > "$STASH/p0d.sh"
|
verification: derive lift dependencies instead of hand-keeping them
Round-9 swept all ten instruments per fork instead of only the two buttons,
and found selftest-axgate.sh red in all four. Root cause, identical to the
KERNLOG defect fixed the day before: a self-test lifts one phase out of
check.sh and supplies its variables from a HAND-WRITTEN preamble, and the
phase later grew a dependency nobody told the preamble about.
Phase 2b stopped globbing Proofs/*.olean and started reading $PROOFS by
membership. Bash does not error on an unset array under `set -u` — it
expands to nothing — so the gate silently received
expected := [".olean"]
one entry with an empty name, and rejected the baseline via its own
fail-closed absence check. Exit code right, reason wrong.
· lift-guard.sh (new, pinned): derives every variable the lifted payload
reads, subtracts what the driver defines, and fails AT LIFT TIME naming
the remainder. Wired into all five lifting self-tests; selftest-shapes.sh
drops its inline copy. One implementation, not five, since drifting out
of sync is the entire failure mode.
· selftest-axgate.sh: lifts PROOFS verbatim, and its case 2 now poisons a
MANIFESTED leaf module rather than adding a stray file. The stray-file
attack was a no-op against membership semantics — that is the dead-file
gate's job, proven by selftest-harness case 8 — and it is also the
weaker attack, since adding files to Proofs/ must clear a gate that
editing an already-manifested module does not.
· Phase 2c's accounting identity moves behind its own marker with its own
ACCTFAIL verdict, so the phase is liftable by construction rather than
by a self-test knowing where to stop. Truncating the lift range alone
would have dropped Phase 2c's own verdict and left the test unable to fail.
· The other four lifting self-tests assert on the PAYLOAD rather than the
assembled driver, so a marker in the preamble cannot satisfy a check
meant to prove the lift landed.
Certified: 8/8 self-tests green in all four forks (~212 assertions, none
failing); check.sh bytes unchanged and still matching the pin written before
the 3h08m round-9 sweep, whose 36 GREEN button rows therefore stand.
--audit-only re-run against the new HARNESS.sha256 in every fork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 11:07:28 +00:00
|
|
|
# Assert on the PAYLOAD, not the concatenation: a marker appearing in the
|
|
|
|
|
# preamble would otherwise satisfy a check meant to prove the lift landed.
|
P2-c: classify and pin the extraction boundary
Aeneas emits a *_Template.lean naming everything the extracted code needs
from outside itself — the extraction's own statement of its boundary.
extract.sh has always said, in prose, "after regenerating, diff the template
against the hand-written file". Prose is not a gate, and the diff cannot be
one: the two files legitimately differ in almost every line, holes and
Aeneas comments against real definitions and modeling policy.
MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two
options — enforce the diff, or pin both files — and the answer turned out to
be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further
things stand here: the generated Funs.lean imports the model and CALLS these
externals, so the Lean compiler enforces their TYPES wherever the extracted
code uses them; and the per-certificate exact cones catch any external that
becomes, or stops being, an assumption anything depends on.
What none of those three sees is the CLASSIFICATION: for each name the
extraction asks for, whether this repository answers with an ASSUMPTION or
with a PROOF. That is the tier-A/B claim the documents make in prose — the
curve calls and the three curve types resolve to proven definitions rather
than axioms, because gen/CurveField/Funs.lean opens `namespace
curve25519_dalek` and so defines the very names Aeneas asks for. Nothing
checked it. A regeneration that renamed one, or a model that quietly
answered one with an axiom instead, would have left the documents claiming a
proof where the repository had an assumption.
Phase 0d recomputes the classification with model-correspondence.py
(namespace-aware, so a definition inside a namespace counts under its full
name) and requires equality with the committed MODEL-CORRESPONDENCE.txt.
UNRESOLVED — the extraction asking for something nothing here provides — is
a hard failure.
dalek 43 MODEL 8 PROVEN 3 EXTRA
anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate)
risc0 36 MODEL 8 PROVEN 4 EXTRA
betrusted 35 MODEL 8 PROVEN 4 EXTRA
selftest-correspondence.sh, five cases, negative-tested by disabling the
comparison. The case that matters is 2: a PROVEN external answered by an
axiom instead. No name changes anywhere, every byte pin still matches, and
it compiles, because the signature is unchanged — before Phase 0d nothing in
the button could tell.
Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows,
which was VACUOUS on anza, since anza has none — it removed nothing, the
table still matched, and the case passed while testing nothing. It now
deletes the first row whatever its verdict AND asserts the file changed.
extract.sh now points at the gate instead of asking a human to look.
Certified by a full sweep: both buttons, all four forks, purged trees,
machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
|
|
|
for want in 'Phase 0d' 'MODEL CORRESPONDENCE' 'model-correspondence.py'; do
|
verification: derive lift dependencies instead of hand-keeping them
Round-9 swept all ten instruments per fork instead of only the two buttons,
and found selftest-axgate.sh red in all four. Root cause, identical to the
KERNLOG defect fixed the day before: a self-test lifts one phase out of
check.sh and supplies its variables from a HAND-WRITTEN preamble, and the
phase later grew a dependency nobody told the preamble about.
Phase 2b stopped globbing Proofs/*.olean and started reading $PROOFS by
membership. Bash does not error on an unset array under `set -u` — it
expands to nothing — so the gate silently received
expected := [".olean"]
one entry with an empty name, and rejected the baseline via its own
fail-closed absence check. Exit code right, reason wrong.
· lift-guard.sh (new, pinned): derives every variable the lifted payload
reads, subtracts what the driver defines, and fails AT LIFT TIME naming
the remainder. Wired into all five lifting self-tests; selftest-shapes.sh
drops its inline copy. One implementation, not five, since drifting out
of sync is the entire failure mode.
· selftest-axgate.sh: lifts PROOFS verbatim, and its case 2 now poisons a
MANIFESTED leaf module rather than adding a stray file. The stray-file
attack was a no-op against membership semantics — that is the dead-file
gate's job, proven by selftest-harness case 8 — and it is also the
weaker attack, since adding files to Proofs/ must clear a gate that
editing an already-manifested module does not.
· Phase 2c's accounting identity moves behind its own marker with its own
ACCTFAIL verdict, so the phase is liftable by construction rather than
by a self-test knowing where to stop. Truncating the lift range alone
would have dropped Phase 2c's own verdict and left the test unable to fail.
· The other four lifting self-tests assert on the PAYLOAD rather than the
assembled driver, so a marker in the preamble cannot satisfy a check
meant to prove the lift landed.
Certified: 8/8 self-tests green in all four forks (~212 assertions, none
failing); check.sh bytes unchanged and still matching the pin written before
the 3h08m round-9 sweep, whose 36 GREEN button rows therefore stand.
--audit-only re-run against the new HARNESS.sha256 in every fork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 11:07:28 +00:00
|
|
|
grep -qF "$want" "$STASH/payload.sh" || {
|
P2-c: classify and pin the extraction boundary
Aeneas emits a *_Template.lean naming everything the extracted code needs
from outside itself — the extraction's own statement of its boundary.
extract.sh has always said, in prose, "after regenerating, diff the template
against the hand-written file". Prose is not a gate, and the diff cannot be
one: the two files legitimately differ in almost every line, holes and
Aeneas comments against real definitions and modeling policy.
MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two
options — enforce the diff, or pin both files — and the answer turned out to
be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further
things stand here: the generated Funs.lean imports the model and CALLS these
externals, so the Lean compiler enforces their TYPES wherever the extracted
code uses them; and the per-certificate exact cones catch any external that
becomes, or stops being, an assumption anything depends on.
What none of those three sees is the CLASSIFICATION: for each name the
extraction asks for, whether this repository answers with an ASSUMPTION or
with a PROOF. That is the tier-A/B claim the documents make in prose — the
curve calls and the three curve types resolve to proven definitions rather
than axioms, because gen/CurveField/Funs.lean opens `namespace
curve25519_dalek` and so defines the very names Aeneas asks for. Nothing
checked it. A regeneration that renamed one, or a model that quietly
answered one with an axiom instead, would have left the documents claiming a
proof where the repository had an assumption.
Phase 0d recomputes the classification with model-correspondence.py
(namespace-aware, so a definition inside a namespace counts under its full
name) and requires equality with the committed MODEL-CORRESPONDENCE.txt.
UNRESOLVED — the extraction asking for something nothing here provides — is
a hard failure.
dalek 43 MODEL 8 PROVEN 3 EXTRA
anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate)
risc0 36 MODEL 8 PROVEN 4 EXTRA
betrusted 35 MODEL 8 PROVEN 4 EXTRA
selftest-correspondence.sh, five cases, negative-tested by disabling the
comparison. The case that matters is 2: a PROVEN external answered by an
axiom instead. No name changes anywhere, every byte pin still matches, and
it compiles, because the signature is unchanged — before Phase 0d nothing in
the button could tell.
Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows,
which was VACUOUS on anza, since anza has none — it removed nothing, the
table still matched, and the case passed while testing nothing. It now
deletes the first row whatever its verdict AND asserts the file changed.
extract.sh now points at the gate instead of asking a human to look.
Certified by a full sweep: both buttons, all four forks, purged trees,
machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
|
|
|
echo "FATAL: the lifted driver has no '$want' — check.sh's phase markers moved."; exit 1; }
|
|
|
|
|
done
|
verification: derive lift dependencies instead of hand-keeping them
Round-9 swept all ten instruments per fork instead of only the two buttons,
and found selftest-axgate.sh red in all four. Root cause, identical to the
KERNLOG defect fixed the day before: a self-test lifts one phase out of
check.sh and supplies its variables from a HAND-WRITTEN preamble, and the
phase later grew a dependency nobody told the preamble about.
Phase 2b stopped globbing Proofs/*.olean and started reading $PROOFS by
membership. Bash does not error on an unset array under `set -u` — it
expands to nothing — so the gate silently received
expected := [".olean"]
one entry with an empty name, and rejected the baseline via its own
fail-closed absence check. Exit code right, reason wrong.
· lift-guard.sh (new, pinned): derives every variable the lifted payload
reads, subtracts what the driver defines, and fails AT LIFT TIME naming
the remainder. Wired into all five lifting self-tests; selftest-shapes.sh
drops its inline copy. One implementation, not five, since drifting out
of sync is the entire failure mode.
· selftest-axgate.sh: lifts PROOFS verbatim, and its case 2 now poisons a
MANIFESTED leaf module rather than adding a stray file. The stray-file
attack was a no-op against membership semantics — that is the dead-file
gate's job, proven by selftest-harness case 8 — and it is also the
weaker attack, since adding files to Proofs/ must clear a gate that
editing an already-manifested module does not.
· Phase 2c's accounting identity moves behind its own marker with its own
ACCTFAIL verdict, so the phase is liftable by construction rather than
by a self-test knowing where to stop. Truncating the lift range alone
would have dropped Phase 2c's own verdict and left the test unable to fail.
· The other four lifting self-tests assert on the PAYLOAD rather than the
assembled driver, so a marker in the preamble cannot satisfy a check
meant to prove the lift landed.
Certified: 8/8 self-tests green in all four forks (~212 assertions, none
failing); check.sh bytes unchanged and still matching the pin written before
the 3h08m round-9 sweep, whose 36 GREEN button rows therefore stand.
--audit-only re-run against the new HARNESS.sha256 in every fork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 11:07:28 +00:00
|
|
|
"$HERE/lift-guard.sh" "$STASH/payload.sh" "$STASH/p0d.sh" "check.sh Phase 0d" || exit 1
|
P2-c: classify and pin the extraction boundary
Aeneas emits a *_Template.lean naming everything the extracted code needs
from outside itself — the extraction's own statement of its boundary.
extract.sh has always said, in prose, "after regenerating, diff the template
against the hand-written file". Prose is not a gate, and the diff cannot be
one: the two files legitimately differ in almost every line, holes and
Aeneas comments against real definitions and modeling policy.
MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two
options — enforce the diff, or pin both files — and the answer turned out to
be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further
things stand here: the generated Funs.lean imports the model and CALLS these
externals, so the Lean compiler enforces their TYPES wherever the extracted
code uses them; and the per-certificate exact cones catch any external that
becomes, or stops being, an assumption anything depends on.
What none of those three sees is the CLASSIFICATION: for each name the
extraction asks for, whether this repository answers with an ASSUMPTION or
with a PROOF. That is the tier-A/B claim the documents make in prose — the
curve calls and the three curve types resolve to proven definitions rather
than axioms, because gen/CurveField/Funs.lean opens `namespace
curve25519_dalek` and so defines the very names Aeneas asks for. Nothing
checked it. A regeneration that renamed one, or a model that quietly
answered one with an axiom instead, would have left the documents claiming a
proof where the repository had an assumption.
Phase 0d recomputes the classification with model-correspondence.py
(namespace-aware, so a definition inside a namespace counts under its full
name) and requires equality with the committed MODEL-CORRESPONDENCE.txt.
UNRESOLVED — the extraction asking for something nothing here provides — is
a hard failure.
dalek 43 MODEL 8 PROVEN 3 EXTRA
anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate)
risc0 36 MODEL 8 PROVEN 4 EXTRA
betrusted 35 MODEL 8 PROVEN 4 EXTRA
selftest-correspondence.sh, five cases, negative-tested by disabling the
comparison. The case that matters is 2: a PROVEN external answered by an
axiom instead. No name changes anywhere, every byte pin still matches, and
it compiles, because the signature is unchanged — before Phase 0d nothing in
the button could tell.
Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows,
which was VACUOUS on anza, since anza has none — it removed nothing, the
table still matched, and the case passed while testing nothing. It now
deletes the first row whatever its verdict AND asserts the file changed.
extract.sh now points at the gate instead of asking a human to look.
Certified by a full sweep: both buttons, all four forks, purged trees,
machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
|
|
|
|
|
|
|
|
expect() { # expect <label> <want-rc> <want-substring>
|
|
|
|
|
local label="$1" want_rc="$2" want_txt="$3" out rc
|
|
|
|
|
out=$(bash "$STASH/p0d.sh" 2>&1); rc=$?
|
|
|
|
|
if [ "$rc" -ne "$want_rc" ]; then
|
|
|
|
|
echo " ✗ $label: exit $rc, expected $want_rc"; tail -6 <<<"$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"; tail -6 <<<"$out" | sed 's/^/ /'
|
|
|
|
|
FAILURES=$((FAILURES+1)); return
|
|
|
|
|
fi
|
|
|
|
|
echo " ✓ $label"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echo "=== selftest-correspondence: attacking check.sh Phase 0d ==="
|
|
|
|
|
echo " template: $TMPL_REL"
|
|
|
|
|
|
|
|
|
|
expect "control: the committed table matches the files" 0 "answered by PROVEN definitions"
|
|
|
|
|
|
|
|
|
|
# ── 1. the extraction asks for something nothing provides ──────────────────
|
|
|
|
|
printf '\naxiom selftest_unprovided_external : Nat\n' >> "$HERE/$TMPL_REL"
|
|
|
|
|
expect "case 1: an external nothing provides" 1 "MODEL CORRESPONDENCE FAILED"
|
|
|
|
|
cp "$STASH/tmpl" "$HERE/$TMPL_REL"
|
|
|
|
|
|
|
|
|
|
# ── 2. a proof silently downgraded to an assumption ────────────────────────
|
|
|
|
|
# Answer a PROVEN external with an axiom in the model. The name does not change
|
|
|
|
|
# anywhere; only the way it is answered does. Nothing else in the button sees
|
|
|
|
|
# this: the byte pins still match their files, the compiler is content because
|
|
|
|
|
# the signature is unchanged, and no certificate's cone moves unless something
|
|
|
|
|
# happens to depend on it.
|
|
|
|
|
if [ -n "$PROVEN_ROW" ]; then
|
|
|
|
|
PROVEN_NAME=$(cut -d'|' -f2 <<<"$PROVEN_ROW")
|
|
|
|
|
PROVEN_TMPL=$(cut -d'|' -f1 <<<"$PROVEN_ROW")
|
|
|
|
|
VICTIM_MODEL="gen/${PROVEN_TMPL}.lean"
|
|
|
|
|
cp "$HERE/$VICTIM_MODEL" "$STASH/model2"
|
|
|
|
|
printf '\naxiom %s : Nat\n' "$PROVEN_NAME" >> "$HERE/$VICTIM_MODEL"
|
|
|
|
|
expect "case 2: a PROVEN external downgraded to an assumption" 1 "MODEL CORRESPONDENCE DRIFT"
|
|
|
|
|
cp "$STASH/model2" "$HERE/$VICTIM_MODEL"
|
|
|
|
|
else
|
|
|
|
|
echo " · case 2 skipped: this fork's extraction has no PROVEN externals"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── 3/4. the committed table itself ────────────────────────────────────────
|
|
|
|
|
# Delete the FIRST row, whatever its verdict. An earlier draft deleted the
|
|
|
|
|
# PROVEN rows, which was vacuous on anza — that fork's extraction has none, so
|
|
|
|
|
# nothing was removed, the table still matched, and the case passed by testing
|
|
|
|
|
# nothing. Pick a row every fork is guaranteed to have.
|
|
|
|
|
sed '0,/|/{/|/d}' "$STASH/corr" > "$HERE/MODEL-CORRESPONDENCE.txt"
|
|
|
|
|
if ! diff -q "$STASH/corr" "$HERE/MODEL-CORRESPONDENCE.txt" >/dev/null; then
|
|
|
|
|
expect "case 3: a row deleted from the committed table" 1 "MODEL CORRESPONDENCE DRIFT"
|
|
|
|
|
else
|
|
|
|
|
echo " ✗ case 3: the table was not actually modified — the case is vacuous"
|
|
|
|
|
FAILURES=$((FAILURES+1))
|
|
|
|
|
fi
|
|
|
|
|
cp "$STASH/corr" "$HERE/MODEL-CORRESPONDENCE.txt"
|
|
|
|
|
|
|
|
|
|
sed -i '0,/|MODEL$/s/|MODEL$/|PROVEN/' "$HERE/MODEL-CORRESPONDENCE.txt"
|
|
|
|
|
expect "case 4: a verdict edited in the committed table" 1 "MODEL CORRESPONDENCE DRIFT"
|
|
|
|
|
cp "$STASH/corr" "$HERE/MODEL-CORRESPONDENCE.txt"
|
|
|
|
|
|
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed
THE DEFECT, as found. GPT-5.6 showed the textual classifier could report
PROVEN for a name Lean resolves to an axiom: a definition appearing only
inside a `/- -/` comment was read as real. Repairing that exposed something
larger and already live. Aeneas wraps long declarations:
axiom
curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg
The old scanner required keyword and name on one physical line, so it matched
nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN
row, no failure. Nine to ten externals per fork had no row at all, and the
tier-A/B `neg` was missing from every committed table while the trust
documents claimed that class was machine-checked. Four pinned tables were
wrong, in four repositories, with the buttons green over them. No attacker
was required; Aeneas's own formatting did it.
The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it
could not parse produced silence instead of a stop. A gate that drops what it
cannot read is worse than no gate, because the green covers a gap that is
invisible in the diff.
THE REPAIR, in three layers that each do only what they honestly can.
· DISCOVERY stays textual, because the template cannot be imported — it
declares the same names as the model and they would clash. It now strips
NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks
namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2
naming file and line.
· Phase 0d keeps the cheap pre-compile comparison against the table.
· Phase 2d is new and authoritative. After compilation it generates a temp
Lean driver (the Phase 2b idiom, so no permanent module and no manifest
churn), imports every non-template gen module, and asks env.find? what
each external IS: kind and originating module. Model module -> MODEL;
non-axiom in a generated module -> PROVEN; an axiom outside the model, or
a name that does not resolve, is a failure. Lean's verdict must equal the
committed table's, and the answer count must equal the question count so
a truncated resolver cannot pass.
THE TABLES were regenerated and verified three ways before being committed:
an independently written second scanner agreed on 228/228 rows across the
four forks; all 47 changed rows carry file:line evidence; and the PROVEN row
was put to Lean directly — kind = def, module = CurveField.Funs.
dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56
Also corrected: `from_residual` was recorded EXTRA in every fork; the
template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA`
row from a mis-parse.
NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test,
a PROVEN row edited to MODEL produces:
SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN
selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample,
the wrapped declaration that was actually live, and a fail-closed check.
WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built
from gen/. It establishes what a name IS — assumption or proof, and where —
not that the extraction faithfully represents the Rust. That remains the
Charon/Aeneas trust assumption and no gate here can close it.
Certified: both buttons, all four forks, purged trees. 8/8 green, with
62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
|
|
|
# ── 5/6. THE ROUND-7 FINDINGS, so they cannot regress ──────────────────────
|
|
|
|
|
# Both were real. Case 5 is GPT-5.6's constructive counterexample: a definition
|
|
|
|
|
# that exists ONLY inside a block comment was read as a real declaration, so the
|
|
|
|
|
# scanner reported PROVEN for a name Lean resolves to an axiom. Case 6 is the
|
|
|
|
|
# one that was live in four committed tables: Aeneas wraps long declarations,
|
|
|
|
|
# the old scanner required keyword and name on one physical line, and so it
|
|
|
|
|
# SILENTLY DROPPED them — nine to ten externals per fork had no row at all.
|
|
|
|
|
#
|
|
|
|
|
# Case 6 is the more important of the two. A gate that drops what it cannot
|
|
|
|
|
# read is worse than no gate: it prints green across a gap that is invisible in
|
|
|
|
|
# the diff. The scanner must now FAIL rather than skip.
|
|
|
|
|
CX=$(mktemp -d)
|
|
|
|
|
mkdir -p "$CX/gen/Forged"
|
|
|
|
|
printf 'axiom Forged.value : Nat\n' > "$CX/gen/Forged/FunsExternal_Template.lean"
|
|
|
|
|
printf 'axiom\n Forged.value : Nat\n' > "$CX/gen/Forged/FunsExternal.lean"
|
|
|
|
|
printf '/-\nnamespace Forged\ndef value : Nat := 0\nend Forged\n-/\n' > "$CX/gen/Forged/Funs.lean"
|
|
|
|
|
OUT=$(python3 "$HERE/model-correspondence.py" "$CX" 2>&1)
|
|
|
|
|
if grep -q 'Forged.value|MODEL' <<<"$OUT"; then
|
|
|
|
|
echo " ✓ case 5: a definition inside a block comment is not read as a declaration"
|
|
|
|
|
else
|
|
|
|
|
echo " ✗ case 5: comment-only definition mis-read — scanner says:"; sed 's/^/ /' <<<"$OUT"
|
|
|
|
|
FAILURES=$((FAILURES+1))
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
printf 'axiom\n Forged.wrapped\n :\n Nat\n' >> "$CX/gen/Forged/FunsExternal_Template.lean"
|
|
|
|
|
OUT=$(python3 "$HERE/model-correspondence.py" "$CX" 2>&1); RC=$?
|
|
|
|
|
if [ "$RC" -ne 0 ] && grep -q 'UNRESOLVED\|Forged.wrapped' <<<"$OUT"; then
|
|
|
|
|
echo " ✓ case 6: a declaration whose name wraps to the next line is SEEN, not dropped"
|
|
|
|
|
else
|
|
|
|
|
echo " ✗ case 6: wrapped declaration dropped or mis-handled (rc=$RC):"; sed 's/^/ /' <<<"$OUT"
|
|
|
|
|
FAILURES=$((FAILURES+1))
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
printf 'axiom\n' > "$CX/gen/Forged/FunsExternal_Template.lean"
|
|
|
|
|
OUT=$(python3 "$HERE/model-correspondence.py" "$CX" 2>&1); RC=$?
|
|
|
|
|
if [ "$RC" -eq 2 ] && grep -q 'fails closed' <<<"$OUT"; then
|
|
|
|
|
echo " ✓ case 7: an unparseable declaration stops the scanner (exit 2), never silence"
|
|
|
|
|
else
|
|
|
|
|
echo " ✗ case 7: unparseable declaration did not fail closed (rc=$RC)"
|
|
|
|
|
FAILURES=$((FAILURES+1))
|
|
|
|
|
fi
|
|
|
|
|
rm -rf "$CX"
|
|
|
|
|
|
P2-c: classify and pin the extraction boundary
Aeneas emits a *_Template.lean naming everything the extracted code needs
from outside itself — the extraction's own statement of its boundary.
extract.sh has always said, in prose, "after regenerating, diff the template
against the hand-written file". Prose is not a gate, and the diff cannot be
one: the two files legitimately differ in almost every line, holes and
Aeneas comments against real definitions and modeling policy.
MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two
options — enforce the diff, or pin both files — and the answer turned out to
be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further
things stand here: the generated Funs.lean imports the model and CALLS these
externals, so the Lean compiler enforces their TYPES wherever the extracted
code uses them; and the per-certificate exact cones catch any external that
becomes, or stops being, an assumption anything depends on.
What none of those three sees is the CLASSIFICATION: for each name the
extraction asks for, whether this repository answers with an ASSUMPTION or
with a PROOF. That is the tier-A/B claim the documents make in prose — the
curve calls and the three curve types resolve to proven definitions rather
than axioms, because gen/CurveField/Funs.lean opens `namespace
curve25519_dalek` and so defines the very names Aeneas asks for. Nothing
checked it. A regeneration that renamed one, or a model that quietly
answered one with an axiom instead, would have left the documents claiming a
proof where the repository had an assumption.
Phase 0d recomputes the classification with model-correspondence.py
(namespace-aware, so a definition inside a namespace counts under its full
name) and requires equality with the committed MODEL-CORRESPONDENCE.txt.
UNRESOLVED — the extraction asking for something nothing here provides — is
a hard failure.
dalek 43 MODEL 8 PROVEN 3 EXTRA
anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate)
risc0 36 MODEL 8 PROVEN 4 EXTRA
betrusted 35 MODEL 8 PROVEN 4 EXTRA
selftest-correspondence.sh, five cases, negative-tested by disabling the
comparison. The case that matters is 2: a PROVEN external answered by an
axiom instead. No name changes anywhere, every byte pin still matches, and
it compiles, because the signature is unchanged — before Phase 0d nothing in
the button could tell.
Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows,
which was VACUOUS on anza, since anza has none — it removed nothing, the
table still matched, and the case passed while testing nothing. It now
deletes the first row whatever its verdict AND asserts the file changed.
extract.sh now points at the gate instead of asking a human to look.
Certified by a full sweep: both buttons, all four forks, purged trees,
machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
|
|
|
expect "restored: the table matches again" 0 "answered by PROVEN definitions"
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
if [ "$FAILURES" -eq 0 ]; then
|
|
|
|
|
echo "SELFTEST PASSED — an external cannot change how it is answered, and a"
|
|
|
|
|
echo "proof cannot be downgraded to an assumption, without failing the button."
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
echo "SELFTEST FAILED: $FAILURES case(s) did not behave as claimed."
|
|
|
|
|
exit 1
|