mirror of
https://github.com/saymrwulf/risc0-ed25519-verified.git
synced 2026-09-04 20:03:41 +00:00
313 lines
16 KiB
Text
313 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
|