betrusted-ed25519-verified/verification/Proofs/Square2Spec.lean

289 lines
15 KiB
Text
Raw Permalink Normal View History

/- ─────────────────────────────────────────────────────────────────────────────
Proofs/Square2Spec.lean — total correctness of `square2` (compute 2·a²)
WHAT THIS FILE PROVES
`square2_spec` (and its step-friendly wrapper `square2_spec'`):
ASCII: Bnd(a, 2^54) ==> square2 a = ok r with
Bnd(r, 2^53) and [[r]] = 2 * ([[a]] * [[a]])
LaTeX: $\mathrm{Bnd}(a,2^{54}) \Rightarrow
\llbracket \mathrm{square2}(a)\rrbracket
= 2\,\llbracket a\rrbracket^2$
where Bnd/[[·]] = ⟪·⟫ are the invariant and denotation of Proofs/Denote.lean
and p = 2^255 - 19. As everywhere, `f x ⦃ post ⦄` is TOTAL correctness:
no panic/overflow — in particular the five u64 `*= 2` multiplications of the
doubling loop are PROVED in range, not assumed.
RUST ANALOG
`FieldElement51::square2`, curve25519/solana-ed25519/src/backend/serial/u64/
field.rs:566-573:
let mut square = self.pow2k(1);
for i in 0..5 { square.0[i] *= 2; }
square
i.e. one radix-2^51 squaring (pow2k with k = 1, verified in
Proofs/SquareSpec.lean) followed by a limbwise doubling — point doubling
(`ProjectivePoint::double`, curve_models.rs:381-397) calls it for 2·Z².
Charon splits the Rust `for` loop into two generated items in
gen/CurveField/Funs.lean (same shape as `add_assign`'s loop in
Proofs/AddSpec.lean):
* `…FieldElement51.square2_loop.body (iter, square)` — ONE iteration,
returning a `ControlFlow` value: it calls `Iterator::next` on the
`Range<usize>` iterator and either answers `.done square` (range
exhausted) or doubles limb i (`Array.index_usize`, `* 2#u64`,
`Array.update`) and answers `.cont (iter1, a)`;
* `…FieldElement51.square2_loop` — `Aeneas.Std.loop` applied to that body;
and `…FieldElement51.square2` itself is `pow2k self 1#u32` bound into the
loop started at the literal range { start := 0, end := 5 }.
(No `fe_square2` alias exists in Proofs/Denote.lean; we use the full
generated name throughout — Denote.lean is not modified.)
WHY THE DOUBLING LOOP CANNOT OVERFLOW
`pow2k` outputs limbs < 2^51 + 2^13 (pow2k_spec, Proofs/SquareSpec.lean), so
each u64 product limb·2 is < 2^52 + 2^14 < 2^64: the `*= 2` (a genuine U64
multiplication in the generated code, `i1 * 2#u64`) is always in range.
The output limbs are < 2^52 + 2^14 ≤ 2^53, which is the (comfortable) bound
we expose — still strictly below the 2^54 input invariant of mul/sub/square,
so square2's result can feed any downstream field op directly.
PROOF ARCHITECTURE
Exactly the AddSpec playbook for the one other `for i in 0..5` loop in this
crate, specialized to one array instead of two:
* `square2_loop_spec` unrolls the loop 5-fold by hand: 5 × (apply
`loop_step` (Proofs/AddSpec.lean), substitute the generated body, run
`range_next_lt_spec` + index/mul/update steps — the mul's side condition
is closed from the < 2^63 limb hypothesis) and a 6th `loop_step` where
`range_next_ge_spec` (5 ≥ 5) makes the body return `done`. The result
array is the input overwritten at 0..4 with the doubled limbs;
collapsing the five set operations (`Array.set_val_eq`) gives the
limb-exact postcondition r_i = 2·s_i.
* `square2_spec` chains pow2k_spec (k = 1, reused as-is via `spec_bind`)
into the loop spec (`spec_mono`), then repackages:
- bound: r_i = 2·s_i < 2·(2^51 + 2^13) ≤ 2^53 (omega);
- value: feVal r = 2·feVal s EXACTLY over (the doubling is linear
in the limbs — omega), cast into 𝔽_p, then
⟪r⟫ = 2·⟪s⟫ = 2·⟪a⟫^(2^1) = 2·(⟪a⟫·⟪a⟫) — the pow2k exponent 2^1 is
bridged to the product form by `ring`, as in square_spec.
* `square2_spec'` is the `@[step]`-registered wrapper with the limbs
hidden (destructured internally via `Fe.exists_limbs`), so the `step` /
`let*` machinery of downstream proofs (e.g. point doubling) can consume
square2 in one step.
ROLE IN THE MAIN THEOREM
Not a field axiom itself: square2 is an EdDSA-level optimization
(2·Z² in `ProjectivePoint::double` saves one full mul). Verifying it here,
with the same invariant discipline as the eleven core ops, makes the
point-doubling code symbolically executable later.
Imports: Proofs/SquareSpec (pow2k_spec; transitively MulSpec's architecture
and AddSpec's loop_step / range_next_*_spec machinery).
───────────────────────────────────────────────────────────────────────── -/
import Proofs.SquareSpec
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 4000000
set_option maxRecDepth 8000
set_option linter.unusedSimpArgs false
namespace CurveFieldProofs
-- the weakest-precondition layer: spec_mono / spec_bind / spec_ok used below
open Aeneas.Std.WP
/-- Limb-level spec for the doubling loop of `square2`: total (no u64
overflow) when all input limbs are < 2⁶³, and the output limbs are
exactly the doubled input limbs.
Rust: the `for i in 0..5 { square.0[i] *= 2; }` loop of
`FieldElement51::square2`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:568-570
(generated as `…FieldElement51.square2_loop` = `Aeneas.Std.loop` applied
to `…square2_loop.body`, started at the range { start := 0, end := 5 }).
MATH:
ASCII: forall s : Fe, (s_i < 2^63 for i = 0..4) ==>
square2_loop {0, 5} s = ok r with r_i = 2 * s_i for all i.
LaTeX: $\forall i,\ s_i < 2^{63} \Rightarrow \exists r,\
\mathrm{loop}(s) = \mathrm{ok}\ r \wedge \forall i,\ r_i = 2 s_i$.
The hypothesis is exactly the panic condition of the Rust `*= 2` on u64
(s_i·2 < 2^64 iff s_i < 2^63); the caller instantiates it with the much
stronger pow2k output bound 2^51 + 2^13.
WHY NEEDED: the strongest (limb-exact) description of the loop, consumed
by `square2_spec` below; as in AddSpec, keeping the 6-fold `loop_step`
unrolling separate from the feVal/Bnd repackaging keeps both readable. -/
theorem square2_loop_spec (s : Fe) (s0 s1 s2 s3 s4 : U64)
(hs : (↑s : List U64) = [s0, s1, s2, s3, s4])
(hbnd : s0.val < 2^63 ∧ s1.val < 2^63 ∧ s2.val < 2^63 ∧
s3.val < 2^63 ∧ s4.val < 2^63) :
backend.serial.u64.field.FieldElement51.square2_loop
{ start := 0#usize, «end» := 5#usize } s
⦃ r => ∃ r0 r1 r2 r3 r4 : U64,
(↑r : List U64) = [r0, r1, r2, r3, r4] ∧
r0.val = 2 * s0.val ∧ r1.val = 2 * s1.val ∧ r2.val = 2 * s2.val ∧
r3.val = 2 * s3.val ∧ r4.val = 2 * s4.val ⦄ := by
obtain ⟨hbnd0, hbnd1, hbnd2, hbnd3, hbnd4⟩ := hbnd
-- expose the loop combinator (gen/CurveField/Funs.lean)
unfold backend.serial.u64.field.FieldElement51.square2_loop
-- Iteration 1 (i = 0)
-- Pattern repeated for each of the 5 iterations (cf. add_limbs_spec):
-- loop_step — peel one iteration of the loop combinator,
-- simp only [..body] — substitute the loop body's definition,
-- step with range_next_lt_spec — Iterator::next yields some i, range
-- advances by one,
-- step — read square[i] (x_k),
-- step — u64 multiply by 2#u64; its overflow side
-- condition is closed by hbnd_i (x_k < 2^63),
-- step — write the doubled limb back (array update t_k),
-- spec_ok — the body returns `cont` with the updated state.
-- hv_k records v_k = s_k · 2; hd_k records the updated array for the next
-- round's get-after-set bookkeeping.
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o1, iter1, ho1, hs1, he1⟩
simp only [ho1]
step as ⟨x1, hx1⟩
simp [hs] at hx1
step as ⟨v0, hv0⟩
rw [hx1] at hv0
step as ⟨t1, hd1⟩
try simp only [spec_ok]
-- Iteration 2 (i = 1) — the simp at hx2 additionally rewrites through
-- iteration 1's array update (hd1 + Array.set_val_eq: get-after-set) so the
-- read still refers to the ORIGINAL limb s1.
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o2, iter2, ho2, hs2, he2⟩
simp only [ho2]
step as ⟨x2, hx2⟩
simp [hd1, Array.set_val_eq, hs, hs1, he1] at hx2
step as ⟨v1, hv1⟩
rw [hx2] at hv1
step as ⟨t2, hd2⟩
try simp only [spec_ok]
-- Iteration 3 (i = 2)
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o3, iter3, ho3, hs3, he3⟩
simp only [ho3]
step as ⟨x3, hx3⟩
simp [hd1, hd2, Array.set_val_eq, hs, hs1, he1, hs2, he2] at hx3
step as ⟨v2, hv2⟩
rw [hx3] at hv2
step as ⟨t3, hd3⟩
try simp only [spec_ok]
-- Iteration 4 (i = 3)
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o4, iter4, ho4, hs4, he4⟩
simp only [ho4]
step as ⟨x4, hx4⟩
simp [hd1, hd2, hd3, Array.set_val_eq, hs, hs1, he1, hs2, he2, hs3, he3] at hx4
step as ⟨v3, hv3⟩
rw [hx4] at hv3
step as ⟨t4, hd4⟩
try simp only [spec_ok]
-- Iteration 5 (i = 4)
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o5, iter5, ho5, hs5, he5⟩
simp only [ho5]
step as ⟨x5, hx5⟩
simp [hd1, hd2, hd3, hd4, Array.set_val_eq, hs, hs1, he1, hs2, he2, hs3, he3,
hs4, he4] at hx5
step as ⟨v4, hv4⟩
rw [hx5] at hv4
step as ⟨t5, hd5⟩
try simp only [spec_ok]
-- Iteration 6 (range exhausted: 5 ≥ 5) — next returns none, body answers
-- `done` with the accumulated element
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_ge_spec as ⟨o6, iter6, ho6, hr6⟩
simp only [ho6]
try simp only [spec_ok]
-- Final: exhibit the limbs — the result array is s's array overwritten at
-- 0..4 with v0..v4; collapsing the five set operations (Array.set_val_eq)
-- gives [v0,...,v4]; each value equation v_k = 2·s_k is hv_k (scalar_tac
-- normalizes the (2#u64).val literal and the multiplication order).
refine ⟨v0, v1, v2, v3, v4, ?_, by scalar_tac, by scalar_tac, by scalar_tac,
by scalar_tac, by scalar_tac⟩
simp [hd1, hd2, hd3, hd4, hd5, Array.set_val_eq, hs, hs1, hs2, hs3, hs4]
/-- Main spec for `square2`: under the 2⁵⁴ invariant, no panic, output limbs
< 2⁵³, and the denotation is twice the square in 𝔽_p.
Rust: `FieldElement51::square2`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:566-573
(`pow2k(1)` followed by `for i in 0..5 { square.0[i] *= 2; }`).
MATH (ASCII): Bnd(a, 2^54) ==> square2 a = ok r with
Bnd(r, 2^53) and [[r]] = 2 * ([[a]] * [[a]]).
LaTeX: $\llbracket r\rrbracket = 2\,\llbracket a\rrbracket^2$.
PROOF: chain pow2k_spec at k = 1 (Proofs/SquareSpec.lean) — yielding s
with Bnd(s, 2^51 + 2^13) and ⟪s⟫ = ⟪a⟫^(2^1) — into `square2_loop_spec`
(2^51 + 2^13 < 2^63 keeps every `*= 2` in u64 range). The doubled limbs
give, EXACTLY over , feVal r = 2·feVal s (the radix-2^51 value is linear
in the limbs), hence ⟪r⟫ = 2·⟪s⟫ after the cast into 𝔽_p; the exponent
bridge ⟪a⟫^(2^1) = ⟪a⟫·⟪a⟫ is `ring`, exactly as in square_spec. Bounds:
r_i = 2·s_i < 2^52 + 2^14 ≤ 2^53 — comfortably re-usable, since every
downstream op only asks for < 2^54.
WHY NEEDED: 2·Z² in point doubling (`ProjectivePoint::double`,
curve_models.rs:381-397, generated right below square2 in Funs.lean). -/
theorem square2_spec (a : Fe) (x0 x1 x2 x3 x4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hba : Bnd a (2^54)) :
backend.serial.u64.field.FieldElement51.square2 a
⦃ r => Bnd r (2^53) ∧ ⟪r⟫ = 2 * (⟪a⟫ * ⟪a⟫) ⦄ := by
unfold backend.serial.u64.field.FieldElement51.square2
-- run pow2k at the literal 1#u32 (its debug_assert!(k > 0) is discharged
-- inside pow2k_spec); s carries Bnd(s, 2^51+2^13) and ⟪s⟫ = ⟪a⟫^(2^1)
apply spec_bind (pow2k_spec a 1#u32 x0 x1 x2 x3 x4 ha hba (by scalar_tac))
rintro s ⟨hbs, hvs⟩
-- name the limbs of the squared element and turn its invariant into the
-- five explicit inequalities s_i < 2^51 + 2^13
obtain ⟨s0, s1, s2, s3, s4, hsl⟩ := Fe.exists_limbs s
rw [Bnd_eq s s0 s1 s2 s3 s4 _ hsl] at hbs
-- run the doubling loop: 2^51 + 2^13 < 2^63, so no u64 overflow
apply spec_mono (square2_loop_spec s s0 s1 s2 s3 s4 hsl
⟨by omega, by omega, by omega, by omega, by omega⟩)
rintro r ⟨r0, r1, r2, r3, r4, hrl, h0, h1, h2, h3, h4⟩
-- value over : doubling every limb doubles the radix-2^51 value EXACTLY
have hval : feVal r = 2 * feVal s := by
rw [feVal_eq r r0 r1 r2 r3 r4 hrl, feVal_eq s s0 s1 s2 s3 s4 hsl]
simp only [limbsVal]
omega
refine ⟨?_, ?_⟩
-- bound: r_i = 2·s_i < 2·(2^51 + 2^13) = 2^52 + 2^14 ≤ 2^53
· rw [Bnd_eq r r0 r1 r2 r3 r4 _ hrl]
refine ⟨by omega, by omega, by omega, by omega, by omega⟩
-- denotation: cast the exact equation into 𝔽_p, then bridge ⟪a⟫^(2^1)
-- to ⟪a⟫·⟪a⟫ (cf. square_spec)
· have hr2 : ⟪r⟫ = 2 * ⟪s⟫ := by
simp only [denote]
rw [hval]
push_cast
ring
have h1v : (1#u32).val = 1 := by scalar_tac
rw [hr2, hvs, h1v]
ring
/-- Step-friendly wrapper for `square2_spec`: same statement with the limbs
hidden (no `ha` hypothesis), registered with `@[step]`.
MATH: identical to square2_spec —
Bnd(a, 2^54) ==> square2 a = ok r with Bnd(r, 2^53) and
[[r]] = 2 * ([[a]] * [[a]]).
PROOF: destructure the limbs internally via `Fe.exists_limbs` and apply
`square2_spec`.
WHY NEEDED: the `step`/`let*` machinery matches spec lemmas against the
goal syntactically; a lemma whose hypotheses mention existentially-found
limbs x0..x4 cannot be applied automatically, this one can. Downstream
proofs about point doubling consume square2 through this lemma in one
`let*` step. -/
@[step]
theorem square2_spec' (a : Fe) (hba : Bnd a (2^54)) :
backend.serial.u64.field.FieldElement51.square2 a
⦃ r => Bnd r (2^53) ∧ ⟪r⟫ = 2 * (⟪a⟫ * ⟪a⟫) ⦄ := by
obtain ⟨x0, x1, x2, x3, x4, ha⟩ := Fe.exists_limbs a
exact square2_spec a x0 x1 x2 x3 x4 ha hba
end CurveFieldProofs