mirror of
https://github.com/saymrwulf/risc0-ed25519-verified.git
synced 2026-09-04 20:03:41 +00:00
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.
126 lines
6.2 KiB
Bash
Executable file
126 lines
6.2 KiB
Bash
Executable file
#!/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.
|
|
{ echo 'set -euo pipefail'
|
|
echo "HERE=\"$HERE\""
|
|
awk '/^# ── Phase 0d/{f=1} f&&/^# ── (Phase 1|Phases end)/{exit} f{print}' "$HERE/check.sh"
|
|
} > "$STASH/p0d.sh"
|
|
for want in 'Phase 0d' 'MODEL CORRESPONDENCE' 'model-correspondence.py'; do
|
|
grep -qF "$want" "$STASH/p0d.sh" || {
|
|
echo "FATAL: the lifted driver has no '$want' — check.sh's phase markers moved."; exit 1; }
|
|
done
|
|
|
|
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"
|
|
|
|
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
|