Skip to content

Commit 16f20cc

Browse files
verification(A3.1 step-4): proof-phase foundation (execList_append + setVar frame) + record the N-mask gate finding
Proof-phase foundations (advisor: "frame lemmas first"): - execList_append: running xs ++ ys runs xs then (only if it didn't halt) ys — the structural lemma behind every phase split, halt short-circuit threaded. - setVar_get_eq / setVar_get_ne: binding-frame lemmas so a phase can carry a computed binding (e.g. digest) past later statements touching other vars. FINDING (surfaced by the refinement, documented in A3_1_CLOSURE_PATH.md §8): the deployed Yul rejects non-N-masked pkSeed/pkRoot (L58-65), but verifyYulModel = verifyRefined does `take 16` with NO N-mask gate (confirmed Refined.lean:143). So execC10Asm = verifyYulModel is not literally true; the faithful, STRONGER target is execC10Asm = nMaskedB pkSeed && nMaskedB pkRoot && verifyYulModel — folding the input-gate (currently a separate Halmos check) into one kernel statement. It is a precision win, not a bug: the bytecode is strictly more restrictive (safety direction holds), DeployedBytecode is opaque (nothing provably-false), and the divergence is unreachable via the factory/addOwner (which N-mask). The phase proofs are UNAFFECTED (they prove the both-N-masked branch computes verifyYulModel). The only consequence is a step-6, USER-GATED reconciliation that threads the already-modeled Factory.nMasked invariant through theft_free's liveness use of the A3.1 axiom — old axiom kept in place until then. verify-build green, verify-interp 396/396, theft_free closure unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9682f45 commit 16f20cc

2 files changed

Lines changed: 72 additions & 1 deletion

File tree

contracts/verification/docs/A3_1_CLOSURE_PATH.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,41 @@ with the climb lemmas as the loop-refinement engine.
369369
6. Replace `solidityVerifier_compiles_correctly` with the narrow
370370
`DeployedBytecode.SPHINCsC10Asm_verify = execC10Asm` transcription axiom
371371
(empirically backed by step 3 / `make verify-interp`) + re-derive the old
372-
statement as a theorem.
372+
statement as a theorem. **See the N-mask finding below — this step touches
373+
`theft_free` and is USER-GATED.**
374+
375+
### FINDING (2026-06-17, surfaced by the refinement) — N-mask gate: bytecode strict, model lenient
376+
377+
The deployed Yul (`SPHINCsC10Asm.sol` L58–65) rejects (`return false`) any
378+
`pkSeed`/`pkRoot` not in N-mask shape (`and(key, N_MASK) != key`). The Lean model
379+
`verifyYulModel = Verifier.Refined.verifyRefined` (confirmed at `Refined.lean:143`)
380+
takes `ByteVec 32` keys and does `pkSeed.take 16`**silently discarding the
381+
bottom 16 bytes, with NO N-mask check.** So `execC10Asm = verifyYulModel` is **not
382+
literally true**: on a non-N-masked key, `execC10Asm = false` (gate) while
383+
`verifyYulModel` uses the top 16 and can be `true`. (Deductively certain;
384+
`verify-interp` 396/396 is consistent because every KAT/bulk key is N-masked.)
385+
386+
- **It is a precision WIN, not a bug.** The bytecode is strictly *more* restrictive
387+
(`bytecode true → model true` always; divergence only at `bytecode false, model
388+
true`, reachable only by inputs the factory/`addOwner` never produce — they
389+
N-mask). `DeployedBytecode` is opaque, so the old axiom is unfaithful-to-reality,
390+
NOT provably-false (no soundness fire). The project already covers the gate via
391+
**Halmos input-gates, separately** from the Lean equiv; the interpreter folds
392+
*core + gate* into ONE kernel statement, subsuming the Halmos check.
393+
- **True target theorem (step 5/6 end-state):**
394+
`execC10Asm pkS pkR m sig = nMaskedB pkS && nMaskedB pkR && verifyYulModel pkS pkR m sig`,
395+
with `nMaskedB key := (wordOf key) &&& N_MASK == wordOf key` (Bool, matching the
396+
bytecode) + a small lemma to `Wallet.Factory.nMasked` (Prop). The phase proofs
397+
(H_msg→FORS→WOTS→hypertree) are UNAFFECTED — they prove the both-N-masked branch
398+
computes `verifyYulModel`; the gate is the top-level `ifnz` wrapper.
399+
- **`theft_free` interaction (step 6, USER-GATED):** `theft_free` (Theorems.lean
400+
:350-352) uses the A3.1 axiom in the LIVENESS direction (`rw [hbridge]; exact
401+
hverify`: `verifyYulModel true → DeployedBytecode true`). Under the faithful
402+
characterization that needs the stored keys N-masked — modeled
403+
(`Factory.nMasked`/`Storage.hasNMaskLayout`) and enforced by the factory/addOwner,
404+
so threadable, but it edits `theft_free`. **KEEP the old axiom in place for now;
405+
bring the swap (transcription axiom + characterization + threading the N-mask
406+
invariant) to the user at step 6, with the characterization already proven.**
373407

374408
- **(historical) the original executable-first NEXT list (now steps 1–3 done):**
375409
1. **`Interpreter/Yul.lean`** — the C10 opcode-subset AST (`Expr`:

contracts/verification/lean/SphincsCVerify/Interpreter/Yul.lean

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,43 @@ mutual
181181
| (vm', some h) => (vm', some h)
182182
end
183183

184+
/-! ## Composition + binding-frame lemmas (proof-phase foundation)
185+
186+
These are the workhorses the phase-wise refinement consumes (`docs/A3_1_CLOSURE_PATH.md`
187+
§8 step 4): `execList_append` splits the program at phase boundaries (with the
188+
halt short-circuit threaded), and the `setVar` frame lemmas let a phase carry a
189+
computed binding (e.g. `digest`) past later statements that touch other vars. -/
190+
191+
/-- Reading the just-bound variable returns the bound value. -/
192+
@[simp] theorem setVar_get_eq (env : VarEnv) (x : String) (v : Nat) :
193+
setVar env x v x = v := by
194+
unfold setVar; rw [if_pos rfl]
195+
196+
/-- Reading a *different* variable is unaffected by a binding. -/
197+
@[simp] theorem setVar_get_ne (env : VarEnv) (x y : String) (v : Nat) (h : y ≠ x) :
198+
setVar env x v y = env y := by
199+
unfold setVar; rw [if_neg h]
200+
201+
/-- **Phase composition.** Running `xs ++ ys` runs `xs`, and — only if `xs` did
202+
not halt — continues with `ys` from the resulting state; a halt in `xs`
203+
short-circuits. The structural lemma behind every phase split. -/
204+
theorem execList_append {n : Nat} (sha : List UInt8 → Spec.ByteVec 32) (sig : Spec.ByteVec n)
205+
(xs ys : List Stmt) (vm : VM) :
206+
execList sha sig (xs ++ ys) vm
207+
= match execList sha sig xs vm with
208+
| (vm', none) => execList sha sig ys vm'
209+
| (vm', some h) => (vm', some h) := by
210+
induction xs generalizing vm with
211+
| nil => simp only [List.nil_append, execList]
212+
| cons s rest ih =>
213+
show execList sha sig (s :: (rest ++ ys)) vm = _
214+
rw [execList]
215+
cases hs : execStmt sha sig s vm with
216+
| mk vm' o =>
217+
cases o with
218+
| none => rw [execList, hs]; exact ih vm'
219+
| some h => rw [execList, hs]
220+
184221
/-! ## Top-level program execution -/
185222

186223
/-- Run a program from a caller-supplied initial memory (`mem0` lets the caller

0 commit comments

Comments
 (0)