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

191 lines
10 KiB
Text
Raw Normal View History

/- ──────────────────────────────────────────────────────────────────────────────
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_dalek
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