risc0-ed25519-verified/verification/Proofs/Denote.lean
mrwulf 59ab518093 field layer: proofs pass, fieldImplementation axiom-clean
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>
2026-07-02 14:38:52 +02:00

312 lines
16 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/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^(p2) 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 04 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