anza-ed25519-verified/verification/Proofs/EdCurve.lean
mrwulf 90c019b8d4 group-law layer: complete twisted Edwards addition law proven
Extraction widened to backend::serial::curve_models + edwards (matching the
reference recipe; extra opaque: backend::scalar_fits_in_128_bits — a
post-reference NAF-path helper whose generated code trips an Aeneas
namespace-shadowing wart). Reference Ed* suite compiles UNCHANGED (same
crate namespace). All proofs pass; both certificates axiom-clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:04:25 +02:00

588 lines
30 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/EdCurve.lean — pure mathematics of the curve25519 twisted Edwards curve
(curve constant d, completeness of the addition law,
and the basic point-arithmetic laws over 𝔽_p)
═══════════════════════════════════════════════════════════════════════════════
WHAT THIS FILE PROVES (all in 𝔽_p, p = 2²⁵⁵ 19, over `Fp := ZMod P` from
Proofs/Field.lean — no transpiled code appears here):
* `edD` — the Ed25519 curve constant d = 121665/121666 ∈ 𝔽_p,
with its decode-friendly characterization `edD_char`.
* `OnCurve x y` — the twisted Edwards curve equation with a = 1:
x² + y² = 1 + d·x²·y² (the Ed25519 curve x²+y² =
1 + d x² y², RFC 8032 §5.1).
* `edAdd/edNeg/edId`— the complete twisted Edwards addition law, negation
and neutral element on coordinate pairs.
* `edD_not_square` — d is a quadratic NON-residue mod p. This is the
number-theoretic heart of the file: by Euler's
criterion it reduces to the 255-bit exponentiation
d^((p1)/2) ≡ 1 (mod p), which the Lean KERNEL
checks via a fuel-based `powMod` (the technique of
Proofs/P25519.lean — no `native_decide`, no axioms).
* `completeness` — THE BernsteinLange completeness theorem (BBJLP,
"Twisted Edwards curves" / BernsteinLange "Faster
addition and doubling on elliptic curves", Thm 3.3):
because d is not a square (and 1 IS a square), the
denominators 1 ± d·x₁x₂y₁y₂ of the addition law NEVER
vanish on curve points. Hence `edAdd` is total: no
case distinctions, no exceptional pairs — the property
that makes Edwards form attractive for constant-time
cryptography in the first place.
* `onCurve_id`, `onCurve_neg`, `edAdd_id`, `edAdd_comm`, `edAdd_neg`,
`edAdd_closure` — the mechanical group-operation laws: (0,1) is neutral,
negation stays on the curve, p + (p) = identity,
addition is commutative and CLOSED on the curve.
(Associativity is deliberately out of scope — Tier 2.)
RUST ANALOG (indirect — this file is meta-level mathematics):
curve25519/solana-ed25519/src/backend/serial/u64/constants.rs defines
`EDWARDS_D` (the limb encoding of d = 121665/121666) and `SQRT_M1`;
src/edwards.rs implements point addition in extended coordinates whose
projective denominators are exactly the 1 ± d·x₁x₂y₁y₂ treated here. The
Rust code never checks for exceptional cases — `completeness` is the
mathematical fact that justifies this.
PLACE IN THE IMPORT GRAPH
Imports Proofs.Field (for `P`, `Fp := ZMod P`, the `Fact (Nat.Prime P)` and
`NeZero P` instances) plus mathlib (Euler's criterion). Nothing imports it
yet: it is the Tier-1 pure-mathematics layer for the upcoming verification
of the transpiled twisted Edwards point arithmetic.
PROOF TECHNIQUE, FOR THE LAY READER
1. Big-number facts (d^((p1)/2) = 1, sqrt(1)² = 1, the canonical
residue of d) are stated as closed equations between `Nat` literals and
checked by the KERNEL with `decide`, through the same fuel-based
square-and-multiply `powMod` used by Proofs/P25519.lean. GMP-backed
kernel `Nat` arithmetic makes each check milliseconds.
2. Algebraic identities on the curve are proved with `linear_combination`:
every identity is exhibited as an EXPLICIT polynomial combination of the
two curve equations (the cofactor polynomials below were computed and
verified offline with an exact Gröbner-basis computation; the Lean
`ring` normalizer re-verifies them from scratch, so they are trusted
only as HINTS, not as facts).
3. The completeness argument follows BernsteinLange: if a denominator
1 ± d·x₁x₂y₁y₂ vanished, d = (…/…)² would be a square — contradiction
with `edD_not_square`. The a = 1 twist is handled directly using
i = √1 ∈ 𝔽_p (p ≡ 1 mod 4): the role (x₁ ± y₁)² plays for a = 1 is
played by (i·x₁ ± ε·y₁)² here.
-/
import Proofs.Field
import Mathlib.NumberTheory.LegendreSymbol.Basic
import Mathlib.FieldTheory.Finite.Basic
import Mathlib.Tactic.LinearCombination
import Mathlib.Tactic.FieldSimp
-- Big decimal literals (255-bit numbers) build deep numeral terms during
-- elaboration; same limits as Proofs/P25519.lean and Proofs/Field.lean.
set_option maxHeartbeats 4000000
set_option maxRecDepth 8000
namespace CurveFieldProofs
/-! ## Kernel-checkable modular exponentiation
The technique of Proofs/P25519.lean, recreated here so this file depends only
on the MATHEMATICAL interface of Proofs/Field.lean (`P`, `Fp`, the instances)
and not on the primality certificate's internal helpers: a fuel-based
square-and-multiply on raw `Nat`, with a once-proved correctness lemma. Each
255-bit exponentiation below then becomes a single closed `Nat` equation that
the kernel `decide`s with GMP big-integer arithmetic in milliseconds — no
`native_decide`, no axioms. -/
/-- Fuel-based binary modular exponentiation, kernel-reducible.
MATH (for sufficient fuel; made precise by `powModAux_eq`):
`powModAux fuel a k n = a^k mod n`.
Algorithm: square-and-multiply along the binary digits of `k`, every
intermediate reduced mod n (so nothing exceeds n² ≈ 510 bits here).
WHY THE `fuel` ARGUMENT: structural recursion on `fuel` is what the kernel
can unfold step by step during `decide`; recursion on `k/2 < k` would compile
to `WellFounded.fix`, which the kernel cannot evaluate. -/
def powModAux : Nat → Nat → Nat → Nat → Nat
| 0, _, _, n => 1 % n
| fuel + 1, a, k, n =>
if k = 0 then 1 % n
else if k % 2 = 1 then powModAux fuel (a * a % n) (k / 2) n * a % n
else powModAux fuel (a * a % n) (k / 2) n
/- Correctness of `powModAux`.
MATH (ASCII): forall fuel a k n, k < 2^fuel ==>
powModAux fuel a k n = a^k mod n
The hypothesis `k < 2^fuel` says the fuel covers every binary digit of the
exponent, so the recursion never runs dry.
WHY NEEDED: turns each kernel computation `powMod a k n = …` into the
mathematical statement `a^k % n = …` consumed by the bridge lemmas below.
Proof: induction on `fuel`, mirroring the recursion of `powModAux`. -/
theorem powModAux_eq : ∀ (fuel a k n : ), k < 2 ^ fuel → powModAux fuel a k n = a ^ k % n := by
intro fuel
induction fuel with
| zero =>
-- base case: k < 2^0 = 1 forces k = 0, and both sides reduce to 1 % n
intro a k n hk
rw [pow_zero] at hk
have hk0 : k = 0 := by omega
subst hk0
simp [powModAux]
| succ f ih =>
intro a k n hk
by_cases hk0 : k = 0
-- k = 0: both sides are 1 % n by definition
· subst hk0; simp [powModAux]
-- k ≠ 0: the recursive call gets exponent k/2, which fits in f bits…
· have hk2 : k / 2 < 2 ^ f := by
rw [pow_succ] at hk
omega
-- …so the induction hypothesis describes it: (a²%n)^(k/2) % n
have hrec := ih (a * a % n) (k / 2) n hk2
have haa : a * a = a ^ 2 := (pow_two a).symm
simp only [powModAux, if_neg hk0]
-- split on the lowest bit of k and reassemble the exponent:
by_cases hodd : k % 2 = 1
-- odd k: (a²)^(k/2) · a = a^(2·(k/2)+1) = a^k (mods commute via Nat.pow_mod)
· rw [if_pos hodd, hrec, ← Nat.pow_mod, Nat.mod_mul_mod, haa, ← pow_mul, ← pow_succ,
show 2 * (k / 2) + 1 = k by omega]
-- even k: (a²)^(k/2) = a^(2·(k/2)) = a^k
· rw [if_neg hodd, hrec, ← Nat.pow_mod, haa, ← pow_mul,
show 2 * (k / 2) = k by omega]
/-- `powMod a k n = a ^ k % n` for all `k < 2 ^ 256` — fuel fixed at 256,
enough for every exponent below 2²⁵⁶, in particular for `P / 2` (< 2²⁵⁴).
WHY NEEDED: the single entry point of all kernel computations below. -/
def powMod (a k n : ) : := powModAux 256 a k n
/- Bridge from the `Nat` computation into `ZMod n`.
MATH: k < 2^256 ==> (a : ZMod n)^k = (powMod a k n : ZMod n).
Casting `a` into Z/n and exponentiating there agrees with computing
`a^k mod n` over the naturals and casting the result (the cast is a ring
homomorphism that kills `% n`).
WHY NEEDED: this is how the kernel-checked equation
`powMod dNum (P/2) P = P 1` becomes the field equation
`edD^(P/2) = 1` in `edD_pow_eq_neg_one`. -/
theorem cast_pow_eq (a k n : ) (hk : k < 2 ^ 256) :
(a : ZMod n) ^ k = ((powMod a k n : ) : ZMod n) := by
rw [powMod, powModAux_eq 256 a k n hk, ZMod.natCast_mod, Nat.cast_pow]
/-! ## Casting `Nat` residues into 𝔽_p
Two tiny workhorses: a natural number is 0 in 𝔽_p iff p divides it. Every
kernel-checked congruence below enters the field through these. -/
/- MATH: n % P = 0 ==> (n : F_p) = 0 — multiples of p vanish in 𝔽_p.
WHY NEEDED: imports each kernel-checked `Nat` congruence (a closed `% P`
equation, checked by `decide`) into the field 𝔽_p. -/
theorem natCast_eq_zero_of_mod {n : } (h : n % P = 0) : (n : Fp) = 0 := by
rw [← ZMod.natCast_mod n P, h, Nat.cast_zero]
/- MATH: n % P ≠ 0 ==> (n : F_p) ≠ 0 — non-multiples of p survive in 𝔽_p.
Distinct naturals can collide in Z/p, so the cast-level inequality needs
the residues compared mod p (`ZMod.natCast_eq_natCast_iff'`).
WHY NEEDED: gives the nonvanishing of the small constants 2, 121665,
121666 (each `% P` check is a kernel `decide`: P is huge, they are tiny). -/
theorem natCast_ne_zero_of_mod {n : } (h : n % P ≠ 0) : (n : Fp) ≠ 0 := by
intro hc
apply h
have h0 : ((n : ) : Fp) = ((0 : ) : Fp) := by rw [Nat.cast_zero]; exact hc
have h1 := (ZMod.natCast_eq_natCast_iff' n 0 P).mp h0
rwa [Nat.zero_mod] at h1
/- MATH: (121666 : F_p) ≠ 0. True because 0 < 121666 < P, i.e. P ∤ 121666.
WHY NEEDED: 121666 is the denominator of d — without this, `edD` would be
division by zero and `edD_char`/`dNum_cast` would be vacuous.
(`private`: Proofs/EdDenote.lean exports an identical public lemma of the
same name; importers that need both files use that one. Privacy here
avoids a duplicate-declaration clash without changing any statement.) -/
private theorem c121666_ne_zero : (121666 : Fp) ≠ 0 := by
have h : ((121666 : ) : Fp) ≠ 0 := natCast_ne_zero_of_mod (by decide)
exact_mod_cast h
/- MATH: (121665 : F_p) ≠ 0 (numerator of d). WHY NEEDED: `edD_ne_zero`. -/
theorem c121665_ne_zero : (121665 : Fp) ≠ 0 := by
have h : ((121665 : ) : Fp) ≠ 0 := natCast_ne_zero_of_mod (by decide)
exact_mod_cast h
/- MATH: (2 : F_p) ≠ 0 — the field has characteristic ≠ 2 (p is odd).
WHY NEEDED: the completeness proof at one point divides the curve world
into i·x₂ = ±y₂ and needs "both" to force x₂ = 0 via 2·i·x₂ = 0. -/
theorem two_ne_zero_Fp : (2 : Fp) ≠ 0 := by
have h : ((2 : ) : Fp) ≠ 0 := natCast_ne_zero_of_mod (by decide)
exact_mod_cast h
/-! ## The curve constant d = 121665/121666 -/
/-- The Ed25519 twisted Edwards curve constant
MATH: d := 121665/121666 ∈ 𝔽_p (RFC 8032 §5.1; BBJLP "Twisted Edwards
curves" parameters a = 1, d).
Rust analog: `constants::EDWARDS_D` in curve25519/solana-ed25519/src/
backend/serial/u64/constants.rs — the limb vector denoting exactly this
field element.
WHY NEEDED: parametrizes the curve equation `OnCurve` and the addition
law `edAdd`; its NON-squareness (`edD_not_square`) is what makes the
addition law complete. -/
noncomputable def edD : Fp := -(121665 : Fp) / 121666
/- MATH: 121666 · d = 121665 — the denominator-free characterization.
This is the form a DECODER of the constant can check by pure limb
arithmetic (multiply by 121666, compare with 121665), with no inversion.
Proof: cancel the nonzero denominator 121666.
WHY NEEDED: the bridge between the abstract fraction and any concrete
representation of d — used right below to certify the canonical residue
`dNum`, and intended as the spec hook for the transpiled `EDWARDS_D`. -/
theorem edD_char : (121666 : Fp) * edD = -121665 := by
unfold edD
rw [mul_comm, div_mul_cancel₀ _ c121666_ne_zero]
/- MATH: d ≠ 0 — numerator 121665 and denominator 121666 are both nonzero.
WHY NEEDED: Euler's criterion (`ZMod.euler_criterion`) only speaks about
nonzero elements; also gives x,y ≠ 0 extraction in the completeness proof. -/
theorem edD_ne_zero : edD ≠ 0 := by
unfold edD
exact div_ne_zero (neg_ne_zero.mpr c121665_ne_zero) c121666_ne_zero
/-! ## d as a canonical residue, and d^((p1)/2) = 1
To exponentiate d with the kernel we need d as a NATURAL number. `dNum` is
the canonical representative (computed offline as
(p 121665)·121666⁻¹ mod p; the kernel re-certifies it below, so the
literal is trusted only as a hint). -/
/-- The unique n < P with (n : 𝔽_p) = d, as a decimal literal.
MATH: dNum := (p 121665) · (121666⁻¹ mod p) mod p.
WHY NEEDED: `powMod` computes on `Nat`, not on `ZMod P`; this literal is
the entry ticket for the kernel computation of d^((p1)/2). -/
def dNum : := 37095705934669439343138083508754565189542113879843219016388785533085940283555
/- MATH: (dNum : F_p) = d.
Proof: by `edD_char`-style cancellation it suffices that
121666·dNum + 121665 ≡ 0 (mod p) — a closed `Nat` congruence the kernel
`decide`s (one big multiplication and one division by P).
WHY NEEDED: transports the kernel-checked power of `dNum` to a statement
about `edD` itself. -/
theorem dNum_cast : (dNum : Fp) = edD := by
unfold edD
rw [eq_div_iff c121666_ne_zero]
-- the kernel certifies: p | dNum·121666 + 121665
have key : ((dNum * 121666 + 121665 : ) : Fp) = 0 := natCast_eq_zero_of_mod (by decide)
push_cast at key
linear_combination key
/- MATH: d^((p1)/2) = 1 in 𝔽_p (stated with `P / 2`, which equals
(p1)/2 since p is odd — this is the exact exponent in mathlib's
`ZMod.euler_criterion`).
Proof: the kernel computes powMod dNum (P/2) P = P 1 (≈255 squarings of
255-bit numbers — milliseconds via GMP), and (P 1 : 𝔽_p) = 1 because
(P1) + 1 = P ≡ 0.
WHY NEEDED: with Euler's criterion this IS the non-squareness of d. -/
theorem edD_pow_eq_neg_one : edD ^ (P / 2) = -1 := by
rw [← dNum_cast, cast_pow_eq dNum (P / 2) P (by decide),
show powMod dNum (P / 2) P = P - 1 from by decide]
-- (P 1 : 𝔽_p) = 1, i.e. ↑(P1) + 1 = 0, i.e. ↑P = 0
have hP1 : 1 ≤ P := by norm_num [P]
have h : ((P - 1 : ) : Fp) + ((1 : ) : Fp) = 0 := by
rw [← Nat.cast_add, Nat.sub_add_cancel hP1]
exact natCast_eq_zero_of_mod (by simp)
rw [Nat.cast_one] at h
linear_combination h
/- MATH: 1 ≠ 1 in 𝔽_p (characteristic ≠ 2: their difference is 2 ≠ 0).
WHY NEEDED: turns `edD^(P/2) = 1` into `edD^(P/2) ≠ 1` for Euler. -/
theorem neg_one_ne_one_Fp : (-1 : Fp) ≠ 1 := by
intro hc
exact two_ne_zero_Fp (by linear_combination -hc)
/-- d is a quadratic NON-residue modulo p.
MATH (ASCII): ¬ exists r in F_p, d = r·r.
LaTeX: $\left(\frac{d}{p}\right) = -1$.
Proof: Euler's criterion (`ZMod.euler_criterion`, d ≠ 0) reduces
squareness to d^(p/2) = 1; but d^(p/2) = 1 ≠ 1 by the kernel
computation above.
WHY NEEDED: THE hypothesis of the BernsteinLange completeness theorem.
A square d would admit exceptional point pairs where the addition law's
denominators vanish; non-square d (this theorem) rules them ALL out. -/
theorem edD_not_square : ¬ IsSquare edD := by
intro hsq
have h := (ZMod.euler_criterion P edD_ne_zero).mp hsq
rw [edD_pow_eq_neg_one] at h
exact neg_one_ne_one_Fp h
/-! ## √1 in 𝔽_p
p ≡ 1 (mod 4), so 1 is a square mod p. A concrete square root (the same
distinguished one the Rust constant `SQRT_M1` denotes, namely
2^((p1)/4) mod p) lets the a = 1 completeness proof run DIRECTLY on the
twisted curve: wherever the classical a = 1 proof squares (x₁ ± y₁), we
square (i·x₁ ± ε·y₁) instead. -/
/-- The canonical √1 of 𝔽_p as a decimal literal: 2^((p1)/4) mod p
(computed offline; the kernel re-certifies the defining equation below).
Rust analog: `constants::SQRT_M1`.
WHY NEEDED: makes 1 an EXPLICIT square, which is what lets the a = 1
twisted curve reuse the BernsteinLange square-exhibition argument. -/
def sNum : := 19681161376707505956807079304988542015446066515923890162744021073123829784752
/-- √1 as a field element. -/
noncomputable def sqrtM1 : Fp := (sNum : Fp)
/- MATH: sqrtM1² = 1. Proof: the kernel certifies p | sNum·sNum + 1 (one
255×255-bit multiplication), and a multiple of p vanishes in 𝔽_p.
WHY NEEDED: the only property of `sqrtM1` the completeness proof uses. -/
theorem sqrtM1_sq : sqrtM1 ^ 2 = -1 := by
have key : ((sNum * sNum + 1 : ) : Fp) = 0 := natCast_eq_zero_of_mod (by decide)
push_cast at key
unfold sqrtM1
linear_combination key
/- MATH: sqrtM1 ≠ 0 (its square is 1 ≠ 0).
WHY NEEDED: cancellation in the `i·x₂ = ±y₂ ⇒ x₂ = 0` step. -/
theorem sqrtM1_ne_zero : sqrtM1 ≠ 0 := by
intro h0
have h := sqrtM1_sq
rw [h0] at h
exact one_ne_zero (by linear_combination h)
/-! ## The curve, its addition law, negation, neutral element -/
/-- The twisted Edwards curve equation with a = 1 (Ed25519, RFC 8032 §5.1):
MATH (ASCII): OnCurve x y :<=> -x^2 + y^2 = 1 + d·x^2·y^2.
LaTeX: $-x^2 + y^2 = 1 + d x^2 y^2$.
Rust analog: the (implicit) invariant of `EdwardsPoint` in
curve25519/solana-ed25519/src/edwards.rs — affine coordinates here,
extended coordinates there. -/
def OnCurve (x y : Fp) : Prop := -(x^2) + y^2 = 1 + edD * x^2 * y^2
/-- The COMPLETE twisted Edwards addition law (BBJLP "Twisted Edwards
curves", §6, a = 1):
MATH: (x₁,y₁) + (x₂,y₂) =
( (x₁y₂ + x₂y₁) / (1 + d·x₁x₂y₁y₂), (y₁y₂ + x₁x₂) / (1 d·x₁x₂y₁y₂) ).
By `completeness` the denominators never vanish on curve points, so the
division is honest field division everywhere we ever apply it. (On the
pair type `Fp × Fp` at large, mathlib's junk-value convention x/0 = 0
applies — all theorems below restrict to curve points.)
Rust analog: `EdwardsPoint: Add` (extended-coordinate version) in
src/edwards.rs — the projective P³ formulas compute exactly these two
fractions. -/
noncomputable def edAdd (p q : Fp × Fp) : Fp × Fp :=
( (p.1*q.2 + q.1*p.2) / (1 + edD*p.1*q.1*p.2*q.2),
(p.2*q.2 + p.1*q.1) / (1 - edD*p.1*q.1*p.2*q.2) )
/-- Point negation: (x, y) = (x, y) (Edwards curves negate the
x-coordinate). Rust analog: `EdwardsPoint: Neg` in src/edwards.rs. -/
def edNeg (p : Fp × Fp) : Fp × Fp := (-p.1, p.2)
/-- The neutral element (0, 1).
Rust analog: `EdwardsPoint::identity()` (X=0, Y=Z=1, T=0). -/
def edId : Fp × Fp := (0, 1)
/-! ## Completeness of the addition law (BernsteinLange)
The mathematical heart of the file. Shape of the argument (BBJLP Thm 3.3 /
BernsteinLange "Faster addition and doubling", adapted to a = 1):
Suppose some denominator vanished, i.e. ε := d·x₁x₂y₁y₂ ∈ {1, +1}. Then
x₁, x₂, y₁, y₂ are all nonzero (their product is ±1/d ≠ 0), and with
i := √1 the two curve equations combine into the EXPLICIT square identities
(i·x₁ + ε·y₁)² = d · x₁²y₁² · (i·x₂ + y₂)²
(i·x₁ ε·y₁)² = d · x₁²y₁² · (i·x₂ y₂)²
(kernel-of-the-proof: ε² = 1 turns d²·(x₁x₂y₁y₂)² into 1, which is what
collapses everything). At least one of i·x₂ ± y₂ is nonzero — otherwise
2·i·x₂ = 0 forces x₂ = 0 — so dividing the corresponding identity by the
nonzero square (x₁y₁·(i·x₂ ± y₂))² exhibits d as a SQUARE in 𝔽_p,
contradicting `edD_not_square`. ∎ -/
/- The shared core: NO curve points make d·x₁x₂y₁y₂ a square root of 1.
MATH (ASCII): OnCurve x1 y1 ∧ OnCurve x2 y2 ∧ ε² = 1 ∧
d·x1·x2·y1·y2 = ε ==> False.
Instantiated with ε = 1 (resp. ε = +1) this kills the "+" (resp. "")
denominator of `edAdd`. The two `linear_combination` certificates are the
polynomial cofactors of the square identities above w.r.t. the ideal
generated by the two curve equations, heq, i² = 1 and ε² = 1 (computed
and verified offline; `ring` re-verifies them here).
WHY NEEDED: the engine behind both halves of `completeness`. -/
theorem denominator_core {x1 y1 x2 y2 : Fp} (h1 : OnCurve x1 y1) (h2 : OnCurve x2 y2)
(ε : Fp) (hε : ε ^ 2 = 1) (heq : edD * x1 * x2 * y1 * y2 = ε) : False := by
-- ε is a unit, hence so is the product d·x₁x₂y₁y₂: all factors are nonzero
have hε0 : ε ≠ 0 := by
intro h0
rw [h0] at hε
exact one_ne_zero (by linear_combination -hε)
have hne : edD * x1 * x2 * y1 * y2 ≠ 0 := by rw [heq]; exact hε0
have hy2 : y2 ≠ 0 := right_ne_zero_of_mul hne
have hy1 : y1 ≠ 0 := right_ne_zero_of_mul (left_ne_zero_of_mul hne)
have hx2 : x2 ≠ 0 := right_ne_zero_of_mul (left_ne_zero_of_mul (left_ne_zero_of_mul hne))
have hx1 : x1 ≠ 0 :=
right_ne_zero_of_mul (left_ne_zero_of_mul (left_ne_zero_of_mul (left_ne_zero_of_mul hne)))
have hi := sqrtM1_sq
unfold OnCurve at h1 h2
-- the two BernsteinLange square identities (cofactors verified offline)
have key₁ : (sqrtM1*x1 + ε*y1)^2 = edD * x1^2*y1^2 * (sqrtM1*x2 + y2)^2 := by
linear_combination h1 - edD*x1^2*y1^2 * h2
- (edD*x1*x2*y1*y2 + ε + 2*sqrtM1*x1*y1) * heq
+ (x1^2 - edD*x1^2*y1^2*x2^2) * hi + (y1^2 - 1) * hε
have key₂ : (sqrtM1*x1 - ε*y1)^2 = edD * x1^2*y1^2 * (sqrtM1*x2 - y2)^2 := by
linear_combination h1 - edD*x1^2*y1^2 * h2
- (edD*x1*x2*y1*y2 + ε - 2*sqrtM1*x1*y1) * heq
+ (x1^2 - edD*x1^2*y1^2*x2^2) * hi + (y1^2 - 1) * hε
-- at least one of i·x₂ ± y₂ is a unit; divide the matching identity by it
by_cases hc : sqrtM1*x2 + y2 = 0
· -- i·x₂ + y₂ = 0, so i·x₂ y₂ ≠ 0 (else 2·i·x₂ = 0 forces x₂ = 0)
have hc2 : sqrtM1*x2 - y2 ≠ 0 := by
intro hc2
apply hx2
have h2x : (2 * sqrtM1) * x2 = 0 := by linear_combination hc + hc2
rcases mul_eq_zero.mp h2x with h | h
· rcases mul_eq_zero.mp h with h' | h'
· exact absurd h' two_ne_zero_Fp
· exact absurd h' sqrtM1_ne_zero
· exact h
-- d = ((i·x₁ ε·y₁)/(x₁y₁(i·x₂ y₂)))² — a square, contradiction
apply edD_not_square
refine ⟨(sqrtM1*x1 - ε*y1) / (x1*y1*(sqrtM1*x2 - y2)), ?_⟩
have hden : x1*y1*(sqrtM1*x2 - y2) ≠ 0 := mul_ne_zero (mul_ne_zero hx1 hy1) hc2
rw [div_mul_div_comm, eq_div_iff (mul_ne_zero hden hden)]
linear_combination -key₂
· -- i·x₂ + y₂ ≠ 0: same square exhibition with the "+" identity
apply edD_not_square
refine ⟨(sqrtM1*x1 + ε*y1) / (x1*y1*(sqrtM1*x2 + y2)), ?_⟩
have hden : x1*y1*(sqrtM1*x2 + y2) ≠ 0 := mul_ne_zero (mul_ne_zero hx1 hy1) hc
rw [div_mul_div_comm, eq_div_iff (mul_ne_zero hden hden)]
linear_combination -key₁
/-- THE COMPLETENESS THEOREM (BernsteinLange, a = 1 twisted case).
MATH (ASCII): for all curve points (x1,y1), (x2,y2):
1 + d·x1·x2·y1·y2 ≠ 0 and 1 d·x1·x2·y1·y2 ≠ 0.
LaTeX: $1 \pm d\,x_1x_2y_1y_2 \neq 0$ on $E \times E$.
Both denominators of `edAdd` are units at EVERY pair of curve points —
the addition law is complete: one formula, no exceptions, defined even
for doubling (p = q). This is precisely why the Rust implementation can
be branch-free (constant-time) without an exceptional-case audit.
Proof: if a denominator vanished, d·x₁x₂y₁y₂ would be ∓1, which
`denominator_core` (using ¬IsSquare d) refutes. -/
theorem completeness {x1 y1 x2 y2 : Fp} (h1 : OnCurve x1 y1) (h2 : OnCurve x2 y2) :
1 + edD * x1 * x2 * y1 * y2 ≠ 0 ∧ 1 - edD * x1 * x2 * y1 * y2 ≠ 0 := by
constructor
· intro hbad
exact denominator_core h1 h2 (-1) (by norm_num) (by linear_combination hbad)
· intro hbad
exact denominator_core h1 h2 1 (by norm_num) (by linear_combination -hbad)
/-! ## The mechanical laws -/
/- MATH: (0, 1) lies on the curve: 0² + 1² = 1 = 1 + d·0²·1².
WHY NEEDED: the neutral element must be a point for `edAdd_id`/`edAdd_neg`
to be statements about curve points. -/
theorem onCurve_id : OnCurve 0 1 := by
unfold OnCurve
norm_num
/- MATH: (x, y) on the curve ⇒ (x, y) on the curve ((x)² = x²).
WHY NEEDED: `edNeg` maps points to points; feeds `edAdd_neg`. -/
theorem onCurve_neg {x y : Fp} (h : OnCurve x y) : OnCurve (-x) y := by
unfold OnCurve at h ⊢
linear_combination h
/- MATH: (x,y) + (0,1) = (x,y) — right identity. At q = (0,1) the
denominators are LITERALLY 1 (no completeness needed): the components
reduce to (x·1 + 0·y)/1 and (y·1 + x·0)/1. -/
theorem edAdd_id {x y : Fp} (_h : OnCurve x y) : edAdd (x, y) edId = (x, y) := by
unfold edAdd edId
simp
/- MATH: p + q = q + p — the formula is literally symmetric in p, q (both
numerators and both denominators are, up to commuting products/sums).
WHY NEEDED: with `edAdd_id` it gives the LEFT identity for free, and it
is half of the abelian-group structure (Tier 2). -/
theorem edAdd_comm (p q : Fp × Fp) : edAdd p q = edAdd q p := by
unfold edAdd
rw [Prod.mk.injEq]
constructor
· rw [show q.1*p.2 + p.1*q.2 = p.1*q.2 + q.1*p.2 from by ring,
show edD*q.1*p.1*q.2*p.2 = edD*p.1*q.1*p.2*q.2 from by ring]
· rw [show q.2*p.2 + q.1*p.1 = p.2*q.2 + p.1*q.1 from by ring,
show edD*q.1*p.1*q.2*p.2 = edD*p.1*q.1*p.2*q.2 from by ring]
/- MATH: (x,y) + (x,y) = (0,1) — every point has an inverse.
The x-component's numerator is x·y + (x)·y = 0 (no completeness needed:
0/z = 0 for ANY z). The y-component is (y² x²)/(1 + d·x²y²); the curve
equation says numerator = denominator, and completeness (at the point and
its negation, where the "" denominator of `edAdd` is 1 + d·x²y²)
guarantees the denominator is a unit, so the quotient is 1.
WHY NEEDED: inverses — another quarter of the group structure. -/
theorem edAdd_neg {x y : Fp} (h : OnCurve x y) : edAdd (x, y) (edNeg (x, y)) = edId := by
-- the "" denominator at ((x,y), (x,y)) is 1 d·x·(x)·y·y = 1 + d·x²y² ≠ 0
have hm := (completeness h (onCurve_neg h)).2
unfold OnCurve at h
show ( (x*y + -x*y) / (1 + edD * x * -x * y * y),
(y*y + x * -x) / (1 - edD * x * -x * y * y) ) = (0, 1)
rw [Prod.mk.injEq]
constructor
· rw [show x*y + -x*y = (0 : Fp) from by ring, zero_div]
· rw [div_eq_iff hm, one_mul]
linear_combination h
/- MATH: the sum of two curve points is a curve point — `edAdd` is CLOSED on
the curve. With x₃ = T/(1+D), y₃ = N/(1D) (T = x₁y₂+x₂y₁, N = y₁y₂+x₁x₂,
D = d·x₁x₂y₁y₂), multiplying the target curve equation by the unit
(1+D)²(1D)² turns it into the polynomial identity
T²(1D)² + N²(1+D)² = (1+D)²(1D)² + d·T²·N²,
which is an explicit combination of the two input curve equations: the
two big cofactor polynomials in the `linear_combination` below were
computed offline by a Gröbner-basis reduction of the identity modulo the
curve ideal, and are re-verified from scratch by `ring` here (the `hT`/
`hN` summands merely re-relate u = x₃, v = y₃ to their numerators).
WHY NEEDED: well-definedness of the group operation — the final quarter
of the Tier-1 group-law package (associativity is Tier 2). -/
theorem edAdd_closure {x1 y1 x2 y2 : Fp} (h1 : OnCurve x1 y1) (h2 : OnCurve x2 y2) :
OnCurve (edAdd (x1, y1) (x2, y2)).1 (edAdd (x1, y1) (x2, y2)).2 := by
obtain ⟨hp, hm⟩ := completeness h1 h2
unfold OnCurve at h1 h2
show -(((x1*y2 + x2*y1) / (1 + edD*x1*x2*y1*y2))^2)
+ ((y1*y2 + x1*x2) / (1 - edD*x1*x2*y1*y2))^2
= 1 + edD * ((x1*y2 + x2*y1) / (1 + edD*x1*x2*y1*y2))^2
* ((y1*y2 + x1*x2) / (1 - edD*x1*x2*y1*y2))^2
-- name the two components and recover their defining equations
have hT : (x1*y2 + x2*y1) / (1 + edD*x1*x2*y1*y2) * (1 + edD*x1*x2*y1*y2)
= x1*y2 + x2*y1 := div_mul_cancel₀ _ hp
have hN : (y1*y2 + x1*x2) / (1 - edD*x1*x2*y1*y2) * (1 - edD*x1*x2*y1*y2)
= y1*y2 + x1*x2 := div_mul_cancel₀ _ hm
set u := (x1*y2 + x2*y1) / (1 + edD*x1*x2*y1*y2) with hu
set v := (y1*y2 + x1*x2) / (1 - edD*x1*x2*y1*y2) with hv
-- clear the (unit) denominators: multiply both sides by (1+D)²(1D)²
apply mul_right_cancel₀ (mul_ne_zero (pow_ne_zero 2 hp) (pow_ne_zero 2 hm))
-- …and certify the resulting polynomial identity from the curve equations
linear_combination
(edD^3*x1^2*y1^2*x2^4*y2^4 - edD^2*x1^2*x2^4*y2^4 + edD^2*y1^2*x2^4*y2^4
- edD^2*x2^4*y2^4 - edD*x1^2*x2^4*y2^2 + edD*y1^2*x2^4*y2^2
+ edD*x1^2*x2^2*y2^4 - edD*y1^2*x2^2*y2^4 - 2*edD*x2^4*y2^4
- 2*x2^4*y2^2 + 2*x2^2*y2^4 - 2*edD*x2^2*y2^2 + x2^4 - 4*x2^2*y2^2
+ y2^4) * h1
+ (edD*x1^4*x2^2*y2^2 + edD*y1^4*x2^2*y2^2 + 2*edD*x1^2*x2^2*y2^2
- 2*edD*y1^2*x2^2*y2^2 + 2*x1^2*x2^2*y2^2 - 2*y1^2*x2^2*y2^2
+ edD*x2^2*y2^2 - x1^2*x2^2 + y1^2*x2^2 + x1^2*y2^2 - y1^2*y2^2
+ 2*x2^2*y2^2 - x2^2 + y2^2 + 1) * h2
+ (-(1 - edD*x1*x2*y1*y2)^2 * (u*(1 + edD*x1*x2*y1*y2) + (x1*y2 + x2*y1))
- edD*(y1*y2 + x1*x2)
* (u*v*(1 + edD*x1*x2*y1*y2)*(1 - edD*x1*x2*y1*y2)
+ (x1*y2 + x2*y1)*(y1*y2 + x1*x2))) * hT
+ ((1 + edD*x1*x2*y1*y2)^2 * (v*(1 - edD*x1*x2*y1*y2) + (y1*y2 + x1*x2))
- edD*u*(1 + edD*x1*x2*y1*y2)
* (u*v*(1 + edD*x1*x2*y1*y2)*(1 - edD*x1*x2*y1*y2)
+ (x1*y2 + x2*y1)*(y1*y2 + x1*x2))) * hN
end CurveFieldProofs