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

313 lines
16 KiB
Text
Raw Normal View History

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