risc0-ed25519-verified/verification/Proofs/DsmNafSpec.lean

85 lines
4.6 KiB
Text
Raw Permalink Normal View History

NAF encoder proven end-to-end + the phase-1 double-scalar-mul apex The complete non_adjacent_form(5) verification (four stages): - `Proofs/DsmNafLoadSpec.lean` (generated) — the LE byte-to-word load. - `Proofs/DsmNafMath.lean` — the digit loop's arithmetic core: window-read lemmas (single/cross-word), the exact ZZ invariant steps (Nat.mod_mul telescope), the carry-kill argument from V < 2^253, and the exit theorem. - `Proofs/DsmNafLoopSpec.lean` — the w=5 digit loop by induction on the remaining-bits measure: per-step 64-bit window read (4-way word split), digit write via hcast/wrapping_sub (exact value window - 32*carry', oddness, |d| < 16), invariant carried through even/odd steps. - `Proofs/DsmNafSpec.lean` — the public spec: both entry masserts DISCHARGED; the digits satisfy the NAF conditions and sum naf[k]*2^k = V EXACTLY (integers, no modular slack) for any scalar whose LE byte value V is below 2^253. And the campaign's brick 4, `Proofs/DsmMulSpec.lean`: - `run_basepoint` — the transpiled ED25519_BASEPOINT_POINT is the standard base point: valid extended coordinates (X*Y = Z*T) and the curve equation, kernel-checked via denominator-free 121666-scaled witnesses. Includes the generic witness lemmas fp_mul_eq_of_witness / onCurve_of_witness. - `vartime_double_base_mul_spec` — THE PHASE-1 COMPUTATIONAL SPEC of vartime_double_base::mul: for canonical scalars and a valid on-curve A, the result is valid, on-curve, and denotes dsmFold (naf a) (naf b) (edPt A) edBasePt edId 256 with both digit arrays proven exact NAF encodings. Phase 2 (group semantics [a]A + [b]B) requires Edwards associativity — deferred and documented; nothing assumes it. Also: removed a vestigial pre-re-extraction axiom stub (backend.serial.scalar_mul.vartime_double_base.mul) from FunsExternal — a root-level leftover that shadowed the real namespaced definition during name resolution in proof files. Never referenced by any certificate (the #print-axioms audit guards against that); deleted for hygiene. CERTS += naf_load_spec, naf_exit, naf_digit_loop_spec, non_adjacent_form_spec, run_basepoint, vartime_double_base_mul_spec — each audited to exactly [propext, Classical.choice, Quot.sound]. Full check.sh green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:52:08 +00:00
/- ──────────────────────────────────────────────────────────────────────────────
Proofs/DsmNafSpec.lean — NAF campaign, stage 4: the public spec of
`Scalar::non_adjacent_form(5)`.
Composes the proven stages: both entry masserts DISCHARGED (w = 5 is in
[2,8]), the LE byte→word load (DsmNafLoadSpec), width = 1<<<5 = 32 and
window_mask = 31 computed, and the digit loop (DsmNafLoopSpec) seeded
with the all-zeros state whose invariant is trivial.
POST: the 256 digits satisfy the NAF conditions (odd-or-zero, |d| < 16 —
exactly `NafDigits`, what dsm_loop_spec consumes) and their signed sum
reconstructs the scalar's little-endian byte value EXACTLY:
nafSum res 256 = V (as integers, no modular slack).
Requires V < 2^253 — canonical scalars, which the mul call sites provide.
────────────────────────────────────────────────────────────────────────────── -/
import Proofs.DsmNafLoadSpec
import Proofs.DsmNafLoopSpec
open Aeneas Aeneas.Std Result ControlFlow
open curve25519_dalek
set_option maxHeartbeats 8000000
set_option linter.unusedSimpArgs false
set_option maxRecDepth 8000
namespace CurveFieldProofs
open Aeneas.Std.WP
/-- **Scalar::non_adjacent_form(5)**: for a scalar whose 32-byte LE value V
is below 2^253, the result is a 256-entry NAF digit array — every digit
odd or zero with |d| < 16, and Σ naf[k]·2^k = V exactly. Both entry
masserts are discharged. -/
theorem non_adjacent_form_spec (self : scalar.Scalar)
(b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13 b14 b15 b16 b17 b18 b19 b20 b21 b22 b23 b24 b25 b26 b27 b28 b29 b30 b31 : Std.U8)
(hb : (↑self.bytes : List Std.U8) = [b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31])
(V : )
(hVbytes : V = b0.val + b1.val * 2^8 + b2.val * 2^16 + b3.val * 2^24 + b4.val * 2^32 + b5.val * 2^40 + b6.val * 2^48 + b7.val * 2^56 + b8.val * 2^64 + b9.val * 2^72 + b10.val * 2^80 + b11.val * 2^88 + b12.val * 2^96 + b13.val * 2^104 + b14.val * 2^112 + b15.val * 2^120 + b16.val * 2^128 + b17.val * 2^136 + b18.val * 2^144 + b19.val * 2^152 + b20.val * 2^160 + b21.val * 2^168 + b22.val * 2^176 + b23.val * 2^184 + b24.val * 2^192 + b25.val * 2^200 + b26.val * 2^208 + b27.val * 2^216 + b28.val * 2^224 + b29.val * 2^232 + b30.val * 2^240 + b31.val * 2^248)
(hV : V < 2^253) :
scalar.Scalar.non_adjacent_form self 5#usize ⦃ res =>
NafDigits res ∧ nafSum res 256 = (V : ) ⦄ := by
unfold scalar.Scalar.non_adjacent_form
step with (massert_spec (5#usize ≥ 2#usize) (by scalar_tac)) as ⟨h2⟩
step with (massert_spec (5#usize ≤ 8#usize) (by scalar_tac)) as ⟨h8⟩
-- the LE load fills x_u64[0..3]; word 4 stays 0
step with (naf_load_spec self (Array.repeat 5#usize 0#u64)
b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13 b14 b15 b16 b17 b18 b19 b20 b21 b22 b23 b24 b25 b26 b27 b28 b29 b30 b31
hb (by simp [List.replicate])) as ⟨v0, v1, v2, v3, ws, hws, hv0, hv1, hv2, hv3⟩
-- width ← 1 <<< 5 (= 32), window_mask ← width 1 (= 31)
step as ⟨wd, hwd⟩
have hwdv : wd = 32#u64 := by clear * - hwd; scalar_tac
rw [hwdv]
step as ⟨mk, hmk⟩
have hmkv : mk = 31#u64 := by clear * - hmk; scalar_tac
rw [hmkv]
-- the initial all-zeros digit state
have hz : ∀ k, k < 256 → nafDigit (Array.repeat 256#usize 0#i8) k = 0 := by
intro k hk
unfold nafDigit
rw [getElem!_pos (↑(Array.repeat 256#usize 0#i8) : List Std.I8) k
(by simp; omega)]
simp only [Array.repeat_val, List.getElem_replicate]
simp
have hsum0 : nafSum (Array.repeat 256#usize 0#i8) 256 = 0 := by
unfold nafSum
apply Finset.sum_eq_zero
intro k hk
rw [hz k (Finset.mem_range.mp hk)]
ring
-- the word-form value
have hVw : V = v0.val + 2^64 * (v1.val + 2^64 * (v2.val + 2^64 * (v3.val + 2^64 * 0))) := by
rw [hVbytes, hv0, hv1, hv2, hv3]
ring
-- the digit loop from the trivial invariant
apply naf_digit_loop_spec ws v0 v1 v2 v3 V hws hVw hV 256
(Array.repeat 256#usize 0#i8) 0#usize 0#u64
(by scalar_tac)
(by scalar_tac)
(by intro h; simp at h)
(fun k _ hk => hz k hk)
(fun k hk => ⟨Or.inl (hz k hk), by rw [hz k hk]; norm_num,
by rw [hz k hk]; norm_num⟩)
(by simp [hsum0])
end CurveFieldProofs