dalek-ed25519-verified/verification/Proofs/DsmLoopSpec.lean
mrwulf 4071d68457 Double-scalar-mul proof campaign, bricks 1-3: table, digit step, loop
Three new proof files over the CurveField extraction, composing the proven
group-law layer (no new axioms, no associativity assumed — computational
layering over the abstract `edAdd`):

- `Proofs/DsmTableSpec.lean` — `NafLookupTable5::from(&A)`: the 8 entries
  are valid `ProjectiveNielsPoint` caches of valid on-curve points denoting
  the odd multiples A, 3A, ..., 15A as the `edOdd` double-and-add recursion.
  7 explicit loop peels over edwards_as_projective_niels_spec /
  add_projniels_law / compl_as_extended_law, seeded by edwards_double_law.
  `select`: both masserts (x odd, x < 16) DISCHARGED — panic-freedom is
  proven, not assumed; post enumerates all 8 digit cases.

- `Proofs/DsmStepSpec.lean` — `proj_double_law` (the projective doubling
  denotes `edAdd P P`; same Z^2-scaled linear_combination discipline as the
  extended-coordinate law), `compl_as_projective_law` ((X:Z),(Y:T) to
  (XT:YZ:ZT) preserves the point), `naf_select_entry` (digit-indexed lookup
  returns THE entry: NafEntryOf r A ((x-1)/2)), and `dsm_step_p_law` /
  `dsm_step_b_law`: the three-way NAF digit step denotes `edDigit` — add
  the d-th odd multiple, add its negation, or pass through.

- `Proofs/DsmLoopSpec.lean` — the 256-iteration Straus loop by GENUINE
  induction on the counter (one symbolic body walk, no unrolling):
  `dsm_loop_spec` — from the identity, the loop returns a valid on-curve
  point denoting `dsmFold ... edId 256`, the abstract double-and-add fold
  of both digit arrays over the table points. Digit and table hypotheses
  are exactly what the NAF spec and naf_table_spec provide (layering).

check.sh wired: PROOFS + AUDIT_IMPORTS + 7 new CERTS (naf_table_spec,
naf_select_spec, proj_double_law, compl_as_projective_law, dsm_step_p_law,
dsm_step_b_law, dsm_loop_spec), each `#print axioms`-audited to exactly
[propext, Classical.choice, Quot.sound]. Full check.sh green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:30:18 +02:00

216 lines
10 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/- ──────────────────────────────────────────────────────────────────────────────
Proofs/DsmLoopSpec.lean — double-scalar-mul campaign, brick 3:
the 256-iteration Straus/NAF loop, by genuine induction (no unrolling).
dsm_loop seeds r = identity, j = top+1 and runs
while j > 0 { idx = j-1; t = r.double();
t = dsm_step_p(t, table_a, a_naf[idx]);
t = dsm_step_b(t, table_b, b_naf[idx]);
r = t.as_projective(); j = idx; }
ABSTRACTION: `dsmFold aD bD pA pB P j` — process digits j1 … 0 onto the
accumulator P over the abstract Edwards addition: each iteration doubles
and applies the two digits through `edDigit` (DsmStepSpec). The loop
invariant is
ProjValid r ∧ OnCurve (projPt r) ∧ result = dsmFold … (projPt r) j
proven by induction on j.val, one symbolic body-walk per induction step:
proj_double_law → dsm_step_p_law → dsm_step_b_law → compl_as_projective_law.
Digit hypotheses (odd-or-zero, |d| < 16, for all 256 positions of both
arrays) are exactly what the NAF spec will provide (layering). Table
hypotheses are naf_table_spec's post for both tables.
────────────────────────────────────────────────────────────────────────────── -/
import Proofs.DsmStepSpec
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
/-! ### The abstract double-and-add fold -/
/-- The k-th NAF digit of an extracted digit array, as an integer. -/
def nafDigit (a : Std.Array Std.I8 256#usize) (k : ) : := (a.val[k]!).val
/-- `dsmFold aD bD pA pB P j`: apply digits j1, j2, …, 0 to accumulator P —
per digit: double, add aD's digit-multiple of pA, add bD's digit-multiple
of pB (all through `edDigit`, over the abstract `edAdd`). -/
noncomputable def dsmFold (aD bD : ) (pA pB : Fp × Fp) :
(Fp × Fp) → → Fp × Fp
| P, 0 => P
| P, j + 1 => dsmFold aD bD pA pB
(edDigit pB (bD j) (edDigit pA (aD j) (edAdd P P))) j
@[simp] theorem dsmFold_zero (aD bD : ) (pA pB P : Fp × Fp) :
dsmFold aD bD pA pB P 0 = P := rfl
theorem dsmFold_succ (aD bD : ) (pA pB P : Fp × Fp) (j : ) :
dsmFold aD bD pA pB P (j + 1) = dsmFold aD bD pA pB
(edDigit pB (bD j) (edDigit pA (aD j) (edAdd P P))) j := rfl
/- The neutral point is on the curve: `onCurve_id` (EdCurve.lean). -/
/-- Digit-array contract: every position is a NAF digit — odd or zero,
magnitude below 16. (The NAF spec provides this; layering.) -/
def NafDigits (a : Std.Array Std.I8 256#usize) : Prop :=
∀ k : , k < 256 →
(nafDigit a k = 0 nafDigit a k % 2 = 1) ∧
-16 < nafDigit a k ∧ nafDigit a k < 16
/-- Table contract: the 8 entries are proven caches of the odd multiples
of A (naf_table_spec's post, bundled). -/
def NafTableOf
(tbl : window.NafLookupTable5 backend.serial.curve_models.ProjectiveNielsPoint)
(A : EdPoint) : Prop :=
∃ e0 e1 e2 e3 e4 e5 e6 e7,
tblEntries tbl = [e0, e1, e2, e3, e4, e5, e6, e7] ∧
NafEntryOf e0 A 0 ∧ NafEntryOf e1 A 1 ∧ NafEntryOf e2 A 2 ∧
NafEntryOf e3 A 3 ∧ NafEntryOf e4 A 4 ∧ NafEntryOf e5 A 5 ∧
NafEntryOf e6 A 6 ∧ NafEntryOf e7 A 7
/-! ### The loop induction -/
/-- **The Straus loop body, by induction on the counter.** From any valid
on-curve accumulator r at counter j ≤ 256, the loop returns a valid
on-curve point denoting `dsmFold` of the remaining digits applied to
(projX r, projY r). One symbolic walk per induction step. -/
theorem dsm_loop_loop_spec
(a_naf b_naf : Std.Array Std.I8 256#usize)
(ta tb : window.NafLookupTable5 backend.serial.curve_models.ProjectiveNielsPoint)
(A B : EdPoint)
(hta : NafTableOf ta A) (htb : NafTableOf tb B)
(hda : NafDigits a_naf) (hdb : NafDigits b_naf)
(n : ) :
∀ (j : Usize) (r : ProjPoint), j.val = n → n ≤ 256 →
ProjValid r → OnCurve (projX r) (projY r) →
backend.serial.scalar_mul.vartime_double_base.dsm_loop_loop
a_naf b_naf ta tb r j ⦃ res =>
ProjValid res ∧ OnCurve (projX res) (projY res) ∧
(projX res, projY res) = dsmFold (nafDigit a_naf) (nafDigit b_naf)
(edPt A) (edPt B) (projX r, projY r) n ⦄ := by
obtain ⟨eA0, eA1, eA2, eA3, eA4, eA5, eA6, eA7, hlA,
hA0, hA1, hA2, hA3, hA4, hA5, hA6, hA7⟩ := hta
obtain ⟨eB0, eB1, eB2, eB3, eB4, eB5, eB6, eB7, hlB,
hB0, hB1, hB2, hB3, hB4, hB5, hB6, hB7⟩ := htb
induction n with
| zero =>
intro j r hj hle hv hc
unfold backend.serial.scalar_mul.vartime_double_base.dsm_loop_loop
apply loop_step
simp only [backend.serial.scalar_mul.vartime_double_base.dsm_loop_loop.body]
have hj0 : ¬ (j > 0#usize) := by clear * - hj; scalar_tac
rw [if_neg hj0]
try simp only [spec_ok]
exact ⟨hv, hc, rfl⟩
| succ n ih =>
intro j r hj hle hv hc
unfold backend.serial.scalar_mul.vartime_double_base.dsm_loop_loop
apply loop_step
simp only [backend.serial.scalar_mul.vartime_double_base.dsm_loop_loop.body]
have hjpos : j > 0#usize := by clear * - hj; scalar_tac
rw [if_pos hjpos]
-- idx ← j 1
step as ⟨idx, hidx⟩
have hidxv : idx.val = n := by clear * - hidx hj; scalar_tac
-- t0 ← r.double() (the projective doubling law)
step with (proj_double_law r hv hc) as
⟨t0, t0bX, t0bY, t0bZ, t0bT, t0z, t0t, t0x, t0y⟩
have hct0 : OnCurve (complX t0) (complY t0) := by
rw [t0x, t0y]
exact edAdd_closure hc hc
-- da ← a_naf[idx]
step as ⟨da, hdav⟩
have hban : n < (↑a_naf : List Std.I8).length := by
clear * - hle; scalar_tac
have hdaq : da.val = nafDigit a_naf n := by
rw [hdav]
simp only [nafDigit, hidxv, getElem!_pos (↑a_naf : List Std.I8) n hban]
obtain ⟨hdA1, hdA2, hdA3⟩ := hda n (by omega)
-- t1 ← dsm_step_p t0 ta da
step with (dsm_step_p_law t0 ta da A eA0 eA1 eA2 eA3 eA4 eA5 eA6 eA7 hlA
hA0 hA1 hA2 hA3 hA4 hA5 hA6 hA7 t0bX t0bY t0bZ t0bT t0z t0t hct0
(by clear * - hdA1 hdaq; omega)
(by clear * - hdA2 hdaq; omega) (by clear * - hdA3 hdaq; omega)) as
⟨t1, t1bX, t1bY, t1bZ, t1bT, t1z, t1t, hct1, ht1⟩
-- db ← b_naf[idx]
step as ⟨db, hdbv⟩
have hbbn : n < (↑b_naf : List Std.I8).length := by
clear * - hle; scalar_tac
have hdbq : db.val = nafDigit b_naf n := by
rw [hdbv]
simp only [nafDigit, hidxv, getElem!_pos (↑b_naf : List Std.I8) n hbbn]
obtain ⟨hdB1, hdB2, hdB3⟩ := hdb n (by omega)
-- t2 ← dsm_step_b t1 tb db
step with (dsm_step_b_law t1 tb db B eB0 eB1 eB2 eB3 eB4 eB5 eB6 eB7 hlB
hB0 hB1 hB2 hB3 hB4 hB5 hB6 hB7 t1bX t1bY t1bZ t1bT t1z t1t hct1
(by clear * - hdB1 hdbq; omega) (by clear * - hdB2 hdbq; omega)
(by clear * - hdB3 hdbq; omega)) as
⟨t2, t2bX, t2bY, t2bZ, t2bT, t2z, t2t, hct2, ht2⟩
-- r1 ← t2.as_projective()
step with (compl_as_projective_law t2 t2bX t2bY t2bZ t2bT t2z t2t) as
⟨r1, hr1v, hr1x, hr1y⟩
have hcr1 : OnCurve (projX r1) (projY r1) := by
rw [hr1x, hr1y]; exact hct2
-- the digit chain: (projPt r1) = edDigit_B (edDigit_A (edAdd P P))
have hchain : (projX r1, projY r1)
= edDigit (edPt B) (nafDigit b_naf n)
(edDigit (edPt A) (nafDigit a_naf n)
(edAdd (projX r, projY r) (projX r, projY r))) := by
have e1 : (complX t0, complY t0)
= edAdd (projX r, projY r) (projX r, projY r) := by
rw [Prod.ext_iff]; exact ⟨t0x, t0y⟩
have e2 : (projX r1, projY r1) = (complX t2, complY t2) := by
rw [Prod.ext_iff]; exact ⟨hr1x, hr1y⟩
rw [e2, ht2, hdbq, ht1, hdaq, e1]
try simp only [spec_ok]
-- close with the induction hypothesis at counter idx (= n)
apply spec_mono (ih idx r1 hidxv (by omega) hr1v hcr1)
intro res ⟨hv', hc', heq⟩
refine ⟨hv', hc', ?_⟩
rw [heq, hchain, dsmFold_succ]
/-! ### The top-index constant and the public loop wrapper -/
/-- `dsm_top_index` is the constant 255 (leading zero digits are identity
doublings — the source patch starts every walk at bit 255). -/
theorem dsm_top_index_spec (a b : Std.Array Std.I8 256#usize) :
backend.serial.scalar_mul.vartime_double_base.dsm_top_index a b
⦃ r => r = 255#usize ⦄ := by
unfold backend.serial.scalar_mul.vartime_double_base.dsm_top_index
simp [spec_ok]
/-- **The full Straus loop**: from the identity, process all 256 digit
positions — the result denotes `dsmFold … edId 256`. -/
theorem dsm_loop_spec (i : Usize) (hi : i.val = 255)
(a_naf b_naf : Std.Array Std.I8 256#usize)
(ta tb : window.NafLookupTable5 backend.serial.curve_models.ProjectiveNielsPoint)
(A B : EdPoint)
(hta : NafTableOf ta A) (htb : NafTableOf tb B)
(hda : NafDigits a_naf) (hdb : NafDigits b_naf) :
backend.serial.scalar_mul.vartime_double_base.dsm_loop
i a_naf b_naf ta tb ⦃ res =>
ProjValid res ∧ OnCurve (projX res) (projY res) ∧
(projX res, projY res) = dsmFold (nafDigit a_naf) (nafDigit b_naf)
(edPt A) (edPt B) edId 256 ⦄ := by
obtain ⟨P0, hP0ok, hP0v, hP0x, hP0y⟩ := run_projective_identity
unfold backend.serial.scalar_mul.vartime_double_base.dsm_loop
rw [hP0ok]
simp only [bind_tc_ok]
have hcP0 : OnCurve (projX P0) (projY P0) := by
rw [hP0x, hP0y]; exact onCurve_id
-- j ← i + 1 (= 256)
step as ⟨j, hj⟩
have hjv : j.val = 256 := by clear * - hj hi; scalar_tac
apply spec_mono (dsm_loop_loop_spec a_naf b_naf ta tb A B hta htb hda hdb
256 j P0 hjv (by omega) hP0v hcP0)
intro res ⟨hv, hc, heq⟩
refine ⟨hv, hc, ?_⟩
rw [heq, hP0x, hP0y]
rfl
end CurveFieldProofs