mirror of
https://github.com/saymrwulf/risc0-ed25519-verified.git
synced 2026-09-04 20:03:41 +00:00
Ported from the locally verified Hermes working copy; FeQ and Square2Spec (dead files in the published replica) now compile and are in the check manifest. Basic.lean (never compiled under v4 Aeneas) removed rather than shipped dead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
312 lines
16 KiB
Text
312 lines
16 KiB
Text
/- ────────────────────────────────────────────────────────────────────────────
|
||
Proofs/Denote.lean — the SEMANTIC FOUNDATION: from machine limbs to 𝔽_p
|
||
────────────────────────────────────────────────────────────────────────────
|
||
|
||
Denotation layer: interpret the transpiled `FieldElement51` into 𝔽_p,
|
||
p = 2²⁵⁵ − 19, and define the limb-bound invariant.
|
||
|
||
BACKGROUND. The Rust crate curve25519/solana-ed25519 (Anza's fork of
|
||
curve25519-dalek) implements arithmetic in the field F_p, p = 2^255 − 19,
|
||
on five radix-2^51 u64 limbs (`FieldElement51`). The files src/field.rs
|
||
and src/backend/serial/u64/field.rs were transpiled MECHANICALLY to Lean 4
|
||
with Charon + Aeneas (Rust → LLBC → Lean): gen/CurveField/Types.lean and
|
||
gen/CurveField/Funs.lean. In that model, machine integers become Aeneas
|
||
types (`U64` = 64-bit unsigned, with `.val : ℕ` its mathematical value) and
|
||
fallible machine arithmetic returns in the `Result` monad (`ok x` =
|
||
success, `fail` = panic/overflow); `x ⦃ post ⦄` asserts total correctness:
|
||
"x succeeds AND its value satisfies post".
|
||
|
||
THIS FILE is where the transpiled machine code first meets mathematics.
|
||
It defines, ABOUT the generated code (never modifying it):
|
||
|
||
• `P`, `Fp` — the prime p = 2²⁵⁵ − 19 and the field 𝔽_p (ZMod P);
|
||
• `Fe`, `fe_*` — short aliases for the transpiled type [u64; 5] and
|
||
its eleven operations;
|
||
• `Fe.exists_limbs`— the destructuring device every proof starts with:
|
||
an `Fe` IS five named u64 limbs a0 … a4;
|
||
• `limbsVal`/`feVal` — the EXACT natural-number value
|
||
a0 + a1·2⁵¹ + a2·2¹⁰² + a3·2¹⁵³ + a4·2²⁰⁴ (no mod);
|
||
• `denote` ⟪·⟫ — THE bridge: ⟪a⟫ = (feVal a) mod p ∈ 𝔽_p. The main
|
||
theorem (Proofs/FieldMain.lean, `fieldImplementation`)
|
||
says every transpiled op is total under the limb
|
||
invariant and implements the 𝔽_p op through this map;
|
||
• `Bnd` — the dalek limb discipline ("all limbs < c"): the
|
||
invariant under which the ops are proven panic-free;
|
||
• `P_pos`, `two_pow_255_eq` — basic facts about p; in particular
|
||
2²⁵⁵ = 19 in 𝔽_p, the single modular identity on
|
||
which all carry / "19-folding" reasoning hangs.
|
||
|
||
RUST CORRESPONDENCE (paths abbreviated in the rest of this file):
|
||
u64/field.rs = curve25519/solana-ed25519/src/backend/serial/u64/field.rs
|
||
field.rs = curve25519/solana-ed25519/src/field.rs
|
||
`Fe` models `pub struct FieldElement51(pub(crate) [u64; 5])`,
|
||
u64/field.rs:43. The radix-2⁵¹ representation and the "coefficients are
|
||
allowed to grow up to 2⁵⁴ between reductions" discipline are the crate's
|
||
own documentation, u64/field.rs:26-42.
|
||
|
||
PLACE IN THE PROOF GRAPH. Imports only the generated model
|
||
(CurveField.Funs). Imported by Proofs/ReduceSpec.lean and
|
||
Proofs/ConstSpecs.lean and, through them, by every other proof file up to
|
||
the main theorem (AddSpec, SubNegSpec, MulSpec, SquareSpec, Field,
|
||
InvertSpec, FieldMain).
|
||
|
||
NOTE: nothing in Proofs/ modifies the transpiled code (gen/CurveField/);
|
||
we only define functions *about* it and prove equivalences. -/
|
||
import CurveField.Funs
|
||
open Aeneas Aeneas.Std Result
|
||
open curve25519_dalek
|
||
|
||
namespace CurveFieldProofs
|
||
|
||
-- ───────────────────────── The prime and the field ─────────────────────────
|
||
|
||
/-- The field characteristic p = 2²⁵⁵ − 19.
|
||
|
||
MATH: P = 2^255 − 19 (a 255-bit number, ℕ-subtraction is exact here).
|
||
P is prime — proved by an axiom-free Lucas/Pratt certificate chain in
|
||
Proofs/P25519.lean; primality is what later makes `Fp` a field and powers
|
||
Fermat's little theorem in the `invert` proof.
|
||
WHY NEEDED: fixes the modulus once and for all; every denotation and
|
||
every operation spec is an equation mod P. -/
|
||
def P : ℕ := 2^255 - 19
|
||
|
||
/-- The mathematical field 𝔽_p (mathlib's `ZMod P`).
|
||
|
||
MATH: Z/pZ, integers modulo p. It is a commutative ring by construction;
|
||
it is known to be a FIELD only once `Fact P.Prime` is in scope
|
||
(instantiated in Proofs/Field.lean from the certificate in P25519.lean).
|
||
WHY NEEDED: the codomain of ⟪·⟫; "the code implements 𝔽_p" is stated as
|
||
equations between elements of this type. -/
|
||
abbrev Fp := ZMod P
|
||
|
||
/-- Short alias for the transpiled field-element type ([u64; 5]).
|
||
|
||
Rust: `pub struct FieldElement51(pub(crate) [u64; 5])`, u64/field.rs:43.
|
||
Aeneas renders `[u64; 5]` as `Array U64 5#usize`: a Lean `List U64`
|
||
bundled with a proof that its length is 5 (exploited by
|
||
`Fe.exists_limbs` below).
|
||
WHY NEEDED: the carrier type of the implementation; every spec in
|
||
Proofs/ quantifies over it. -/
|
||
abbrev Fe := backend.serial.u64.field.FieldElement51
|
||
|
||
/-! Short aliases for the transpiled operations (the Aeneas names are long).
|
||
These are definitionally the generated functions — `rfl`-equal.
|
||
|
||
The Rust source spans cited below are taken verbatim from the generated
|
||
docstrings in gen/CurveField/Funs.lean (Charon records them; they are
|
||
ground truth). Each alias points to the proof file that specifies it. -/
|
||
|
||
/-- Rust: `impl Add<&FieldElement51> for &FieldElement51`, u64/field.rs:68-72
|
||
(limbwise `a[i] + b[i]`, no carry, no reduction — Proofs/AddSpec.lean). -/
|
||
abbrev fe_add :=
|
||
SharedAFieldElement51.Insts.CoreOpsArithAddSharedBFieldElement51FieldElement51.add
|
||
/-- Rust: `impl Sub<&FieldElement51> for &FieldElement51`, u64/field.rs:84-101
|
||
(adds the constant 16p limbwise before subtracting, so u64 subtraction
|
||
cannot underflow; then reduces — Proofs/SubNegSpec.lean). -/
|
||
abbrev fe_sub :=
|
||
SharedAFieldElement51.Insts.CoreOpsArithSubSharedBFieldElement51FieldElement51.sub
|
||
/-- Rust: `impl Mul<&FieldElement51> for &FieldElement51`, u64/field.rs:115-213
|
||
(radix-2⁵¹ schoolbook product, high limbs folded back ×19, u128 carry
|
||
chain — Proofs/MulSpec.lean). -/
|
||
abbrev fe_mul :=
|
||
SharedAFieldElement51.Insts.CoreOpsArithMulSharedBFieldElement51FieldElement51.mul
|
||
/-- Rust: `FieldElement51::negate`, u64/field.rs:276-286
|
||
(computes 16p − a limbwise, then reduces — Proofs/SubNegSpec.lean). -/
|
||
abbrev fe_neg := backend.serial.u64.field.FieldElement51.negate
|
||
/-- Rust: `FieldElement51::reduce`, u64/field.rs:290-323
|
||
(carry chain: each limb keeps its low 51 bits, passes the high bits up;
|
||
the top carry re-enters at limb 0 multiplied by 19 — ReduceSpec.lean). -/
|
||
abbrev fe_reduce := backend.serial.u64.field.FieldElement51.reduce
|
||
/-- Rust: `FieldElement51::square`, u64/field.rs:562-564
|
||
(literally `pow2k(1)` — Proofs/SquareSpec.lean). -/
|
||
abbrev fe_square := backend.serial.u64.field.FieldElement51.square
|
||
/-- Rust: `FieldElement51::pow2k`, u64/field.rs:454-559
|
||
(k-fold squaring, k ≥ 1 enforced by a `debug_assert!` that survives
|
||
translation as a provable `massert` — Proofs/SquareSpec.lean). -/
|
||
abbrev fe_pow2k := backend.serial.u64.field.FieldElement51.pow2k
|
||
/-- Rust: `FieldElement51::invert`, field.rs:239-248
|
||
(x^(p−2) via the pow22501 addition chain; equals x⁻¹ by Fermat's little
|
||
theorem — Proofs/InvertSpec.lean). -/
|
||
abbrev fe_invert := field.FieldElement51.invert
|
||
/-- Rust: `FieldElement51::ZERO`, u64/field.rs:263 (limbs [0,0,0,0,0] —
|
||
Proofs/ConstSpecs.lean). -/
|
||
abbrev fe_zero := backend.serial.u64.field.FieldElement51.ZERO
|
||
/-- Rust: `FieldElement51::ONE`, u64/field.rs:265 (limbs [1,0,0,0,0] —
|
||
Proofs/ConstSpecs.lean). -/
|
||
abbrev fe_one := backend.serial.u64.field.FieldElement51.ONE
|
||
/-- Rust: `FieldElement51::MINUS_ONE`, u64/field.rs:267-273 (the limbs of the
|
||
literal p − 1 — Proofs/ConstSpecs.lean). -/
|
||
abbrev fe_minus_one := backend.serial.u64.field.FieldElement51.MINUS_ONE
|
||
|
||
-- ───────────────────── Destructuring a field element ───────────────────────
|
||
|
||
/-- Every `Fe` is a 5-element list of u64 limbs.
|
||
|
||
MATH: forall a : Fe, exists a0 a1 a2 a3 a4 : U64,
|
||
a = [a0, a1, a2, a3, a4].
|
||
LaTeX: $\forall a,\ \exists a_0\dots a_4,\ a = [a_0,a_1,a_2,a_3,a_4]$.
|
||
|
||
The destructuring device EVERY proof in Proofs/ starts with
|
||
(`obtain ⟨a0, a1, a2, a3, a4, hl⟩ := Fe.exists_limbs a`): `Fe` is a
|
||
length-5 subtype, and the limbs must be given names before any limb
|
||
arithmetic can be stated.
|
||
WHY NEEDED: turns the abstract array into the concrete 5-element shape on
|
||
which `feVal_eq` / `Bnd_eq` and the symbolic execution of the generated
|
||
code (which indexes limbs 0..4) can fire. -/
|
||
theorem Fe.exists_limbs (a : Fe) :
|
||
∃ a0 a1 a2 a3 a4 : U64, (↑a : List U64) = [a0, a1, a2, a3, a4] := by
|
||
-- The subtype carries the proof that the underlying list has length 5.
|
||
have h : (↑a : List U64).length = 5 := by
|
||
have := a.property
|
||
simp_all
|
||
-- Case-split on the shape of the list; only length 5 is consistent with h.
|
||
match hl : (↑a : List U64) with
|
||
| [a0, a1, a2, a3, a4] => exact ⟨a0, a1, a2, a3, a4, rfl⟩
|
||
-- Lists of length 0–4 contradict h …
|
||
| [] | [_] | [_,_] | [_,_,_] | [_,_,_,_] => simp [hl] at h
|
||
-- … as does any list of length ≥ 6.
|
||
| _::_::_::_::_::_::_ => simp [hl] at h
|
||
|
||
-- ─────────────────── Exact ℕ value and the denotation ⟪·⟫ ──────────────────
|
||
|
||
/-- Value of a limb vector as a natural number (radix 2⁵¹).
|
||
|
||
MATH: limbsVal a0 a1 a2 a3 a4
|
||
= a0 + a1·2⁵¹ + a2·2¹⁰² + a3·2¹⁵³ + a4·2²⁰⁴ (in ℕ).
|
||
LaTeX: $\sum_{i=0}^{4} a_i \cdot 2^{51 i}$.
|
||
|
||
This is the EXACT integer value — no `mod p`, no wraparound. All
|
||
overflow/carry accounting in ReduceSpec/AddSpec/SubNegSpec/MulSpec/
|
||
SquareSpec is performed on this ℕ value first (e.g. ReduceSpec proves
|
||
`feVal r + p·(l4 >> 51) = feVal l` exactly); only at the end is the value
|
||
cast into 𝔽_p by `denote`.
|
||
WHY NEEDED: separates the bit-level bookkeeping (ℕ, decided by `omega`/
|
||
`ring`) from the modular reasoning (𝔽_p, one cast at the end). -/
|
||
def limbsVal (a0 a1 a2 a3 a4 : U64) : ℕ :=
|
||
a0.val + 2^51 * a1.val + 2^102 * a2.val + 2^153 * a3.val + 2^204 * a4.val
|
||
|
||
/-- Value of a field element as a natural number.
|
||
|
||
MATH: feVal a = limbsVal a0 a1 a2 a3 a4 where [a0,…,a4] are a's limbs.
|
||
Defined by matching on the underlying list; the `_ => 0` default is dead
|
||
code (by `Fe.exists_limbs` the list always has exactly 5 elements) and
|
||
exists only to make the function total without dependent matching.
|
||
WHY NEEDED: lifts `limbsVal` from named limbs to whole field elements, so
|
||
specs can be stated about an abstract `a : Fe`. -/
|
||
def feVal (a : Fe) : ℕ :=
|
||
match (↑a : List U64) with
|
||
| [a0, a1, a2, a3, a4] => limbsVal a0 a1 a2 a3 a4
|
||
| _ => 0
|
||
|
||
/-- Rewriting lemma: once the limbs of `a` are named (via `Fe.exists_limbs`),
|
||
`feVal a` unfolds to the explicit polynomial `limbsVal a0 a1 a2 a3 a4`.
|
||
Marked `@[simp]` so it fires automatically during proofs.
|
||
WHY NEEDED: the `match` inside `feVal` cannot reduce on an abstract `a`;
|
||
this lemma is the bridge every value computation goes through. -/
|
||
@[simp]
|
||
theorem feVal_eq (a : Fe) (a0 a1 a2 a3 a4 : U64)
|
||
(h : (↑a : List U64) = [a0, a1, a2, a3, a4]) :
|
||
feVal a = limbsVal a0 a1 a2 a3 a4 := by
|
||
simp [feVal, h]
|
||
|
||
/-- The denotation ⟪a⟫ : 𝔽_p of a field element.
|
||
|
||
MATH: ⟪a⟫ = (a0 + a1·2⁵¹ + a2·2¹⁰² + a3·2¹⁵³ + a4·2²⁰⁴) mod p.
|
||
LaTeX: $\llbracket a\rrbracket=\bigl(\sum_i a_i\,2^{51 i}\bigr)\bmod p$
|
||
(the ℕ → `ZMod P` coercion performs the reduction mod p).
|
||
|
||
THE bridge from machine limbs to mathematics. The main theorem
|
||
(Proofs/FieldMain.lean, `fieldImplementation`) is phrased entirely through
|
||
this map: e.g. "fe_mul a b succeeds with result r and ⟪r⟫ = ⟪a⟫ * ⟪b⟫".
|
||
Note the map is total but NOT injective — many limb vectors denote the
|
||
same field element (the representation is redundant), which is exactly
|
||
why "the code is a field" must be stated via ⟪·⟫ (surjectivity + each op
|
||
realizing its 𝔽_p counterpart) rather than as a `Field Fe` instance. -/
|
||
def denote (a : Fe) : Fp := (feVal a : Fp)
|
||
|
||
/- Bracket notation for the denotation. (⟪·⟫ rather than ⟦·⟧, which collides
|
||
with `Quotient.mk`.) -/
|
||
notation "⟪" a "⟫" => denote a
|
||
|
||
-- ───────────────────── The limb-bound invariant (Bnd) ──────────────────────
|
||
|
||
/-- Limb-bound invariant: all limbs < `c`. Operations require/provide:
|
||
`reduce`/`mul`/`square` outputs satisfy `Bnd · (2^52)`;
|
||
`mul`/`square`/`sub`/`neg` inputs require `Bnd · (2^54)`.
|
||
|
||
MATH: Bnd a c iff a_i < c for all i in 0..4.
|
||
|
||
This is the dalek "limb discipline" stated in the crate's own docs
|
||
(u64/field.rs:29-32: radix-2⁵¹ coefficients "are allowed to grow up to
|
||
2⁵⁴ between reductions modulo p"). Each operation's spec has the shape
|
||
|
||
Bnd inputs (2⁵⁴) ==> op succeeds, output Bnd (2⁵²), ⟪·⟫ correct
|
||
|
||
so any output (< 2⁵²) can be fed back as an input (< 2⁵⁴) via `Bnd.mono`,
|
||
closing the composition loop. Like `feVal`, the non-5-element branch
|
||
(`False`) is dead code that keeps the definition total.
|
||
WHY NEEDED: totality (panic-freedom: every u64/u128 add/mul/shift in the
|
||
carry chains stays in range, every `debug_assert` holds) is only true
|
||
under this invariant; it is the hypothesis of every clause of the main
|
||
theorem. -/
|
||
def Bnd (a : Fe) (c : ℕ) : Prop :=
|
||
match (↑a : List U64) with
|
||
| [a0, a1, a2, a3, a4] =>
|
||
a0.val < c ∧ a1.val < c ∧ a2.val < c ∧ a3.val < c ∧ a4.val < c
|
||
| _ => False
|
||
|
||
/-- Rewriting lemma: once the limbs are named, `Bnd a c` unfolds to the five
|
||
explicit inequalities (companion of `feVal_eq`; `@[simp]`).
|
||
WHY NEEDED: the `match` inside `Bnd` cannot reduce on an abstract `a`. -/
|
||
@[simp]
|
||
theorem Bnd_eq (a : Fe) (a0 a1 a2 a3 a4 : U64) (c : ℕ)
|
||
(h : (↑a : List U64) = [a0, a1, a2, a3, a4]) :
|
||
Bnd a c ↔
|
||
(a0.val < c ∧ a1.val < c ∧ a2.val < c ∧ a3.val < c ∧ a4.val < c) := by
|
||
simp [Bnd, h]
|
||
|
||
/-- `Bnd` is monotone in the bound.
|
||
|
||
MATH: Bnd a c and c ≤ c' ==> Bnd a c'.
|
||
WHY NEEDED: glues operation specs together — outputs carry the tight
|
||
bound 2⁵² while the next operation's hypothesis asks for 2⁵⁴ (and the
|
||
main theorem's invariant clauses weaken bounds the same way). -/
|
||
theorem Bnd.mono {a : Fe} {c c' : ℕ} (h : Bnd a c) (hcc : c ≤ c') : Bnd a c' := by
|
||
-- Name the limbs, rewrite both `Bnd`s to the five inequalities …
|
||
obtain ⟨a0, a1, a2, a3, a4, hl⟩ := Fe.exists_limbs a
|
||
rw [Bnd_eq a a0 a1 a2 a3 a4 c hl] at h
|
||
rw [Bnd_eq a a0 a1 a2 a3 a4 c' hl]
|
||
-- … then it is linear arithmetic.
|
||
omega
|
||
|
||
/-! ## Basic facts about P -/
|
||
|
||
/-- MATH: 0 < p (p is a 255-bit number, so certainly positive).
|
||
WHY NEEDED: positivity feeds the ℕ-subtraction and `mod p` lemmas used
|
||
throughout the value-accounting proofs. -/
|
||
theorem P_pos : 0 < P := by norm_num [P]
|
||
|
||
/-- THE modular identity of the whole development: 2²⁵⁵ = 19 in 𝔽_p.
|
||
|
||
MATH: 2^255 ≡ 19 (mod p), because 2^255 − 19 = p ≡ 0 (mod p).
|
||
LaTeX: $2^{255} \equiv 19 \pmod{2^{255}-19}$.
|
||
|
||
Every appearance of the magic constant 19 in the Rust code is justified
|
||
by exactly this identity: a contribution that overflows past bit 255 may
|
||
be folded back into limb 0 multiplied by 19 (the ×19 limb products in
|
||
`mul`/`square`, the re-entering top carry in `reduce`) and the 16p
|
||
constants added by `sub`/`negate` vanish mod p for the same reason.
|
||
WHY NEEDED: invoked, directly or through derived multiple-of-p facts, by
|
||
every correctness proof that moves value across the 2²⁵⁵ boundary. -/
|
||
theorem two_pow_255_eq :
|
||
((2^255 : ℕ) : Fp) = (19 : Fp) := by
|
||
-- p itself vanishes when cast into 𝔽_p …
|
||
have h : ((P : ℕ) : Fp) = 0 := ZMod.natCast_self P
|
||
-- … and over ℕ, 2²⁵⁵ is literally p + 19 (checked numerically).
|
||
have : (2^255 : ℕ) = P + 19 := by norm_num [P]
|
||
rw [this]
|
||
-- Push the cast through the sum; the p-summand is 0, leaving 19.
|
||
push_cast [h]
|
||
ring
|
||
|
||
end CurveFieldProofs
|