anza-ed25519-verified/verification/Proofs/ConstSpecs.lean
mrwulf 7018fdc3a1 field layer: 14 proofs pass, fieldImplementation axiom-clean
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:42:46 +02:00

190 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/ConstSpecs.lean — the precomputed field constants denote what they claim
WHAT THIS FILE CONTAINS
Specs for the transpiled field constants:
`ZERO`, `ONE`, `MINUS_ONE` (FieldElement51) and `SQRT_M1` (constants).
Each constant evaluates totally (no panic), satisfies the limb-bound
invariant `Bnd`, and denotes the expected element of 𝔽_p.
RUST ANALOG
- `FieldElement51::ZERO = from_limbs([0,0,0,0,0])`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:263
- `FieldElement51::ONE = from_limbs([1,0,0,0,0])`, field.rs:265
- `FieldElement51::MINUS_ONE = from_limbs([2251799813685228, 2251799813685247, ...])`,
field.rs:267-273
- `constants::SQRT_M1 = from_limbs([1718705420411056, ...])`,
curve25519/solana-ed25519/src/backend/serial/u64/constants.rs:99-105
("Precomputed value of one of the square roots of -1 (mod p)").
Transpiled in gen/CurveField/Funs.lean as `...FieldElement51.{ZERO,ONE,MINUS_ONE}`
and `backend.serial.u64.constants.SQRT_M1` (each a `Result Fe` — in the Aeneas
model a Rust `const` is a 0-argument fallible computation that we must prove `ok`).
THE MATH (what each limb vector denotes; recall the radix-2⁵¹ denotation
feVal a = a0 + a1·2⁵¹ + a2·2¹⁰² + a3·2¹⁵³ + a4·2²⁰⁴, ⟪a⟫ = feVal a mod p)
- ZERO: feVal = 0, ⟪·⟫ = 0.
- ONE: feVal = 1, ⟪·⟫ = 1.
- MINUS_ONE: limbs are [2⁵¹20, 2⁵¹1, 2⁵¹1, 2⁵¹1, 2⁵¹1], the radix-2⁵¹
spelling of p 1 = 2²⁵⁵ 20; so feVal = p 1 and ⟪·⟫ = 1.
- SQRT_M1: feVal = N := 19681161376707505956807079304988542015446066515923890162744021073123829784752,
a 255-bit number with N² ≡ 1 (mod p); checked by computing the ~510-bit
square N² and reducing mod p with kernel-verified literal arithmetic
(norm_num) — no decision procedure or native code is trusted for this.
ROLE IN THE MAIN THEOREM
`fieldImplementation` (Proofs/FieldMain.lean) needs distinguished elements 0 and 1
realized by the implementation: FieldMain obtains them from `zero_spec`/`one_spec`
(see its `spec_exists zero_spec` / `spec_exists one_spec`) and derives
impl_zero_add, impl_one_mul, impl_zero_ne_one. `minus_one_spec` and `sqrt_m1_spec`
validate the remaining precomputed constants of the extracted module — `SQRT_M1`
is the constant on which Ed25519 point decompression (`sqrt_ratio_i`) relies, so
a wrong table entry here would be a real-world key-validation bug; the spec proves
the table entry correct.
FILE RELATIONS
Imports Proofs/Denote.lean (denotation + Bnd; no other spec files needed since
constants run no arithmetic beyond `from_limbs`). Imported by Proofs/Field.lean,
hence by FieldMain.lean.
────────────────────────────────────────────────────────────────────────────── -/
import Proofs.Denote
open Aeneas Aeneas.Std Result
open curve25519
namespace CurveFieldProofs
/-! ## Casting `P - 1` into 𝔽_p gives `-1` -/
/-- No Rust analog — arithmetic helper.
MATH: ((p - 1 : N) : F_p) = -1 (since p ≡ 0 in F_p, p 1 ≡ 1).
The proof moves the -subtraction through the cast (legal because 1 ≤ p),
rewrites (p : F_p) = 0, and finishes by ring.
WHY NEEDED: both MINUS_ONE and SQRT_M1 reduce to a feVal equal to p 1
(directly, resp. after squaring mod p); this lemma converts that value into
the field element 1 in their specs. -/
theorem natCast_P_sub_one : ((P - 1 : ) : Fp) = -1 := by
-- 1 ≤ p lets Nat.cast_sub distribute the truncated subtraction
have h1 : (1 : ) ≤ P := by norm_num [P]
rw [Nat.cast_sub h1, ZMod.natCast_self]
push_cast
ring
/-! ## ZERO -/
/-- Rust: `FieldElement51::ZERO`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:263.
MATH: ZERO = ok z with limbs [0,0,0,0,0], Bnd(z, 2^51), [[z]] = 0 in F_p.
(The limb list is exposed verbatim because downstream proofs also need the
syntactic limbs, e.g. to feed `add_spec`.)
WHY NEEDED: provides the implementation's additive identity for
`fieldImplementation` (FieldMain's impl_zero_add / impl_zero_ne_one start from
`spec_exists zero_spec`). `Bnd z (2^51)` certifies that the constant satisfies
the strictest limb invariant, so it can be fed to any operation. -/
theorem zero_spec :
fe_zero ⦃ z =>
(↑z : List U64) = [0#u64, 0#u64, 0#u64, 0#u64, 0#u64] ∧
Bnd z (2^51) ∧ denote z = 0 ⦄ := by
unfold fe_zero backend.serial.u64.field.FieldElement51.ZERO
backend.serial.u64.field.FieldElement51.from_limbs
-- everything is a literal: simp evaluates Array.repeat to [0,...,0], Bnd to
-- 0 < 2⁵¹, and feVal to 0
simp [Bnd, denote, feVal, limbsVal, Array.repeat, List.replicate]
/-! ## ONE -/
/-- Rust: `FieldElement51::ONE`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:265.
MATH: ONE = ok o with limbs [1,0,0,0,0], Bnd(o, 2^51), [[o]] = 1 in F_p
(feVal = 1 + 0·2⁵¹ + ... = 1).
WHY NEEDED: the implementation's multiplicative identity; FieldMain's
impl_one_mul and impl_zero_ne_one consume it via `spec_exists one_spec`. -/
theorem one_spec : fe_one ⦃ o => Bnd o (2^51) ∧ denote o = 1 ⦄ := by
unfold fe_one backend.serial.u64.field.FieldElement51.ONE
backend.serial.u64.field.FieldElement51.from_limbs
-- literal evaluation: feVal [1,0,0,0,0] = 1, and 1 < 2⁵¹
simp [Bnd, denote, feVal, limbsVal, Array.make]
/-! ## MINUS_ONE -/
/-- Rust: `FieldElement51::MINUS_ONE`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:267-273.
MATH: MINUS_ONE = ok m with Bnd(m, 2^52) and [[m]] = -1 in F_p.
The limbs are
[2251799813685228, 2251799813685247, 2251799813685247,
2251799813685247, 2251799813685247]
= [2^51 - 20, 2^51 - 1, 2^51 - 1, 2^51 - 1, 2^51 - 1],
the radix-2⁵¹ "borrowed" spelling of p 1 (cf. sixteen_p in SubNegSpec.lean):
(2^51-20) + (2^51-1)·(2^51 + 2^102 + 2^153 + 2^204) = 2^255 - 20 = p - 1,
so feVal m = p 1 and ⟪m⟫ = 1 by `natCast_P_sub_one`.
(Each limb is < 2⁵¹; the bound is stated as the looser 2⁵² used uniformly for
reduced values in this development.)
WHY NEEDED: validates the third hard-coded table constant of the extracted
module: a typo in any of those 16-digit limbs would silently negate nothing.
It also documents that 1 has a canonical bounded representative, the element
`negate`/`sub` effectively build from. -/
theorem minus_one_spec : fe_minus_one ⦃ m => Bnd m (2^52) ∧ denote m = -1 ⦄ := by
unfold fe_minus_one backend.serial.u64.field.FieldElement51.MINUS_ONE
backend.serial.u64.field.FieldElement51.from_limbs
-- simp discharges the Bnd conjunct and evaluates `feVal` to the literal
-- 2²⁵⁵ 20 = P 1; the remaining goal is `(P 1 : 𝔽_p) = -1`.
simp [Bnd, denote, feVal, limbsVal, Array.make]
-- specialize natCast_P_sub_one to the literal that simp produced for P 1
have h := natCast_P_sub_one
have hc : (P - 1 : ) =
57896044618658097711785492504343953926634992332820282019728792003956564819948 := by
norm_num [P]
rw [hc] at h
exact_mod_cast h
/-! ## SQRT_M1 -/
/-- Rust: `constants::SQRT_M1`,
curve25519/solana-ed25519/src/backend/serial/u64/constants.rs:99-105 —
"Precomputed value of one of the square roots of -1 (mod p)" (it exists since
p ≡ 1 mod 4).
MATH: SQRT_M1 = ok s with Bnd(s, 2^52) and [[s]] * [[s]] = -1 in F_p.
The limbs [1718705420411056, 234908883556509, 2233514472574048,
2117202627021982, 765476049583133] denote the 255-bit number
N = 19681161376707505956807079304988542015446066515923890162744021073123829784752
and the spec certifies N² ≡ 1 (mod p). The verification is brute literal
arithmetic: `norm_num` makes the kernel compute the ~510-bit square N² and its
remainder mod p, equal to p 1 (lemma `hmod` below), then `natCast_P_sub_one`
converts p 1 to 1. (Each limb is < 2⁵¹; stated at the uniform 2⁵² bound.)
WHY NEEDED: `sqrt_ratio_i` — the square-root routine behind Ed25519 point
decompression — multiplies by this table constant whenever the candidate root
fails its sign/QR check; a corrupted constant would make decompression accept or
produce wrong points. This spec pins the precomputed table entry to its defining
equation, complementing the operation-level proofs that feed FieldMain. -/
theorem sqrt_m1_spec :
backend.serial.u64.constants.SQRT_M1 ⦃ s => Bnd s (2^52) ∧ denote s * denote s = -1 ⦄ := by
unfold backend.serial.u64.constants.SQRT_M1
backend.serial.u64.field.FieldElement51.from_limbs
-- simp discharges the Bnd conjunct and evaluates `feVal` to the literal N
-- (the 255-bit value of the SQRT_M1 limbs); the remaining goal is
-- `(N : 𝔽_p) * (N : 𝔽_p) = -1`.
simp [Bnd, denote, feVal, limbsVal, Array.make]
-- N² ≡ P 1 (mod P), checked by literal arithmetic on .
-- (norm_num evaluates the 510-bit product and the division by P inside the kernel)
have hmod :
((19681161376707505956807079304988542015446066515923890162744021073123829784752 *
19681161376707505956807079304988542015446066515923890162744021073123829784752 : ))
% P = P - 1 := by
norm_num [P]
-- assemble: (N:Fp)·(N:Fp) = (N·N : Fp) = ((N·N mod P) : Fp) = ((P1) : Fp) = 1
have key :
((19681161376707505956807079304988542015446066515923890162744021073123829784752 : ) : Fp) *
((19681161376707505956807079304988542015446066515923890162744021073123829784752 : ) : Fp)
= -1 := by
rw [← Nat.cast_mul, ← ZMod.natCast_mod, hmod, natCast_P_sub_one]
exact_mod_cast key
end CurveFieldProofs