scalar layer: Scalar52::sub FULLY proven mod l (sub_val_spec)

sub_val_spec closes the top-level two-clause value spec:
  denote(Scalar52.sub a b) = denote(a) - denote(b)   in ZMod l
for limb-bounded inputs with canonical subtrahend (scVal b < l).
Axiom-clean [propext, Classical.choice, Quot.sound]; no sorry.

The assembly documented-as-remaining last session is now done. Key resolutions:
- Applied the WP "spec_bind" rule manually instead of "step", and reduced the
  resulting "let (difference,borrow) := (dw,w)" Prod-let with an explicit
  "show" — this was the destructuring friction that blocked the earlier
  attempt (step's arity heuristics mis-typed the pair result).
- Inlined the subtle "Choice::from" identity (no step-spec, like csel_step).
- PERFORMANCE: kept "scLimbs" as opaque atoms in the gamma5=1 derivation
  instead of "unfold ... at *" — the unfold exploded omega with 2^52..2^260
  coefficients and blew past 600s; atomic form proves in ~1 min (METHOD 4,
  same kernel-cost discipline as the field layer).
- The 2^260 borrow-wrap and the +l conditional-add cancel in ZMod l:
  beta5=0 direct; beta5=1 forces top carry gamma5=1 from scVal b < l, closed
  by linear_combination over the two telescopes (sub_telescope, add_telescope).

check-scalar.sh: sub_val_spec in the manifest + Phase-3 audit (4/4 clean);
proof-phase caps raised to 600s/8192MB for the assembly; full button green.
exponentiation.threshold raised to 300 for the 2^260 literal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
saymrwulf 2026-07-03 17:26:50 +02:00
parent 8a37ed7273
commit 1766544daf
3 changed files with 110 additions and 30 deletions

View file

@ -27,7 +27,7 @@ in this repository.
|-------|-------------|--------|-----------------------| |-------|-------------|--------|-----------------------|
| Field 𝔽_p | `fieldImplementation` | ✅ proven | `[propext, Classical.choice, Quot.sound]` | | Field 𝔽_p | `fieldImplementation` | ✅ proven | `[propext, Classical.choice, Quot.sound]` |
| Group law (Edwards) | `edwardsImplementation` | ✅ proven | `[propext, Classical.choice, Quot.sound]` | | Group law (Edwards) | `edwardsImplementation` | ✅ proven | `[propext, Classical.choice, Quot.sound]` |
| Scalar mod | `scalarImplementation` (planned; `L_val`, `sub_loop_spec`, `cond_add_l_*_spec` proven) | 🔨 foundation+sub | denotation + L= + sub borrow/carry chains proven; sub value-assembly & mul in progress | | Scalar mod | `scalarImplementation` (planned; `sub_val_spec` ✅, `L_val`, loop specs proven) | 🔨 sub done · add/mul next | denotation + L= + FULL sub (⟦sub a b⟧=⟦a⟧⟦b⟧ in ZMod ) proven; add & Montgomery mul next |
| Signature (EdDSA) | `verifyEquation` (planned) | ⏳ planned | — | | Signature (EdDSA) | `verifyEquation` (planned) | ⏳ planned | — |
Status legend: ✅ proven & axiom-audited · ⏳ in progress · ❌ not started. Status legend: ✅ proven & axiom-audited · ⏳ in progress · ❌ not started.

View file

@ -51,6 +51,7 @@ open curve25519_dalek
set_option maxHeartbeats 4000000 set_option maxHeartbeats 4000000
set_option linter.unusedSimpArgs false set_option linter.unusedSimpArgs false
set_option exponentiation.threshold 300
namespace ScalarProofs namespace ScalarProofs
@ -705,32 +706,111 @@ theorem add_telescope
+ (c0 + 2^52*c1 + 2^104*c2 + 2^156*c3 + 2^208*c4) := by + (c0 + 2^52*c1 + 2^104*c2 + 2^156*c3 + 2^208*c4) := by
omega omega
/-! ### Top-level `sub_val_spec` — remaining assembly (no `sorry` shipped) /-! ### Top-level subtraction value spec -/
Every mechanically hard piece is proven above and kernel-checked: /-- **Scalar subtraction is correct mod .** For limb-bounded inputs with
• `sub_loop_spec` — the full 5-limb borrow chain (wrapping_sub, mask, canonical subtrahend (scVal b < ), the transpiled `Scalar52::sub`
borrow-out bit): the loop unroll that blocked the denotes ⟦a⟧ ⟦b⟧ in `ZMod `. Assembly of `sub_loop_spec` (borrow
earlier attempt; chain) and `cond_add_l_{zero,one}_spec` (conditional +) through the
• `cond_add_l_zero_spec`, `cond_add_l_one_spec` — both conditional-add- two telescopes; the bind is applied manually via `spec_bind` to keep
cases, full carry chains (the condition-1 case drives full control of the postcondition destructuring. -/
the write-back through a stepped `csel_step` so the theorem sub_val_spec (a b : Sc)
`index_mut` matches `sub_loop`'s working pattern); (a0 a1 a2 a3 a4 b0 b1 b2 b3 b4 : U64)
• `sub_telescope`, `add_telescope` — the 2⁵²ⁱ-weighted value telescopes (ha : (↑a : List U64) = [a0, a1, a2, a3, a4])
up to 2²⁶⁰, discharged by `omega` (no kernel-capacity (hb : (↑b : List U64) = [b0, b1, b2, b3, b4])
blowup — coefficients stay ≤ 2²⁶⁰); (hab : a0.val < 2^52 ∧ a1.val < 2^52 ∧ a2.val < 2^52 ∧ a3.val < 2^52 ∧ a4.val < 2^52)
• `sub_step_arith`, `csel_step`, the bit↔arith lemmas. (hbb : b0.val < 2^52 ∧ b1.val < 2^52 ∧ b2.val < 2^52 ∧ b3.val < 2^52 ∧ b4.val < 2^52)
(hcb : scVal b < Ell) :
`sub_val_spec` assembles them: unfold `sub`, run `sub_loop`, read backend.serial.u64.scalar.Scalar52.sub a b
`borrow >>> 63` into a `Choice`, run `conditional_add_l`, split on the ⦃ r => scDenote r = scDenote a - scDenote b ⦄ := by
underflow bit β5. The value identity in `ZMod `, obtain ⟨hA0, hA1, hA2, hA3, hA4⟩ := hab
⟦sub a b⟧ = ⟦a⟧ ⟦b⟧ (canonical inputs, scVal b < ), obtain ⟨hB0, hB1, hB2, hB3, hB4⟩ := hbb
is complete on paper: β5 = 0 gives ⟦d⟧ + ⟦b⟧ = ⟦a⟧ directly; β5 = 1 gives unfold backend.serial.u64.scalar.Scalar52.sub
the borrow-wrap 2²⁶⁰ and the +, which cancel in `ZMod ` once the top step as ⟨sh, hsh⟩
carry γ5 = 1 (forced by scVal b < ). The remaining step is purely the step as ⟨mask, hmask⟩
Aeneas binding-arity for destructuring `sub_loop_spec`'s pair-valued, have hmaskv : mask.val = 2^52 - 1 := by
multi-existential postcondition inside the outer `do`-block — a mechanical, simp [hmask, hsh, U64.size_def, U64.numBits]
not a mathematical or trust gap. Tracked in the control-repo MANIFEST -- bind the borrow loop manually
scalar `open_frontier`; this file ships only kernel-checked content apply spec_bind (sub_loop_spec a b mask a0 a1 a2 a3 a4 b0 b1 b2 b3 b4 ha hb hmaskv
(Invariants H1/H4). -/ ⟨hA0, hA1, hA2, hA3, hA4, hB0, hB1, hB2, hB3, hB4⟩)
rintro ⟨dw, w⟩ ⟨d0, d1, d2, d3, d4, β1, β2, β3, β4, β5, hdl,
hβ1, hβ2, hβ3, hβ4, hβ5, hd0, hd1, hd2, hd3, hd4,
he0, he1, he2, he3, he4, hbor⟩
simp only at hdl hbor
show (do
let i1 ← w >>> 63#i32
let i2 ← lift (UScalar.cast UScalarTy.U8 i1)
let c ← subtle.Choice.Insts.CoreConvertFromU8.from i2
let (_, difference1) ← dw.conditional_add_l c
ok difference1) ⦃ r => scDenote r = scDenote a - scDenote b ⦄
-- borrow >>> 63, cast, Choice
step as ⟨i1, hi1⟩
have hi1v : i1.val = β5 := by rw [hi1]; exact hbor
step as ⟨i2, hi2⟩
-- Choice.from is the identity model; inline c := i2
simp only [subtle.Choice.Insts.CoreConvertFromU8.from, bind_tc_ok]
set cc := i2 with hccdef
have hccv : cc.val = β5 := by
have hb5 : β5 < 2^8 := by omega
rw [hi2, UScalar.cast_val_eq, hi1v]
simp only [UScalarTy.U8, UScalarTy.numBits]
omega
have hdb : d0.val < 2^52 ∧ d1.val < 2^52 ∧ d2.val < 2^52 ∧ d3.val < 2^52 ∧ d4.val < 2^52 :=
⟨hd0, hd1, hd2, hd3, hd4⟩
have hTsub : scLimbs d0 d1 d2 d3 d4 + scLimbs b0 b1 b2 b3 b4
= scLimbs a0 a1 a2 a3 a4 + 2^260 * β5 := by
unfold scLimbs
exact sub_telescope _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ he0 he1 he2 he3 he4
have hsva : scVal a = scLimbs a0 a1 a2 a3 a4 := scVal_eq a a0 a1 a2 a3 a4 ha
have hsvb : scVal b = scLimbs b0 b1 b2 b3 b4 := scVal_eq b b0 b1 b2 b3 b4 hb
rcases (Nat.le_one_iff_eq_zero_or_eq_one.mp hβ5) with hβz | hβo
· -- β5 = 0: no underflow, cond-add is identity on values
have hc0 : cc.val = 0 := by rw [hccv, hβz]
apply spec_bind (cond_add_l_zero_spec dw cc d0 d1 d2 d3 d4 hdl hc0 hdb)
rintro ⟨cw1, cw2⟩ ⟨r0, r1, r2, r3, r4, hrl, hr0, hr1, hr2, hr3, hr4⟩
simp only at hrl
show scDenote cw2 = scDenote a - scDenote b
have hcwval : scVal cw2 = scLimbs d0 d1 d2 d3 d4 := by
rw [scVal_eq cw2 r0 r1 r2 r3 r4 hrl]; unfold scLimbs; rw [hr0, hr1, hr2, hr3, hr4]
have key : scVal cw2 + scVal b = scVal a := by
rw [hcwval, hsva, hsvb]; rw [hβz] at hTsub; simpa using hTsub
have hc := congrArg (Nat.cast (R := ZMod Ell)) key
push_cast at hc
simp only [scDenote]; rw [eq_sub_iff_add_eq]; exact hc
· -- β5 = 1: underflow; +, and the 2^260 wrap cancels in ZMod
have hc1 : cc.val = 1 := by rw [hccv, hβo]
apply spec_bind (cond_add_l_one_spec dw cc d0 d1 d2 d3 d4 hdl hc1 hdb)
rintro ⟨cw1, cw2⟩ ⟨r0, r1, r2, r3, r4, γ1, γ2, γ3, γ4, γ5, hrl,
hgb1, hgb2, hgb3, hgb4, hgb5, hrb0, hrb1, hrb2, hrb3, hrb4,
hf0, hf1, hf2, hf3, hf4⟩
simp only at hrl
show scDenote cw2 = scDenote a - scDenote b
have hLsum : (671914833335277 + 2^52*3916664325105025 + 2^104*1367801
+ 2^156*0 + 2^208*17592186044416 : ) = Ell := by unfold Ell; norm_num
have hTadd : scLimbs r0 r1 r2 r3 r4 + 2^260 * γ5 = scLimbs d0 d1 d2 d3 d4 + Ell := by
have h := add_telescope r0.val r1.val r2.val r3.val r4.val
d0.val d1.val d2.val d3.val d4.val
671914833335277 3916664325105025 1367801 0 17592186044416
γ1 γ2 γ3 γ4 γ5 hf0 hf1 hf2 hf3 hf4
unfold scLimbs; rw [← hLsum]; linear_combination h
have hblt : scLimbs b0 b1 b2 b3 b4 < Ell := by rw [← hsvb]; exact hcb
-- γ5 = 1, derived with scLimbs kept as opaque atoms (no 2^52i unfold →
-- omega stays cheap: 4 atoms + one 2^260 literal + Ell as an atom)
have hrlt : scLimbs r0 r1 r2 r3 r4 < 2^260 := by unfold scLimbs; omega
have hd_eq : scLimbs d0 d1 d2 d3 d4 + scLimbs b0 b1 b2 b3 b4
= scLimbs a0 a1 a2 a3 a4 + 2^260 := by rw [hβo] at hTsub; simpa using hTsub
have hγ5 : γ5 = 1 := by
-- atoms: R,D,B,A := scLimbs …, Ell; facts below force γ5 = 1
have hRnn : 0 ≤ scLimbs a0 a1 a2 a3 a4 := Nat.zero_le _
omega
have hc := congrArg (Nat.cast (R := ZMod Ell)) hTadd
have hc2 := congrArg (Nat.cast (R := ZMod Ell)) hTsub
have hEz : (Ell : ZMod Ell) = 0 := ZMod.natCast_self Ell
simp only [scDenote, scVal_eq cw2 r0 r1 r2 r3 r4 hrl, hsva, hsvb, hβo, hγ5]
rw [hγ5] at hc
rw [hβo] at hc2
push_cast at hc hc2 ⊢
rw [hEz] at hc
linear_combination hc + hc2
end ScalarProofs end ScalarProofs

View file

@ -19,7 +19,7 @@ lake env bash -c "
cd '$HERE/gen' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD:$HERE\" cd '$HERE/gen' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD:$HERE\"
for m in ${GEN[*]}; do echo \" · gen \$m\"; LEAN_TIMEOUT=300 LEAN_MEM_MB=6144 '$HERE/lean-guard' \"\$m.lean\" || exit 1; done for m in ${GEN[*]}; do echo \" · gen \$m\"; LEAN_TIMEOUT=300 LEAN_MEM_MB=6144 '$HERE/lean-guard' \"\$m.lean\" || exit 1; done
cd '$HERE' cd '$HERE'
for m in ${PROOFS[*]}; do echo \" · proof \$m\"; LEAN_TIMEOUT=300 LEAN_MEM_MB=6144 '$HERE/lean-guard' \"Proofs/\$m.lean\" || exit 1; done for m in ${PROOFS[*]}; do echo \" · proof \$m\"; LEAN_TIMEOUT=600 LEAN_MEM_MB=8192 '$HERE/lean-guard' \"Proofs/\$m.lean\" || exit 1; done
" || { echo FAIL; exit 1; } " || { echo FAIL; exit 1; }
echo "=== Phase 3: axiom audit (kernel-level) ===" echo "=== Phase 3: axiom audit (kernel-level) ==="
cd "$AENEAS_LEAN" cd "$AENEAS_LEAN"
@ -30,12 +30,12 @@ lake env bash -c "
AUD=\$(mktemp '$HERE/.audit-scalar-XXXX.lean') AUD=\$(mktemp '$HERE/.audit-scalar-XXXX.lean')
{ echo 'import Proofs.ScalarDenote'; echo 'import Proofs.ScalarSubSpec'; echo '#print axioms ScalarProofs.L_val' { echo 'import Proofs.ScalarDenote'; echo 'import Proofs.ScalarSubSpec'; echo '#print axioms ScalarProofs.L_val'
echo '#print axioms ScalarProofs.sub_loop_spec' echo '#print axioms ScalarProofs.sub_loop_spec'
echo '#print axioms ScalarProofs.cond_add_l_one_spec'; } > \"\$AUD\" echo '#print axioms ScalarProofs.cond_add_l_one_spec'; echo '#print axioms ScalarProofs.sub_val_spec'; } > \"\$AUD\"
OUT=\$(LEAN_TIMEOUT=120 LEAN_MEM_MB=4096 '$HERE/lean-guard' \"\$AUD\" 2>&1) OUT=\$(LEAN_TIMEOUT=120 LEAN_MEM_MB=4096 '$HERE/lean-guard' \"\$AUD\" 2>&1)
echo \"\$OUT\" echo \"\$OUT\"
rm -f \"\$AUD\" \"\${AUD%.lean}.olean\" rm -f \"\$AUD\" \"\${AUD%.lean}.olean\"
N=\$(echo \"\$OUT\" | grep -cF \"depends on axioms: [propext, Classical.choice, Quot.sound]\" || true) N=\$(echo \"\$OUT\" | grep -cF \"depends on axioms: [propext, Classical.choice, Quot.sound]\" || true)
[ \"\$N\" -eq 3 ] || { echo \"AXIOM AUDIT FAILED: \$N/3 clean\"; exit 1; } [ \"\$N\" -eq 4 ] || { echo \"AXIOM AUDIT FAILED: \$N/4 clean\"; exit 1; }
" || { echo FAIL; exit 1; } " || { echo FAIL; exit 1; }
echo " L_val axiom-clean" echo " L_val axiom-clean"