field layer: 14 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. check.sh gates: source integrity, stub audit, zero axiom
declarations under Proofs/, per-certificate axiom audit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-07-02 14:17:44 +02:00
parent 549bef148b
commit a80e360a3e
24 changed files with 7366 additions and 1 deletions

View file

@ -25,7 +25,7 @@ in this repository.
| Layer | Certificate | Status | Axioms of certificate |
|-------|-------------|--------|-----------------------|
| Field 𝔽_p | `fieldImplementation` | ⏳ in progress | — |
| Field 𝔽_p | `fieldImplementation` | ✅ proven | `[propext, Classical.choice, Quot.sound]` |
| Group law (Edwards) | `edwardsImplementation` | ⏳ in progress | — |
| Scalar mod | `scalarImplementation` | ⏳ in progress | — |
| Signature (EdDSA) | `verifyEquation` | ⏳ in progress | — |

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,327 @@
/- ──────────────────────────────────────────────────────────────────────────────
Proofs/AddSpec.lean — limbwise addition (the transpiled Rust for-loop)
WHAT THIS FILE CONTAINS
Spec for the transpiled `FieldElement51` addition
(`impl Add<&FieldElement51> for &FieldElement51`, which calls `AddAssign`):
it never panics provided the limbwise sums do not overflow u64, and the
output is the *limbwise* sum — this addition performs NO modular reduction.
Plus the loop/iterator infrastructure needed to reason about the one Rust
`for` loop in the field code (`loop_step`, `range_next_lt_spec`,
`range_next_ge_spec`).
RUST ANALOG
- `impl AddAssign<&FieldElement51> for FieldElement51::add_assign`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:59-63:
for i in 0..5 { self.0[i] += _rhs.0[i]; }
- `impl Add<&FieldElement51> for &FieldElement51::add`, field.rs:68-72
(copies self, then `output += _rhs`).
Transpiled bodies in gen/CurveField/Funs.lean:
`Shared0FieldElement51.Insts.CoreOpsArithAddSharedAFieldElement51FieldElement51.add`
delegating to `...CoreOpsArithAddAssignSharedAFieldElement51.add_assign`, whose
`for` loop Charon/Aeneas compiled into the tail-recursive combinator
`Aeneas.Std.loop` applied to `add_assign_loop.body`, threading the state
(range-iterator, self, _rhs). The body calls `Iterator::next` on `Range<usize>`
(Rust std: core::iter::range, stepping via `Step::forward_checked`), and either
`done` (range exhausted) or performs one `self[i] + rhs[i]` update and `cont`s.
WHY THE SPEC IS "EXACT LIMBWISE SUMS"
Unlike sub/negate/mul, `add` performs neither carry propagation nor reduction:
r_i = a_i + b_i exactly as u64 (panics iff some a_i + b_i ≥ 2⁶⁴). Consequently
feVal r = feVal a + feVal b holds EXACTLY over , and limb bounds DOUBLE:
Bnd a c ∧ Bnd b c → Bnd r (2c). Callers must track this growth — e.g. adding two
reduced elements (< 2⁵²) yields < 2⁵³ limbs, still safely below the 2⁵⁴ input
invariant of mul/sub; this bookkeeping is done in Proofs/Field.lean / FieldMain.lean.
PROOF ARCHITECTURE
`add_limbs_spec` unrolls the loop 5-fold by hand: 5 × (apply `loop_step`, run one
body iteration with `range_next_lt_spec` + index/add/update steps) and a 6th
`loop_step` where `range_next_ge_spec` (5 ≥ 5) makes the body return `done`.
`add_spec` then repackages the limb equations into the feVal/Bnd form via the
consequence rule `spec_mono`.
ROLE IN THE MAIN THEOREM
Field addition of `fieldImplementation`: totality under no-overflow is one conjunct
of the no-panic claim, and feVal r = feVal a + feVal b casts to ⟪r⟫ = ⟪a⟫ + ⟪b⟫,
from which FieldMain derives impl_add_comm/impl_add_assoc/impl_zero_add/impl_add_neg.
FILE RELATIONS
Imports Proofs/Denote.lean (Fe, feVal, Bnd) and Proofs/ReduceSpec.lean (whose
-level simp lemmas are in scope for scalar_tac). Imported by Proofs/SquareSpec.lean
and Proofs/Field.lean.
────────────────────────────────────────────────────────────────────────────── -/
import Proofs.Denote
import Proofs.ReduceSpec
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 4000000
set_option linter.unusedSimpArgs false
namespace CurveFieldProofs
-- weakest-precondition helpers for the ⦃·⦄ assertions (spec_imp_exists, spec_ok, spec_mono)
open Aeneas.Std.WP
/-- Unfold one iteration of `Aeneas.Std.loop` under a `spec` goal.
No Rust analog — proof infrastructure for the `loop` combinator that Aeneas
emits for every Rust loop.
MATH (one unfolding of the loop's fixed-point semantics):
ASCII: if body x succeeds with a result r such that
- r = cont x' implies loop body x' ⦃ post ⦄ (loop continues), and
- r = done y implies post y (loop exits),
then loop body x ⦃ post ⦄.
`ControlFlow α β` is the transpiled `core::ops::ControlFlow`: `cont x'` carries the
next loop state, `done y` the loop's final value.
WHY NEEDED: `add_assign`'s `for` loop has a statically known trip count (0..5), so
instead of a loop invariant we apply this lemma 6 times — 5 productive iterations
plus the terminating `next = none` check — fully unrolling the loop. Without it the
opaque `Aeneas.Std.loop` could not be executed symbolically. -/
theorem loop_step {α : Type u} {β : Type v}
{body : α → Result (ControlFlow α β)} {x : α} {post : β → Prop}
(h : body x ⦃ r => match r with
| .cont x' => Aeneas.Std.loop body x' ⦃ post ⦄
| .done y => post y ⦄) :
Aeneas.Std.loop body x ⦃ post ⦄ := by
-- extract the body's concrete result r and its postcondition from the spec assertion
obtain ⟨r, hr, hpost⟩ := spec_imp_exists h
-- unfold the fixed point once; the body's result decides continue vs. exit
rw [Aeneas.Std.loop.eq_def, hr]
cases r <;> simpa using hpost
/-- `Iterator::next` on a `usize` range that has not finished yet.
Rust std analog: `impl Iterator for Range<usize>` —
`core::iter::range::Iterator::next`, which (for `start < end`) clones `start`,
advances it via `<usize as Step>::forward_checked(start, 1)`, and returns
`Some(old_start)`. The transpiled model is
`core.iter.range.IteratorRange.next core.iter.range.StepUsize`.
MATH:
ASCII: start < end ==> next {start, end} = ok (some start, {start+1, end}).
The `checked_add` inside cannot return `none`: start < end ≤ usize::MAX implies
start + 1 ≤ usize::MAX.
WHY NEEDED: each of the 5 productive iterations of the `for i in 0..5` loop begins
with this call; the equations `o = some i` / `start' = i+1` are what let the
iteration-k proof know which array index it is operating on. -/
theorem range_next_lt_spec (r : core.ops.range.Range Usize)
(h : r.start.val < r.«end».val) :
core.iter.range.IteratorRange.next core.iter.range.StepUsize r
⦃ (o, r') => o = some r.start ∧ r'.start.val = r.start.val + 1 ∧
r'.«end» = r.«end» ⦄ := by
-- start+1 stays within usize, so checked_add must succeed
have hmax : r.start.val + 1 ≤ Usize.max := by scalar_tac
have hca := Usize.checked_add_bv_spec r.start 1#usize
unfold core.iter.range.IteratorRange.next
-- evaluate the model plumbing: the < comparison, clone, Step::forward_checked
simp only [core.cmp.impls.PartialOrdUsize.lt,
core.clone.impls.CloneUsize.clone, core.iter.range.StepUsize.forward_checked,
liftFun1, liftFun2, bind_tc_ok]
-- the start < end test is true by hypothesis
simp only [h, decide_true, if_true]
-- case on checked_add: the none branch contradicts hmax, the some branch computes
cases hadd : Usize.checked_add r.start 1#usize with
| none => rw [hadd] at hca; simp at hca; scalar_tac
| some n =>
rw [hadd] at hca
simp at hca
simp [spec_ok, hca]
/-- `Iterator::next` on a `usize` range that is finished.
Rust std analog: same `impl Iterator for Range<usize>` as `range_next_lt_spec`,
exhausted branch: when `start >= end`, `next` returns `None` and leaves the
range untouched.
MATH:
ASCII: end <= start ==> next {start, end} = ok (none, {start, end}).
WHY NEEDED: drives the 6th and final `loop_step` of `add_limbs_spec` (range is
{5, 5}): `next` yields `none`, the transpiled body takes the `done` branch, and
the loop returns the accumulated element. -/
theorem range_next_ge_spec (r : core.ops.range.Range Usize)
(h : r.«end».val ≤ r.start.val) :
core.iter.range.IteratorRange.next core.iter.range.StepUsize r
⦃ (o, r') => o = none ∧ r' = r ⦄ := by
unfold core.iter.range.IteratorRange.next
-- evaluate the comparison/clone/step plumbing as before
simp only [core.cmp.impls.PartialOrdUsize.lt,
core.clone.impls.CloneUsize.clone, core.iter.range.StepUsize.forward_checked,
liftFun1, liftFun2, bind_tc_ok]
-- the start < end test is now false; the model returns (none, r) directly
have : ¬ (r.start.val < r.«end».val) := by omega
simp [this]
/-- Limb-level spec for `fe_add`: total (no panic) under the no-overflow
hypothesis, and the output limbs are exactly the limbwise sums
(no modular reduction, no carry propagation).
Rust: `impl Add for &FieldElement51::add` → `add_assign`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:68-72 and 59-63.
MATH:
ASCII: forall a b : Fe, (a_i + b_i < 2^64 for i = 0..4) ==>
fe_add a b = ok r with r_i = a_i + b_i for all i.
LaTeX: $\forall a\,b,\ (\forall i,\ a_i + b_i < 2^{64}) \Rightarrow
\exists r,\ \mathrm{add}(a,b) = \mathrm{ok}\ r \wedge
\forall i,\ r_i = a_i + b_i$.
The hypothesis `hbnd` is exactly the panic condition of the Rust `+=` on u64
(overflow aborts in debug; the Aeneas model makes it `fail` unconditionally), so
proving the spec under `hbnd` IS the panic-freedom proof.
WHY NEEDED: the strongest (limb-exact) description of `add`, consumed by
`add_spec` below; keeping the loop-unrolling proof separate from the
feVal/Bnd repackaging keeps both readable. -/
theorem add_limbs_spec (a b : Fe) (a0 a1 a2 a3 a4 b0 b1 b2 b3 b4 : U64)
(ha : (↑a : List U64) = [a0, a1, a2, a3, a4])
(hb : (↑b : List U64) = [b0, b1, b2, b3, b4])
(hbnd : a0.val + b0.val < 2^64 ∧ a1.val + b1.val < 2^64 ∧
a2.val + b2.val < 2^64 ∧ a3.val + b3.val < 2^64 ∧
a4.val + b4.val < 2^64) :
fe_add a b ⦃ r => ∃ r0 r1 r2 r3 r4 : U64,
(↑r : List U64) = [r0, r1, r2, r3, r4] ∧
r0.val = a0.val + b0.val ∧ r1.val = a1.val + b1.val ∧
r2.val = a2.val + b2.val ∧ r3.val = a3.val + b3.val ∧
r4.val = a4.val + b4.val ⦄ := by
obtain ⟨hbnd0, hbnd1, hbnd2, hbnd3, hbnd4⟩ := hbnd
-- expose the Add → AddAssign → loop-combinator chain (gen/CurveField/Funs.lean)
unfold fe_add
Shared0FieldElement51.Insts.CoreOpsArithAddSharedAFieldElement51FieldElement51.add
backend.serial.u64.field.FieldElement51.Insts.CoreOpsArithAddAssignSharedAFieldElement51.add_assign
backend.serial.u64.field.FieldElement51.Insts.CoreOpsArithAddAssignSharedAFieldElement51.add_assign_loop
-- Iteration 1 (i = 0)
-- Pattern repeated for each of the 5 iterations:
-- loop_step — peel one iteration of the loop combinator,
-- simp only [..body] — substitute the loop body's definition,
-- step with range_next_lt_spec — Iterator::next yields some i, range advances,
-- step ×2 — read rhs[i] (x_k) and self[i] (y_k),
-- step — u64 add; its overflow side condition is closed by hbnd_i,
-- step — write the sum back into self (array update s_k),
-- spec_ok — the body returns `cont` with the updated state.
-- hv_k records v_k = a_k + b_k; hd_k records the updated array for the next round.
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.Insts.CoreOpsArithAddAssignSharedAFieldElement51.add_assign_loop.body]
step with range_next_lt_spec as ⟨o1, iter1, ho1, hs1, he1⟩
simp only [ho1]
step as ⟨x1, hx1⟩
step as ⟨y1, hy1⟩
simp [ha, hb] at hx1 hy1
step as ⟨v0, hv0⟩
rw [hx1, hy1] at hv0
step as ⟨s1, hd1⟩
try simp only [spec_ok]
-- Iteration 2 (i = 1) — same pattern; the simp at hx2/hy2 additionally rewrites
-- through iteration 1's array update (hd1 + Array.set_val_eq: get-after-set) so the
-- reads still refer to the ORIGINAL limbs a1/b1.
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.Insts.CoreOpsArithAddAssignSharedAFieldElement51.add_assign_loop.body]
step with range_next_lt_spec as ⟨o2, iter2, ho2, hs2, he2⟩
simp only [ho2]
step as ⟨x2, hx2⟩
step as ⟨y2, hy2⟩
simp [hd1, Array.set_val_eq, ha, hb, hs1, he1] at hx2 hy2
step as ⟨v1, hv1⟩
rw [hx2, hy2] at hv1
step as ⟨s2, hd2⟩
try simp only [spec_ok]
-- Iteration 3 (i = 2)
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.Insts.CoreOpsArithAddAssignSharedAFieldElement51.add_assign_loop.body]
step with range_next_lt_spec as ⟨o3, iter3, ho3, hs3, he3⟩
simp only [ho3]
step as ⟨x3, hx3⟩
step as ⟨y3, hy3⟩
simp [hd1, hd2, Array.set_val_eq, ha, hb, hs1, he1, hs2, he2] at hx3 hy3
step as ⟨v2, hv2⟩
rw [hx3, hy3] at hv2
step as ⟨s3, hd3⟩
try simp only [spec_ok]
-- Iteration 4 (i = 3)
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.Insts.CoreOpsArithAddAssignSharedAFieldElement51.add_assign_loop.body]
step with range_next_lt_spec as ⟨o4, iter4, ho4, hs4, he4⟩
simp only [ho4]
step as ⟨x4, hx4⟩
step as ⟨y4, hy4⟩
simp [hd1, hd2, hd3, Array.set_val_eq, ha, hb, hs1, he1, hs2, he2, hs3, he3] at hx4 hy4
step as ⟨v3, hv3⟩
rw [hx4, hy4] at hv3
step as ⟨s4, hd4⟩
try simp only [spec_ok]
-- Iteration 5 (i = 4)
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.Insts.CoreOpsArithAddAssignSharedAFieldElement51.add_assign_loop.body]
step with range_next_lt_spec as ⟨o5, iter5, ho5, hs5, he5⟩
simp only [ho5]
step as ⟨x5, hx5⟩
step as ⟨y5, hy5⟩
simp [hd1, hd2, hd3, hd4, Array.set_val_eq, ha, hb, hs1, he1, hs2, he2, hs3, he3,
hs4, he4] at hx5 hy5
step as ⟨v4, hv4⟩
rw [hx5, hy5] at hv4
step as ⟨s5, hd5⟩
try simp only [spec_ok]
-- Iteration 6 (range exhausted: 5 ≥ 5) — next returns none, body answers `done`
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.Insts.CoreOpsArithAddAssignSharedAFieldElement51.add_assign_loop.body]
step with range_next_ge_spec as ⟨o6, iter6, ho6, hr6⟩
simp only [ho6]
try simp only [spec_ok]
-- Final: exhibit the limbs — the result array is a's array overwritten at 0..4 with
-- v0..v4; collapsing the five set operations (Array.set_val_eq) gives [v0,...,v4]
refine ⟨v0, v1, v2, v3, v4, ?_, hv0, hv1, hv2, hv3, hv4⟩
simp [hd1, hd2, hd3, hd4, hd5, Array.set_val_eq, ha, hs1, hs2, hs3, hs4]
/-- Main spec for `fe_add`: the output has 5 limbs, its (unreduced) value is
the sum of the input values, and limb bounds double.
Rust: same as `add_limbs_spec` —
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:58-73.
MATH:
ASCII: forall a b : Fe, (a_i + b_i < 2^64 for all i) ==>
fe_add a b = ok r with |r| = 5 limbs,
feVal r = feVal a + feVal b (EXACT over N, no mod p!),
and forall c, Bnd(a,c) and Bnd(b,c) ==> Bnd(r, 2c).
LaTeX: $\llbracket r\rrbracket_{\mathbb N} = \llbracket a\rrbracket_{\mathbb N}
+ \llbracket b\rrbracket_{\mathbb N}$, hence (cast through → ZMod p)
$\llbracket r\rrbracket = \llbracket a\rrbracket + \llbracket b\rrbracket$
in $\mathbb F_p$.
The doubling clause `Bnd r (2c)` is the price of skipping reduction: limbs grow by
one bit per addition. Downstream (Field.lean) instantiates c = 2⁵² (reduced
operands), giving 2⁵³ < 2⁵⁴ — still inside mul/sub's input invariant, so a single
unreduced add between reduced values is always safe.
WHY NEEDED: this is the form FieldMain consumes for the additive field axioms; the
-exact value equation makes additive laws (comm/assoc) literally inherited from
before casting to 𝔽_p. -/
theorem add_spec (a b : Fe) (a0 a1 a2 a3 a4 b0 b1 b2 b3 b4 : U64)
(ha : (↑a : List U64) = [a0, a1, a2, a3, a4])
(hb : (↑b : List U64) = [b0, b1, b2, b3, b4])
(hbnd : a0.val + b0.val < 2^64 ∧ a1.val + b1.val < 2^64 ∧
a2.val + b2.val < 2^64 ∧ a3.val + b3.val < 2^64 ∧
a4.val + b4.val < 2^64) :
fe_add a b ⦃ r => (↑r : List U64).length = 5 ∧
feVal r = feVal a + feVal b ∧
∀ c, Bnd a c → Bnd b c → Bnd r (2*c) ⦄ := by
-- consequence rule: weaken add_limbs_spec's postcondition to this one
apply spec_mono (add_limbs_spec a b a0 a1 a2 a3 a4 b0 b1 b2 b3 b4 ha hb hbnd)
rintro r ⟨r0, r1, r2, r3, r4, hr, h0, h1, h2, h3, h4⟩
refine ⟨by simp [hr], ?_, ?_⟩
-- value: Σ (a_i + b_i)·2^(51i) = Σ a_i·2^(51i) + Σ b_i·2^(51i) — linear, omega
· rw [feVal_eq r r0 r1 r2 r3 r4 hr, feVal_eq a a0 a1 a2 a3 a4 ha,
feVal_eq b b0 b1 b2 b3 b4 hb]
simp only [limbsVal]
omega
-- bounds: a_i < c and b_i < c give r_i = a_i + b_i < 2c — linear, omega
· intro c hA hB
rw [Bnd_eq a a0 a1 a2 a3 a4 c ha] at hA
rw [Bnd_eq b b0 b1 b2 b3 b4 c hb] at hB
rw [Bnd_eq r r0 r1 r2 r3 r4 (2*c) hr]
omega
end CurveFieldProofs

View file

@ -0,0 +1,172 @@
/- ────────────────────────────────────────────────────────────────────────────
Proofs/Basic.lean — pin the hand-written external models + first sanity
facts about the generated code
────────────────────────────────────────────────────────────────────────────
First proofs over the Aeneas-generated field model (CurveField).
BACKGROUND. The Rust field code of curve25519/solana-ed25519
(src/field.rs + src/backend/serial/u64/field.rs) was transpiled
mechanically to Lean 4 with Charon + Aeneas into gen/CurveField/Types.lean
and Funs.lean; fallible machine operations live in the `Result` monad
(`ok x` = success, `fail` = panic/overflow). Items OUTSIDE the crate —
essentially the `subtle` crate (v2.6.1, constant-time primitives) — are
not transpiled; they are modeled BY HAND in gen/CurveField/FunsExternal.lean
and form the trusted base of the verification (see ../README.md,
"External-model policy").
These establish the proof pattern for this workspace:
- pin down the semantics of the hand-written external models
(gen/CurveField/FunsExternal.lean) as reusable simp lemmas, and
- prove first sanity facts about the generated code itself.
Every external-model lemma below is proved by `rfl` (definitional
equality): it adds NO trust beyond the model itself. Its value is
(a) restating the model in the `= ok …` shape that simp / symbolic
execution consumes, and (b) regression-pinning: if someone edits a model
in FunsExternal.lean to mean something else, this file stops compiling.
PLACE IN THE PROOF GRAPH. Standalone: imports the generated model
(CurveField.Funs) and is compiled first by ../check.sh, but is NOT
imported by the Denote → … → FieldMain chain that proves the main theorem
(CurveFieldProofs.fieldImplementation). Proofs/ConstSpecs.lean proves the
stronger, denotational versions of the constant facts below (it even
reuses the name `zero_spec`, which is harmless precisely because this file
is not imported there).
The real verification targets (limb-bound invariants, add/sub/mul/square
correctness vs. /(2²⁵⁵-19), panic-freedom of the carry chains, and the
sqrt_ratio_i specification) build on these — see ../README.md. -/
import CurveField.Funs
open Aeneas Aeneas.Std Result ControlFlow Error
open curve25519_dalek
namespace CurveFieldProofs
/-! ## Semantics of the external models (subtle)
`subtle.Choice` is the subtle crate's constant-time boolean: a `u8` whose
documented invariant is value ∈ {0, 1} (1 = true). The model declares
`subtle.Choice := U8` (gen/CurveField/TypesExternal.lean), so `c.val` below
is that u8's numeric value. Source spans cite subtle-2.6.1/src/lib.rs,
copied from the model docstrings in gen/CurveField/FunsExternal.lean. -/
/-- `Choice::from(u8)` is the identity (the Rust `black_box` is a barrier).
subtle crate: `impl From<u8> for Choice`, subtle-2.6.1/src/lib.rs:238.
MATH: from b = ok b — total, value unchanged. The Rust body is
`Choice(black_box(input))`; `black_box` is a volatile read that only
defeats compiler optimization, semantically the identity.
WHY NEEDED: the transpiled field code builds `Choice`s through this
conversion (e.g. in `sqrt_ratio_i`); the simp lemma lets proofs step
over those calls. -/
@[simp]
theorem choice_from_u8_spec (b : Std.U8) :
subtle.Choice.Insts.CoreConvertFromU8.from b = ok b := rfl
/-- `bool::from(Choice)` tests non-zeroness.
subtle crate: `impl From<Choice> for bool`, subtle-2.6.1/src/lib.rs:153.
MATH: from c = ok (c ≠ 0); on the documented {0,1} invariant this is
exactly "c = 1".
WHY NEEDED: the field API exposes results of constant-time comparisons
as `bool` through this conversion. -/
@[simp]
theorem bool_from_choice_spec (c : subtle.Choice) :
Bool.Insts.CoreConvertFromChoice.from c = ok (c.val != 0) := rfl
/-- `u64::conditional_select(a, b, c)` keeps `a` iff `c = 0`.
subtle crate: `impl ConditionallySelectable for u64`,
subtle-2.6.1/src/lib.rs:513.
MATH: conditional_select a b c = ok (if c = 0 then a else b).
The Rust mask trick `a ^ ((-(c as i64) as u64) & (a ^ b))` agrees with
this if-then-else on the {0,1} Choice invariant (mask = 0 or all-ones).
WHY NEEDED: limbwise constant-time selection is the building block of
`FieldElement51`'s `ConditionallySelectable` impl, used by the
decompression path (`sqrt_ratio_i`). -/
@[simp]
theorem u64_conditional_select_spec (a b : Std.U64) (c : subtle.Choice) :
U64.Insts.SubtleConditionallySelectable.conditional_select a b c
= ok (if c.val = 0 then a else b) := rfl
/-- `u64::conditional_assign(self, other, c)` keeps `self` iff `c = 0`.
subtle crate: `impl ConditionallySelectable for u64`,
subtle-2.6.1/src/lib.rs:521.
MATH: conditional_assign self other c
= ok (if c = 0 then self else other)
— same mask trick as `conditional_select`, in-place flavor (Aeneas turns
`&mut self` into returning the new value).
WHY NEEDED: `sqrt_ratio_i` conditionally overwrites its candidate root
through this operation. -/
@[simp]
theorem u64_conditional_assign_spec (a b : Std.U64) (c : subtle.Choice) :
U64.Insts.SubtleConditionallySelectable.conditional_assign a b c
= ok (if c.val = 0 then a else b) := rfl
/-- `u8::ct_eq` decides equality.
subtle crate: `impl ConstantTimeEq for u8`, subtle-2.6.1/src/lib.rs:348.
MATH: ct_eq a b = ok (if a = b then 1 else 0) — the specification the
Rust xor/shift bit trick implements for ALL inputs (not just {0,1}).
WHY NEEDED: byte-wise constant-time equality underlies the slice `ct_eq`
used by the field API's equality test. -/
@[simp]
theorem u8_ct_eq_spec (a b : Std.U8) :
U8.Insts.SubtleConstantTimeEq.ct_eq a b
= ok (if a = b then 1#u8 else 0#u8) := rfl
/-! ## First facts about the generated field code
Sanity checks that the TRANSPILED definitions compute what the Rust
constants say — first uses of the `unfold`-then-`rfl`/`simp` pattern the
*Spec files build on. Path abbreviation below:
u64/field.rs = curve25519/solana-ed25519/src/backend/serial/u64/field.rs -/
/-- `FieldElement51::ZERO` is the all-zero limb array.
Rust: `FieldElement51::ZERO = FieldElement51::from_limbs([0,0,0,0,0])`,
u64/field.rs:263.
MATH: ZERO = ok [0, 0, 0, 0, 0] — the constant evaluates without panic to
the all-zero limb vector (`Array.repeat 5 0` = five copies of 0#u64).
Its denotation ⟪·⟫ = 0 is proved later in Proofs/ConstSpecs.lean.
WHY NEEDED: sanity fact; the additive identity of the field must exist
and be panic-free. -/
theorem zero_spec :
backend.serial.u64.field.FieldElement51.ZERO
= ok (Array.repeat 5#usize 0#u64) := by
-- Unfold the generated constant and its `from_limbs` constructor;
-- both sides are then the same literal term.
unfold backend.serial.u64.field.FieldElement51.ZERO
backend.serial.u64.field.FieldElement51.from_limbs
rfl
/-- `<FieldElement51 as Default>::default()` is `ZERO`.
Rust: `impl Default for FieldElement` (the serial-u64 alias of
`FieldElement51`), curve25519/solana-ed25519/src/field.rs:66-70.
MATH: default = ZERO (equal as `Result`-valued constants).
WHY NEEDED: checks the transpiler's `Default`-trait plumbing dispatches
to the right constant. -/
theorem default_eq_zero :
backend.serial.u64.field.FieldElement51.Insts.CoreDefaultDefault.default
= backend.serial.u64.field.FieldElement51.ZERO := by
-- Unfolding the Default-impl wrapper exposes ZERO itself.
unfold
backend.serial.u64.field.FieldElement51.Insts.CoreDefaultDefault.default
rfl
/-- `FieldElement51::from_limbs` never fails.
Rust: `pub(crate) const fn from_limbs(limbs: [u64; 5])`,
u64/field.rs:258-260 — a plain constructor wrapping the array.
MATH: forall limbs, from_limbs limbs = ok limbs — total identity.
WHY NEEDED: every transpiled constant (ZERO/ONE/MINUS_ONE/SQRT_M1) goes
through this constructor; the `@[simp]` lemma erases it during symbolic
execution. -/
@[simp]
theorem from_limbs_spec (limbs : Array Std.U64 5#usize) :
backend.serial.u64.field.FieldElement51.from_limbs limbs = ok limbs := rfl
end CurveFieldProofs

View file

@ -0,0 +1,190 @@
/- ──────────────────────────────────────────────────────────────────────────────
Proofs/ConstSpecs.lean — the precomputed field constants denote what they claim
WHAT THIS FILE CONTAINS
Specs for the transpiled field constants:
`ZERO`, `ONE`, `MINUS_ONE` (FieldElement51) and `SQRT_M1` (constants).
Each constant evaluates totally (no panic), satisfies the limb-bound
invariant `Bnd`, and denotes the expected element of 𝔽_p.
RUST ANALOG
- `FieldElement51::ZERO = from_limbs([0,0,0,0,0])`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:263
- `FieldElement51::ONE = from_limbs([1,0,0,0,0])`, field.rs:265
- `FieldElement51::MINUS_ONE = from_limbs([2251799813685228, 2251799813685247, ...])`,
field.rs:267-273
- `constants::SQRT_M1 = from_limbs([1718705420411056, ...])`,
curve25519/solana-ed25519/src/backend/serial/u64/constants.rs:99-105
("Precomputed value of one of the square roots of -1 (mod p)").
Transpiled in gen/CurveField/Funs.lean as `...FieldElement51.{ZERO,ONE,MINUS_ONE}`
and `backend.serial.u64.constants.SQRT_M1` (each a `Result Fe` — in the Aeneas
model a Rust `const` is a 0-argument fallible computation that we must prove `ok`).
THE MATH (what each limb vector denotes; recall the radix-2⁵¹ denotation
feVal a = a0 + a1·2⁵¹ + a2·2¹⁰² + a3·2¹⁵³ + a4·2²⁰⁴, ⟪a⟫ = feVal a mod p)
- ZERO: feVal = 0, ⟪·⟫ = 0.
- ONE: feVal = 1, ⟪·⟫ = 1.
- MINUS_ONE: limbs are [2⁵¹20, 2⁵¹1, 2⁵¹1, 2⁵¹1, 2⁵¹1], the radix-2⁵¹
spelling of p 1 = 2²⁵⁵ 20; so feVal = p 1 and ⟪·⟫ = 1.
- SQRT_M1: feVal = N := 19681161376707505956807079304988542015446066515923890162744021073123829784752,
a 255-bit number with N² ≡ 1 (mod p); checked by computing the ~510-bit
square N² and reducing mod p with kernel-verified literal arithmetic
(norm_num) — no decision procedure or native code is trusted for this.
ROLE IN THE MAIN THEOREM
`fieldImplementation` (Proofs/FieldMain.lean) needs distinguished elements 0 and 1
realized by the implementation: FieldMain obtains them from `zero_spec`/`one_spec`
(see its `spec_exists zero_spec` / `spec_exists one_spec`) and derives
impl_zero_add, impl_one_mul, impl_zero_ne_one. `minus_one_spec` and `sqrt_m1_spec`
validate the remaining precomputed constants of the extracted module — `SQRT_M1`
is the constant on which Ed25519 point decompression (`sqrt_ratio_i`) relies, so
a wrong table entry here would be a real-world key-validation bug; the spec proves
the table entry correct.
FILE RELATIONS
Imports Proofs/Denote.lean (denotation + Bnd; no other spec files needed since
constants run no arithmetic beyond `from_limbs`). Imported by Proofs/Field.lean,
hence by FieldMain.lean.
────────────────────────────────────────────────────────────────────────────── -/
import Proofs.Denote
open Aeneas Aeneas.Std Result
open curve25519_dalek
namespace CurveFieldProofs
/-! ## Casting `P - 1` into 𝔽_p gives `-1` -/
/-- No Rust analog — arithmetic helper.
MATH: ((p - 1 : N) : F_p) = -1 (since p ≡ 0 in F_p, p 1 ≡ 1).
The proof moves the -subtraction through the cast (legal because 1 ≤ p),
rewrites (p : F_p) = 0, and finishes by ring.
WHY NEEDED: both MINUS_ONE and SQRT_M1 reduce to a feVal equal to p 1
(directly, resp. after squaring mod p); this lemma converts that value into
the field element 1 in their specs. -/
theorem natCast_P_sub_one : ((P - 1 : ) : Fp) = -1 := by
-- 1 ≤ p lets Nat.cast_sub distribute the truncated subtraction
have h1 : (1 : ) ≤ P := by norm_num [P]
rw [Nat.cast_sub h1, ZMod.natCast_self]
push_cast
ring
/-! ## ZERO -/
/-- Rust: `FieldElement51::ZERO`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:263.
MATH: ZERO = ok z with limbs [0,0,0,0,0], Bnd(z, 2^51), [[z]] = 0 in F_p.
(The limb list is exposed verbatim because downstream proofs also need the
syntactic limbs, e.g. to feed `add_spec`.)
WHY NEEDED: provides the implementation's additive identity for
`fieldImplementation` (FieldMain's impl_zero_add / impl_zero_ne_one start from
`spec_exists zero_spec`). `Bnd z (2^51)` certifies that the constant satisfies
the strictest limb invariant, so it can be fed to any operation. -/
theorem zero_spec :
fe_zero ⦃ z =>
(↑z : List U64) = [0#u64, 0#u64, 0#u64, 0#u64, 0#u64] ∧
Bnd z (2^51) ∧ denote z = 0 ⦄ := by
unfold fe_zero backend.serial.u64.field.FieldElement51.ZERO
backend.serial.u64.field.FieldElement51.from_limbs
-- everything is a literal: simp evaluates Array.repeat to [0,...,0], Bnd to
-- 0 < 2⁵¹, and feVal to 0
simp [Bnd, denote, feVal, limbsVal, Array.repeat, List.replicate]
/-! ## ONE -/
/-- Rust: `FieldElement51::ONE`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:265.
MATH: ONE = ok o with limbs [1,0,0,0,0], Bnd(o, 2^51), [[o]] = 1 in F_p
(feVal = 1 + 0·2⁵¹ + ... = 1).
WHY NEEDED: the implementation's multiplicative identity; FieldMain's
impl_one_mul and impl_zero_ne_one consume it via `spec_exists one_spec`. -/
theorem one_spec : fe_one ⦃ o => Bnd o (2^51) ∧ denote o = 1 ⦄ := by
unfold fe_one backend.serial.u64.field.FieldElement51.ONE
backend.serial.u64.field.FieldElement51.from_limbs
-- literal evaluation: feVal [1,0,0,0,0] = 1, and 1 < 2⁵¹
simp [Bnd, denote, feVal, limbsVal, Array.make]
/-! ## MINUS_ONE -/
/-- Rust: `FieldElement51::MINUS_ONE`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:267-273.
MATH: MINUS_ONE = ok m with Bnd(m, 2^52) and [[m]] = -1 in F_p.
The limbs are
[2251799813685228, 2251799813685247, 2251799813685247,
2251799813685247, 2251799813685247]
= [2^51 - 20, 2^51 - 1, 2^51 - 1, 2^51 - 1, 2^51 - 1],
the radix-2⁵¹ "borrowed" spelling of p 1 (cf. sixteen_p in SubNegSpec.lean):
(2^51-20) + (2^51-1)·(2^51 + 2^102 + 2^153 + 2^204) = 2^255 - 20 = p - 1,
so feVal m = p 1 and ⟪m⟫ = 1 by `natCast_P_sub_one`.
(Each limb is < 2⁵¹; the bound is stated as the looser 2⁵² used uniformly for
reduced values in this development.)
WHY NEEDED: validates the third hard-coded table constant of the extracted
module: a typo in any of those 16-digit limbs would silently negate nothing.
It also documents that 1 has a canonical bounded representative, the element
`negate`/`sub` effectively build from. -/
theorem minus_one_spec : fe_minus_one ⦃ m => Bnd m (2^52) ∧ denote m = -1 ⦄ := by
unfold fe_minus_one backend.serial.u64.field.FieldElement51.MINUS_ONE
backend.serial.u64.field.FieldElement51.from_limbs
-- simp discharges the Bnd conjunct and evaluates `feVal` to the literal
-- 2²⁵⁵ 20 = P 1; the remaining goal is `(P 1 : 𝔽_p) = -1`.
simp [Bnd, denote, feVal, limbsVal, Array.make]
-- specialize natCast_P_sub_one to the literal that simp produced for P 1
have h := natCast_P_sub_one
have hc : (P - 1 : ) =
57896044618658097711785492504343953926634992332820282019728792003956564819948 := by
norm_num [P]
rw [hc] at h
exact_mod_cast h
/-! ## SQRT_M1 -/
/-- Rust: `constants::SQRT_M1`,
curve25519/solana-ed25519/src/backend/serial/u64/constants.rs:99-105 —
"Precomputed value of one of the square roots of -1 (mod p)" (it exists since
p ≡ 1 mod 4).
MATH: SQRT_M1 = ok s with Bnd(s, 2^52) and [[s]] * [[s]] = -1 in F_p.
The limbs [1718705420411056, 234908883556509, 2233514472574048,
2117202627021982, 765476049583133] denote the 255-bit number
N = 19681161376707505956807079304988542015446066515923890162744021073123829784752
and the spec certifies N² ≡ 1 (mod p). The verification is brute literal
arithmetic: `norm_num` makes the kernel compute the ~510-bit square N² and its
remainder mod p, equal to p 1 (lemma `hmod` below), then `natCast_P_sub_one`
converts p 1 to 1. (Each limb is < 2⁵¹; stated at the uniform 2⁵² bound.)
WHY NEEDED: `sqrt_ratio_i` — the square-root routine behind Ed25519 point
decompression — multiplies by this table constant whenever the candidate root
fails its sign/QR check; a corrupted constant would make decompression accept or
produce wrong points. This spec pins the precomputed table entry to its defining
equation, complementing the operation-level proofs that feed FieldMain. -/
theorem sqrt_m1_spec :
backend.serial.u64.constants.SQRT_M1 ⦃ s => Bnd s (2^52) ∧ denote s * denote s = -1 ⦄ := by
unfold backend.serial.u64.constants.SQRT_M1
backend.serial.u64.field.FieldElement51.from_limbs
-- simp discharges the Bnd conjunct and evaluates `feVal` to the literal N
-- (the 255-bit value of the SQRT_M1 limbs); the remaining goal is
-- `(N : 𝔽_p) * (N : 𝔽_p) = -1`.
simp [Bnd, denote, feVal, limbsVal, Array.make]
-- N² ≡ P 1 (mod P), checked by literal arithmetic on .
-- (norm_num evaluates the 510-bit product and the division by P inside the kernel)
have hmod :
((19681161376707505956807079304988542015446066515923890162744021073123829784752 *
19681161376707505956807079304988542015446066515923890162744021073123829784752 : ))
% P = P - 1 := by
norm_num [P]
-- assemble: (N:Fp)·(N:Fp) = (N·N : Fp) = ((N·N mod P) : Fp) = ((P1) : Fp) = 1
have key :
((19681161376707505956807079304988542015446066515923890162744021073123829784752 : ) : Fp) *
((19681161376707505956807079304988542015446066515923890162744021073123829784752 : ) : Fp)
= -1 := by
rw [← Nat.cast_mul, ← ZMod.natCast_mod, hmod, natCast_P_sub_one]
exact_mod_cast key
end CurveFieldProofs

View file

@ -0,0 +1,312 @@
/- ────────────────────────────────────────────────────────────────────────────
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 :=
Shared0FieldElement51.Insts.CoreOpsArithAddSharedAFieldElement51FieldElement51.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 :=
Shared0FieldElement51.Insts.CoreOpsArithSubSharedAFieldElement51FieldElement51.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 :=
Shared0FieldElement51.Insts.CoreOpsArithMulSharedAFieldElement51FieldElement51.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

View file

@ -0,0 +1,717 @@
/- ───────────────────────────────────────────────────────────────────────────
Proofs/FeQ.lean — `FeQ`: the transpiled Rust code as a LITERAL mathlib
`Field` instance (and a ring isomorphism `FeQ ≃+* 𝔽_p`).
CONTEXT. The Rust crate curve25519/solana-ed25519 implements 𝔽_p,
p = 2²⁵⁵ 19, on five radix-2⁵¹ u64 limbs (`FieldElement51`); the code was
transpiled mechanically (Charon + Aeneas) to gen/CurveField/{Types,Funs}.lean.
Proofs/FieldMain.lean proved THE MAIN THEOREM (`fieldImplementation`): under
the dalek limb-bound invariant every transpiled operation is TOTAL (panic-
free) and realizes the corresponding 𝔽_p operation through the denotation
⟪·⟫ : Fe → 𝔽_p.
WHY THIS FILE. `fieldImplementation` is a *certificate about* the code; the
field axioms appear there as `impl_*` corollaries, each phrased "the ops run
and the denotations satisfy the law". One step remains to make the claim
"the Rust code IS a field" literally type-check:
instance : Field FeQ
where every CORE structure field (add, sub, neg, mul, inv, zero, one) is
*defined by running the transpiled Rust functions*. A `Field Fe` instance
is impossible (see FieldMain.lean's header):
* the representation is REDUNDANT — one field element has many limb
vectors, so `mul_comm` etc. are FALSE as equalities of limb vectors;
* the operations are PARTIAL — they live in the `Result` monad and are
only guaranteed total on bounded inputs.
The QUOTIENT fixes both defects at once, and it is the *canonical* fix:
1. restrict to the valid elements `VFe := {a : Fe // Bnd a 2⁵²}`
(totality holds there: every op returns `ok` — FieldMain's runners);
2. quotient by equality of denotations: `FeQ := VFe / (⟪·⟫ = ⟪·⟫)`
(redundancy disappears: a class IS a field element).
`FeQ` is therefore "the type of field elements as the Rust code represents
them", with no information added and none removed — and on it the field
laws hold as REAL equalities, so a genuine `Field FeQ` instance exists.
HOW THE OPERATIONS ARE DEFINED — `Classical.choose` extraction. FieldMain's
runners are existence theorems, e.g.
run_mul : Bnd a 2⁵⁴ → Bnd b 2⁵⁴ →
∃ r, fe_mul a b = ok r ∧ Bnd r 2⁵² ∧ ⟪r⟫ = ⟪a⟫·⟪b⟫.
`(run_mul …).choose` names THE result of that run: by the equation
`fe_mul a b = ok r` the value `r` is uniquely determined — `Result` is
deterministic, `ok` is injective — so choice does not "pick" anything, it
merely gives the already-determined machine result a Lean name (the
functions cannot be executed inside Lean terms directly because they
return in `Result`; `choose` is the standard bridge from "the run
succeeds" to "the value of the run"). The accompanying `.choose_spec`
hands back the program equation (`v*_runs` below — the receipt that the
definition really is the Rust run) and the denotation fact (`v*_denote`).
CONTENTS, in order:
§1 `run_reduce`, `run_add_red` — two more runners: `reduce` is total on
ANY input and denotation-preserving; `add`-then-`reduce` restores the
2⁵² bound that bare limbwise `add` (output 2⁵³) does not.
§2 `VFe`, the setoid (a ≈ b ↔ ⟪a⟫ = ⟪b⟫), and `FeQ` — the carrier.
§3 `vzero vone vadd vsub vneg vmul vinv` — the operations on `VFe`,
each extracted from a runner, with `_runs` and `_denote` facts.
§4 congruences + `Quotient.map/map₂` lifts `qadd … qinv` to `FeQ`.
§5 THE BRIDGE `denoteQ : FeQ → 𝔽_p` — injective AND surjective — and
the extensionality principle `feq_ext`.
§6 every field law for the q-operations (each proof: drop to 𝔽_p via
`feq_ext`, rewrite with the denotation equations, close with the
𝔽_p law).
§7 `instance : CommRing FeQ` and `instance : Field FeQ` — built
DIRECTLY (layered structure literals; not via Function.Injective.field),
with the Rust-run operations as the structure fields.
§8 `feQRingEquiv : FeQ ≃+* 𝔽_p`, `Fintype FeQ` (FeQ is a FINITE field),
and the axiom audit (`#print axioms` — the three standard axioms).
AXIOM HYGIENE: `#print axioms feQRingEquiv` reports only
[propext, Classical.choice, Quot.sound] — no sorry, no native_decide, no
custom axiom (the 4 axioms modeling externals in
gen/CurveField/FunsExternal.lean are outside the dependency cone).
`Classical.choice` enters exactly through the `choose` extraction
explained above (and through mathlib's `Field 𝔽_p`); `Quot.sound` through
the quotient. Nothing in gen/ (the transpiled code) was modified.
Imports: Proofs/FieldMain.lean (the main theorem and its runners, plus —
transitively — everything else). Imported by: nothing; this file is the
capstone of the development.
─────────────────────────────────────────────────────────────────────── -/
import Proofs.FieldMain
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 4000000
namespace CurveFieldProofs
/-! ## §1 Two more runners: `reduce`, and `add` followed by `reduce`
`run_add` (FieldMain.lean) outputs `Bnd r 2⁵³` — limbwise addition does not
reduce, so its result is NOT a valid element (`Valid` = `Bnd · 2⁵²`) and
cannot serve as the result of a `VFe`-level addition. The Rust crate's own
answer is `FieldElement51::reduce` (one carry pass); composing the transpiled
`add` with the transpiled `reduce` yields a bound-restoring, denotation-
correct addition. Both runners below follow the `run_*` format of
FieldMain.lean: plain existentials `∃ r, code = ok r ∧ Bnd ∧ ⟪·⟫-equation`. -/
/-- `reduce` runs on ANY input and preserves the denotation.
RUST ANALOG: `FieldElement51::reduce`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:290-323 (one
parallel carry pass; the top carry re-enters at limb 0 multiplied by 19).
MATH: forall a : Fe, exists r,
fe_reduce a = ok r, Bnd(r, 2^52), ⟪r⟫ = ⟪a⟫.
LaTeX: $\forall a\ \exists r,\ \mathrm{reduce}(a)=\mathrm{ok}\,r \wedge
\mathrm{Bnd}(r,2^{52}) \wedge \llbracket r\rrbracket =
\llbracket a\rrbracket$.
Note there is NO precondition: `reduce_spec` (ReduceSpec.lean) is total on
arbitrary u64 limbs. Its exact accounting `feVal r + p·(a₄ div 2⁵¹) =
feVal a` becomes `⟪r⟫ = ⟪a⟫` after one cast to 𝔽_p, because the term
`p·…` is a multiple of p and vanishes (`ZMod.natCast_self : (p : 𝔽_p) = 0`).
The output bound 2⁵¹ + 19·2¹³ is weakened to the uniform 2⁵².
WHY NEEDED: the second half of `run_add_red`; gives `vadd` (§3) a result
that is again `Valid`. -/
theorem run_reduce (a : Fe) :
∃ r, fe_reduce a = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫ := by
-- name the limbs (reduce_spec needs them) and run the spec
obtain ⟨a0, a1, a2, a3, a4, hl⟩ := Fe.exists_limbs a
obtain ⟨r, hr, hbnd, hval⟩ := spec_exists (reduce_spec a a0 a1 a2 a3 a4 hl)
refine ⟨r, hr, hbnd.mono (by norm_num), ?_⟩ -- 2^51 + 19·2^13 ≤ 2^52
-- hval : feVal r + P * (a4.val / 2^51) = feVal a (exact, over ).
-- Cast both sides into 𝔽_p; the P-multiple dies (P ≡ 0 mod P).
have hcast : ((feVal r + P * (a4.val / 2^51) : ) : Fp)
= ((feVal a : ) : Fp) := by rw [hval]
simpa [denote, ZMod.natCast_self] using hcast
/-- Valid + Valid → Valid addition: the transpiled `add` CHAINED INTO the
transpiled `reduce` (at the program level, with the monadic `do`).
RUST ANALOG: `&a + &b` followed by `(…).reduce()` — exactly what the crate
itself does whenever a sum must satisfy the limb discipline again (e.g.
inside `AddAssign`/point formulas); both functions are the transpiled
originals, composed in the `Result` monad.
MATH: Bnd(a,2^52), Bnd(b,2^52) ==> exists r,
(add a b >>= reduce) = ok r, Bnd(r, 2^52), ⟪r⟫ = ⟪a⟫ + ⟪b⟫.
LaTeX: $\llbracket r\rrbracket = \llbracket a\rrbracket +
\llbracket b\rrbracket$ with $r$ again 2⁵²-bounded.
WHY NEEDED: this is the program `vadd` (§3) extracts its value from —
bare `run_add`'s 2⁵³ output bound would leave `VFe` not closed under
addition. -/
theorem run_add_red {a b : Fe} (ha : Bnd a (2^52)) (hb : Bnd b (2^52)) :
∃ r, (do
let s ← fe_add a b
fe_reduce s) = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫ + ⟪b⟫ := by
-- run the limbwise add (result s, Bnd 2^53, ⟪s⟫ = ⟪a⟫+⟪b⟫) …
obtain ⟨s, hs, _, hsv⟩ := run_add ha hb
-- … then the carry pass (any input is legal; ⟪·⟫ preserved)
obtain ⟨r, hr, hrb, hrv⟩ := run_reduce s
refine ⟨r, ?_, hrb, ?_⟩
· -- the do-block: once `fe_add a b` is rewritten to `ok s`, the bind
-- reduces definitionally and the goal IS `fe_reduce s = ok r`.
rw [hs]
exact hr
· rw [hrv, hsv]
/-! ## §2 The carrier: valid elements, the setoid, and the quotient `FeQ` -/
/-- A VALID field element: a limb vector together with the proof that it
satisfies the working invariant `Bnd · 2⁵²` (= `Valid`, FieldMain.lean) —
the bound every transpiled operation re-establishes and `encode`
satisfies.
RUST ANALOG: a `FieldElement51` that respects the crate's documented limb
discipline (u64/field.rs:26-42) — i.e. every value the Rust API actually
produces.
MATH: VFe = { a : Fe | all limbs of a are < 2^52 }.
WHY NEEDED: on `VFe` every operation of the API is TOTAL (the runners'
preconditions hold), so operations extracted from the runners are honest
functions `VFe → VFe`. -/
def VFe := {a : Fe // Bnd a (2^52)}
/-- Two valid elements are equivalent iff they DENOTE the same element of 𝔽_p.
MATH: a ≈ b iff ⟪a⟫ = ⟪b⟫ — the kernel of the denotation map.
Reflexivity/symmetry/transitivity are inherited from `=` on 𝔽_p.
WHY NEEDED: the limb representation is redundant (e.g. the `mul` carry
chains produce DIFFERENT limb vectors for `a*b` and `b*a`); identifying
denotation-equal vectors is exactly what makes the field laws equalities. -/
instance vfeSetoid : Setoid VFe :=
⟨fun a b => ⟪a.1⟫ = ⟪b.1⟫,
fun _ => rfl, fun h => h.symm, fun h₁ h₂ => h₁.trans h₂⟩
/-- Unfolding lemma for the setoid relation (definitionally true).
WHY NEEDED: lets later proofs move between `a ≈ b` and the denotation
equation without relying on definitional unfolding inside `rw`. -/
theorem vfe_equiv_iff {a b : VFe} : a ≈ b ↔ ⟪a.1⟫ = ⟪b.1⟫ := Iff.rfl
/-- **The type of field elements, as the Rust code represents them.**
MATH: FeQ = VFe / ≈ — bounded limb vectors modulo equal denotation.
LaTeX: $\mathrm{FeQ} = \{a : \mathrm{Fe} \mid \mathrm{Bnd}(a,2^{52})\}
/\ (\llbracket\cdot\rrbracket = \llbracket\cdot\rrbracket)$.
An element of `FeQ` is an equivalence class of valid limb vectors; the
bridge `denoteQ` (§5) shows `FeQ` is in BIJECTION with 𝔽_p. On this type
— and only on this type — "the Rust operations form a field" can be a
literal `Field` instance (§7).
WHY NEEDED: the whole point of the file. -/
def FeQ := Quotient vfeSetoid
/-! ## §3 The operations on `VFe`: extracted from the Rust runs
Each definition below is `Classical.choose` of a runner — i.e. *the value the
transpiled Rust function returns on the given inputs* (see the file header:
the run equation `… = ok r` pins the value uniquely; `choose` only names it).
For each operation we record two facts:
* `v*_runs` — the program equation `transpiled-code inputs = ok (v* …)`,
the RECEIPT that the definition is the Rust run (kept
reachable for documentation; the algebra below only needs
the denotation);
* `v*_denote` — the denotation equation, e.g. ⟪vmul a b⟫ = ⟪a⟫·⟪b⟫,
which powers every law in §6.
All definitions are `noncomputable` — Lean cannot RUN the extracted machine
code (it lives in the `Result` monad and `choose` is classical) — but they
are definitionally tied to it by the `v*_runs` equations. -/
/-- The zero of the implementation: the run of `FieldElement51::ZERO`
(limbs [0,0,0,0,0]). RUST ANALOG: u64/field.rs:263.
MATH: ⟪vzero⟫ = 0 (see `vzero_denote`). -/
noncomputable def vzero : VFe :=
⟨run_zero.choose, run_zero.choose_spec.2.1⟩
/-- Receipt: `vzero` IS the value of the transpiled constant. -/
theorem vzero_runs : fe_zero = ok vzero.1 := run_zero.choose_spec.1
/-- MATH: ⟪vzero⟫ = 0 in 𝔽_p. -/
theorem vzero_denote : ⟪vzero.1⟫ = 0 := run_zero.choose_spec.2.2
/-- The one of the implementation: the run of `FieldElement51::ONE`
(limbs [1,0,0,0,0]). RUST ANALOG: u64/field.rs:265. -/
noncomputable def vone : VFe :=
⟨run_one.choose, run_one.choose_spec.2.1⟩
/-- Receipt: `vone` IS the value of the transpiled constant. -/
theorem vone_runs : fe_one = ok vone.1 := run_one.choose_spec.1
/-- MATH: ⟪vone⟫ = 1 in 𝔽_p. -/
theorem vone_denote : ⟪vone.1⟫ = 1 := run_one.choose_spec.2.2
/-- Addition on valid elements: the value of the Rust run
`add a b >>= reduce` (see `run_add_red` — the reduce restores the 2⁵²
bound that bare limbwise add does not).
RUST ANALOG: `&a + &b` then `.reduce()` (u64/field.rs:68-72, 290-323). -/
noncomputable def vadd (a b : VFe) : VFe :=
⟨(run_add_red a.2 b.2).choose, (run_add_red a.2 b.2).choose_spec.2.1⟩
/-- Receipt: `vadd a b` IS the value of the transpiled add-then-reduce run. -/
theorem vadd_runs (a b : VFe) :
(do
let s ← fe_add a.1 b.1
fe_reduce s) = ok (vadd a b).1 :=
(run_add_red a.2 b.2).choose_spec.1
/-- MATH: ⟪vadd a b⟫ = ⟪a⟫ + ⟪b⟫ in 𝔽_p. -/
theorem vadd_denote (a b : VFe) : ⟪(vadd a b).1⟫ = ⟪a.1⟫ + ⟪b.1⟫ :=
(run_add_red a.2 b.2).choose_spec.2.2
/-- Subtraction on valid elements: the value of the Rust run of `sub`
(the +16p underflow trick, then reduce — already 2⁵²-bounded, no extra
reduce needed). RUST ANALOG: `&a - &b`, u64/field.rs:84-101. -/
noncomputable def vsub (a b : VFe) : VFe :=
⟨(run_sub (valid54 a.2) (valid54 b.2)).choose,
(run_sub (valid54 a.2) (valid54 b.2)).choose_spec.2.1⟩
/-- Receipt: `vsub a b` IS the value of the transpiled `sub` run. -/
theorem vsub_runs (a b : VFe) : fe_sub a.1 b.1 = ok (vsub a b).1 :=
(run_sub (valid54 a.2) (valid54 b.2)).choose_spec.1
/-- MATH: ⟪vsub a b⟫ = ⟪a⟫ ⟪b⟫ in 𝔽_p. -/
theorem vsub_denote (a b : VFe) : ⟪(vsub a b).1⟫ = ⟪a.1⟫ - ⟪b.1⟫ :=
(run_sub (valid54 a.2) (valid54 b.2)).choose_spec.2.2
/-- Negation on valid elements: the value of the Rust run of `negate`
(16p a limbwise, then reduce). RUST ANALOG: u64/field.rs:276-286. -/
noncomputable def vneg (a : VFe) : VFe :=
⟨(run_neg (valid54 a.2)).choose, (run_neg (valid54 a.2)).choose_spec.2.1⟩
/-- Receipt: `vneg a` IS the value of the transpiled `negate` run. -/
theorem vneg_runs (a : VFe) : fe_neg a.1 = ok (vneg a).1 :=
(run_neg (valid54 a.2)).choose_spec.1
/-- MATH: ⟪vneg a⟫ = ⟪a⟫ in 𝔽_p. -/
theorem vneg_denote (a : VFe) : ⟪(vneg a).1⟫ = -⟪a.1⟫ :=
(run_neg (valid54 a.2)).choose_spec.2.2
/-- Multiplication on valid elements: the value of the Rust run of `mul`
(radix-2⁵¹ schoolbook with ×19 folding, u128 carry chain).
RUST ANALOG: `&a * &b`, u64/field.rs:115-213. -/
noncomputable def vmul (a b : VFe) : VFe :=
⟨(run_mul (valid54 a.2) (valid54 b.2)).choose,
(run_mul (valid54 a.2) (valid54 b.2)).choose_spec.2.1⟩
/-- Receipt: `vmul a b` IS the value of the transpiled `mul` run. -/
theorem vmul_runs (a b : VFe) : fe_mul a.1 b.1 = ok (vmul a b).1 :=
(run_mul (valid54 a.2) (valid54 b.2)).choose_spec.1
/-- MATH: ⟪vmul a b⟫ = ⟪a⟫ · ⟪b⟫ in 𝔽_p. -/
theorem vmul_denote (a b : VFe) : ⟪(vmul a b).1⟫ = ⟪a.1⟫ * ⟪b.1⟫ :=
(run_mul (valid54 a.2) (valid54 b.2)).choose_spec.2.2
/-- Inversion on valid elements: the value of the Rust run of `invert`
(x^(p2) by the pow22501 addition chain: 254 squarings + 11 mults;
maps 0 to 0 exactly like mathlib's `0⁻¹ = 0`).
RUST ANALOG: field.rs:239-248. -/
noncomputable def vinv (a : VFe) : VFe :=
⟨(run_invert (valid54 a.2)).choose, (run_invert (valid54 a.2)).choose_spec.2.1⟩
/-- Receipt: `vinv a` IS the value of the transpiled `invert` run. -/
theorem vinv_runs (a : VFe) : fe_invert a.1 = ok (vinv a).1 :=
(run_invert (valid54 a.2)).choose_spec.1
/-- MATH: ⟪vinv a⟫ = ⟪a⟫⁻¹ in 𝔽_p (with the 0 ↦ 0 convention on both sides). -/
theorem vinv_denote (a : VFe) : ⟪(vinv a).1⟫ = ⟪a.1⟫⁻¹ :=
(run_invert (valid54 a.2)).choose_spec.2.2
/-! ## §4 Lifting to the quotient
Each operation descends to `FeQ` because it RESPECTS the relation: if the
inputs denote the same field elements, so do the outputs — immediate from the
`v*_denote` equations, since the 𝔽_p-side value depends only on the input
denotations. (This is the formal content of "the result of the Rust run is
well-defined up to representation".) -/
/-- `vadd` respects ≈ (congruence for `Quotient.map₂`).
MATH: ⟪a⟫=⟪a'⟫ and ⟪b⟫=⟪b'⟫ ⟹ ⟪vadd a b⟫ = ⟪a⟫+⟪b⟫ = ⟪a'⟫+⟪b'⟫ = ⟪vadd a' b'⟫. -/
theorem vadd_congr : ∀ ⦃a a' : VFe⦄, a ≈ a' → ∀ ⦃b b' : VFe⦄, b ≈ b' →
vadd a b ≈ vadd a' b' := by
intro a a' ha b b' hb
exact vfe_equiv_iff.mpr (by
rw [vadd_denote, vadd_denote, vfe_equiv_iff.mp ha, vfe_equiv_iff.mp hb])
/-- `vsub` respects ≈. -/
theorem vsub_congr : ∀ ⦃a a' : VFe⦄, a ≈ a' → ∀ ⦃b b' : VFe⦄, b ≈ b' →
vsub a b ≈ vsub a' b' := by
intro a a' ha b b' hb
exact vfe_equiv_iff.mpr (by
rw [vsub_denote, vsub_denote, vfe_equiv_iff.mp ha, vfe_equiv_iff.mp hb])
/-- `vmul` respects ≈. -/
theorem vmul_congr : ∀ ⦃a a' : VFe⦄, a ≈ a' → ∀ ⦃b b' : VFe⦄, b ≈ b' →
vmul a b ≈ vmul a' b' := by
intro a a' ha b b' hb
exact vfe_equiv_iff.mpr (by
rw [vmul_denote, vmul_denote, vfe_equiv_iff.mp ha, vfe_equiv_iff.mp hb])
/-- `vneg` respects ≈. -/
theorem vneg_congr : ∀ ⦃a a' : VFe⦄, a ≈ a' → vneg a ≈ vneg a' := by
intro a a' ha
exact vfe_equiv_iff.mpr (by
rw [vneg_denote, vneg_denote, vfe_equiv_iff.mp ha])
/-- `vinv` respects ≈. -/
theorem vinv_congr : ∀ ⦃a a' : VFe⦄, a ≈ a' → vinv a ≈ vinv a' := by
intro a a' ha
exact vfe_equiv_iff.mpr (by
rw [vinv_denote, vinv_denote, vfe_equiv_iff.mp ha])
/- The q-operations: the Rust-run operations, lifted to equivalence classes.
`Quotient.map₂ f h ⟦a⟧ ⟦b⟧ = ⟦f a b⟧` definitionally, so each q-op applied
to classes literally computes "run the Rust code on representatives and
take the class of the result". -/
/-- Addition on `FeQ` (Rust `add` + `reduce`, lifted). -/
noncomputable def qadd : FeQ → FeQ → FeQ := Quotient.map₂ vadd vadd_congr
/-- Subtraction on `FeQ` (Rust `sub`, lifted). -/
noncomputable def qsub : FeQ → FeQ → FeQ := Quotient.map₂ vsub vsub_congr
/-- Multiplication on `FeQ` (Rust `mul`, lifted). -/
noncomputable def qmul : FeQ → FeQ → FeQ := Quotient.map₂ vmul vmul_congr
/-- Negation on `FeQ` (Rust `negate`, lifted). -/
noncomputable def qneg : FeQ → FeQ := Quotient.map vneg vneg_congr
/-- Inversion on `FeQ` (Rust `invert`, lifted). -/
noncomputable def qinv : FeQ → FeQ := Quotient.map vinv vinv_congr
/-- Zero of `FeQ` (the class of the Rust `ZERO` constant). -/
noncomputable def qzero : FeQ := ⟦vzero⟧
/-- One of `FeQ` (the class of the Rust `ONE` constant). -/
noncomputable def qone : FeQ := ⟦vone⟧
/-! Notation instances: register the q-operations as the meaning of
`+ - * ⁻¹ 0 1` on `FeQ`. Declared BEFORE the ring/field structures so that
(a) mathlib's recursor defaults (`nsmulRec`/`zsmulRec`, which need standalone
`Zero`/`Add`/`Neg` instances) can fire, and (b) the structures below can cite
exactly these operations as their data fields. -/
noncomputable instance : Add FeQ := ⟨qadd⟩
noncomputable instance : Sub FeQ := ⟨qsub⟩
noncomputable instance : Mul FeQ := ⟨qmul⟩
noncomputable instance : Neg FeQ := ⟨qneg⟩
noncomputable instance : Inv FeQ := ⟨qinv⟩
noncomputable instance : Zero FeQ := ⟨qzero⟩
noncomputable instance : One FeQ := ⟨qone⟩
/-! ## §5 THE BRIDGE: `denoteQ : FeQ → 𝔽_p` is a bijection
The denotation ⟪·⟫ : Fe → 𝔽_p is neither injective (redundant limbs) nor
total-friendly (unbounded elements break the ops). On `FeQ` both defects are
gone: `denoteQ` is INJECTIVE by construction of the quotient and SURJECTIVE
by `encode` (Field.lean) — `FeQ` and 𝔽_p are the same field in different
clothes, which §8 upgrades to a ring isomorphism. -/
/-- The denotation of an equivalence class: well-defined because the relation
IS "equal denotation" (the congruence proof is the identity).
MATH: denoteQ ⟦a⟧ = ⟪a⟫. -/
noncomputable def denoteQ : FeQ → Fp :=
Quotient.lift (fun a : VFe => ⟪a.1⟫) (fun _ _ h => h)
/-- Computation rule for `denoteQ` on classes (definitional). -/
@[simp] theorem denoteQ_mk (a : VFe) : denoteQ ⟦a⟧ = ⟪a.1⟫ := rfl
/-- `denoteQ` is INJECTIVE: equal denotations ⟹ equal classes.
MATH: denoteQ x = denoteQ y ⟹ x = y — quotienting by the kernel of ⟪·⟫
makes the induced map injective (`Quotient.sound` does all the work).
WHY NEEDED: half of "FeQ ≅ 𝔽_p"; powers the extensionality `feq_ext`. -/
theorem denoteQ_injective : Function.Injective denoteQ := by
intro x y
refine Quotient.inductionOn₂ x y fun a b h => ?_
exact Quotient.sound h
/-- `denoteQ` is SURJECTIVE: every element of 𝔽_p is the denotation of a
class — witness: the class of `encode y` (the canonical base-2⁵¹ digits
of y, bounded by 2⁵¹ ≤ 2⁵², Field.lean).
MATH: forall y : 𝔽_p, exists x : FeQ, denoteQ x = y.
WHY NEEDED: the other half of "FeQ ≅ 𝔽_p". -/
theorem denoteQ_surjective : Function.Surjective denoteQ := fun y =>
⟨⟦(⟨encode y, (encode_bnd y).mono (by norm_num)⟩ : VFe)⟧, denote_encode y⟩
/-- EXTENSIONALITY for `FeQ`: two classes are equal iff they denote the same
element of 𝔽_p.
MATH: x = y ⟺ denoteQ x = denoteQ y.
(⟸ is `denoteQ_injective`, i.e. induction on both quotients +
`Quotient.sound`; ⟹ is `congrArg`.)
WHY NEEDED: THE proof device of §6/§7 — every field axiom for `FeQ` drops
through `feq_ext.mpr` to an equation in 𝔽_p, where mathlib's field theory
closes it. -/
theorem feq_ext {x y : FeQ} : x = y ↔ denoteQ x = denoteQ y :=
⟨fun h => by rw [h], fun h => denoteQ_injective h⟩
/-! Denotation equations for the q-operations: `denoteQ` is a homomorphism
for every Rust-run operation. Each proof is quotient induction + the
`v*_denote` fact of §3 (definitional on representatives). -/
/-- MATH: denoteQ (qadd x y) = denoteQ x + denoteQ y. -/
theorem denoteQ_qadd (x y : FeQ) :
denoteQ (qadd x y) = denoteQ x + denoteQ y := by
refine Quotient.inductionOn₂ x y fun a b => ?_
exact vadd_denote a b
/-- MATH: denoteQ (qsub x y) = denoteQ x denoteQ y. -/
theorem denoteQ_qsub (x y : FeQ) :
denoteQ (qsub x y) = denoteQ x - denoteQ y := by
refine Quotient.inductionOn₂ x y fun a b => ?_
exact vsub_denote a b
/-- MATH: denoteQ (qmul x y) = denoteQ x · denoteQ y. -/
theorem denoteQ_qmul (x y : FeQ) :
denoteQ (qmul x y) = denoteQ x * denoteQ y := by
refine Quotient.inductionOn₂ x y fun a b => ?_
exact vmul_denote a b
/-- MATH: denoteQ (qneg x) = denoteQ x. -/
theorem denoteQ_qneg (x : FeQ) : denoteQ (qneg x) = -denoteQ x := by
refine Quotient.inductionOn x fun a => ?_
exact vneg_denote a
/-- MATH: denoteQ (qinv x) = (denoteQ x)⁻¹. -/
theorem denoteQ_qinv (x : FeQ) : denoteQ (qinv x) = (denoteQ x)⁻¹ := by
refine Quotient.inductionOn x fun a => ?_
exact vinv_denote a
/-- MATH: denoteQ qzero = 0. -/
theorem denoteQ_qzero : denoteQ qzero = 0 := vzero_denote
/-- MATH: denoteQ qone = 1. -/
theorem denoteQ_qone : denoteQ qone = 1 := vone_denote
/-! ## §6 The field laws for the q-operations
Every law has the same one-line proof skeleton:
feq_ext.mpr (drop to 𝔽_p) → rewrite with denoteQ_q* → the 𝔽_p law (ring /
field_simp / mul_inv_cancel₀).
This is precisely the transfer "the implementation satisfies the axiom
because 𝔽_p does and the denotations agree" — the same content as the
`impl_*` corollaries of FieldMain.lean, but now as REAL equalities on `FeQ`. -/
/-- (x+y)+z = x+(y+z) on FeQ. -/
theorem qadd_assoc (x y z : FeQ) : qadd (qadd x y) z = qadd x (qadd y z) :=
feq_ext.mpr (by simp only [denoteQ_qadd]; ring)
/-- x+y = y+x on FeQ. -/
theorem qadd_comm (x y : FeQ) : qadd x y = qadd y x :=
feq_ext.mpr (by simp only [denoteQ_qadd]; ring)
/-- 0+x = x on FeQ. -/
theorem qzero_add (x : FeQ) : qadd qzero x = x :=
feq_ext.mpr (by simp only [denoteQ_qadd, denoteQ_qzero]; ring)
/-- x+0 = x on FeQ. -/
theorem qadd_zero (x : FeQ) : qadd x qzero = x :=
feq_ext.mpr (by simp only [denoteQ_qadd, denoteQ_qzero]; ring)
/-- (x)+x = 0 on FeQ — additive inverses, computed by Rust `negate`. -/
theorem qneg_add_cancel (x : FeQ) : qadd (qneg x) x = qzero :=
feq_ext.mpr (by simp only [denoteQ_qadd, denoteQ_qneg, denoteQ_qzero]; ring)
/-- xy = x+(y) on FeQ: the Rust `sub` agrees with `add`-of-`negate`
(denotationally — the limb-level programs are different!). -/
theorem qsub_eq_add_neg (x y : FeQ) : qsub x y = qadd x (qneg y) :=
feq_ext.mpr (by simp only [denoteQ_qsub, denoteQ_qadd, denoteQ_qneg]; ring)
/-- (x·y)·z = x·(y·z) on FeQ. -/
theorem qmul_assoc (x y z : FeQ) : qmul (qmul x y) z = qmul x (qmul y z) :=
feq_ext.mpr (by simp only [denoteQ_qmul]; ring)
/-- x·y = y·x on FeQ (false at limb level, true on the quotient!). -/
theorem qmul_comm (x y : FeQ) : qmul x y = qmul y x :=
feq_ext.mpr (by simp only [denoteQ_qmul]; ring)
/-- 1·x = x on FeQ. -/
theorem qone_mul (x : FeQ) : qmul qone x = x :=
feq_ext.mpr (by simp only [denoteQ_qmul, denoteQ_qone]; ring)
/-- x·1 = x on FeQ. -/
theorem qmul_one (x : FeQ) : qmul x qone = x :=
feq_ext.mpr (by simp only [denoteQ_qmul, denoteQ_qone]; ring)
/-- x·(y+z) = x·y + x·z on FeQ. -/
theorem qleft_distrib (x y z : FeQ) :
qmul x (qadd y z) = qadd (qmul x y) (qmul x z) :=
feq_ext.mpr (by simp only [denoteQ_qmul, denoteQ_qadd]; ring)
/-- (x+y)·z = x·z + y·z on FeQ. -/
theorem qright_distrib (x y z : FeQ) :
qmul (qadd x y) z = qadd (qmul x z) (qmul y z) :=
feq_ext.mpr (by simp only [denoteQ_qmul, denoteQ_qadd]; ring)
/-- 0·x = 0 on FeQ. -/
theorem qzero_mul (x : FeQ) : qmul qzero x = qzero :=
feq_ext.mpr (by simp only [denoteQ_qmul, denoteQ_qzero]; ring)
/-- x·0 = 0 on FeQ. -/
theorem qmul_zero (x : FeQ) : qmul x qzero = qzero :=
feq_ext.mpr (by simp only [denoteQ_qmul, denoteQ_qzero]; ring)
/-- 0 ≠ 1 on FeQ — the implementation is a nontrivial ring (because
0 ≠ 1 in 𝔽_p: p ≥ 2, primality from Proofs/P25519.lean). -/
theorem qzero_ne_qone : qzero ≠ qone := by
intro h
have h' := feq_ext.mp h
rw [denoteQ_qzero, denoteQ_qone] at h'
exact zero_ne_one h'
/-- x·x⁻¹ = 1 for x ≠ 0 — THE field axiom, with the inverse computed by the
real Rust addition chain (`invert`) and the product by the real Rust
`mul`; Fermat's little theorem (InvertSpec.lean) makes it 1. -/
theorem qmul_inv_cancel (x : FeQ) (h : x ≠ qzero) : qmul x (qinv x) = qone := by
-- x ≠ qzero transfers to denoteQ x ≠ 0 along the bijection
have h0 : denoteQ x ≠ 0 := fun hz =>
h (feq_ext.mpr (by rw [hz, denoteQ_qzero]))
exact feq_ext.mpr (by
rw [denoteQ_qmul, denoteQ_qinv, denoteQ_qone, mul_inv_cancel₀ h0])
/-- 0⁻¹ = 0 on FeQ: the Rust `invert` maps 0 to 0 (it computes 0^(p2) = 0),
matching mathlib's junk-value convention exactly. -/
theorem qinv_qzero : qinv qzero = qzero :=
feq_ext.mpr (by rw [denoteQ_qinv, denoteQ_qzero, inv_zero])
/-! ## §7 `FeQ` IS a mathlib field — with the Rust runs as structure fields
Built DIRECTLY, in two layers (CommRing, then Field), so that the data
fields are EXACTLY the q-operations of §4 — i.e. the Rust-run operations:
add = qadd (Rust add+reduce) mul = qmul (Rust mul)
neg = qneg (Rust negate) sub = qsub (Rust sub)
inv = qinv (Rust invert) 0 = ⟦Rust ZERO⟧ 1 = ⟦Rust ONE⟧
The remaining *auxiliary* data (nsmul/zsmul/npow/natCast/intCast, div, zpow,
-casts) is left to mathlib's canonical defaults — they are DERIVED from the
core ops (e.g. `div a b := a * b⁻¹` runs Rust mul + invert) and carry no
axiomatic content. `nnqsmul := _`/`qsmul := _` follow the instruction in
mathlib's `DivisionRing` docstring (unification fills `(cast · * ·)`). -/
/-- `FeQ` is a commutative ring, operation by operation the Rust code. -/
noncomputable instance instCommRingFeQ : CommRing FeQ where
add := qadd
add_assoc := qadd_assoc
zero := qzero
zero_add := qzero_add
add_zero := qadd_zero
add_comm := qadd_comm
mul := qmul
left_distrib := qleft_distrib
right_distrib := qright_distrib
zero_mul := qzero_mul
mul_zero := qmul_zero
mul_assoc := qmul_assoc
one := qone
one_mul := qone_mul
mul_one := qmul_one
neg := qneg
sub := qsub
sub_eq_add_neg := qsub_eq_add_neg
neg_add_cancel := qneg_add_cancel
mul_comm := qmul_comm
-- auxiliary data: mathlib's canonical recursors (iterated qadd/qneg —
-- still the Rust operations underneath)
nsmul := nsmulRec
zsmul := zsmulRec
/-- **`FeQ` is a mathlib `Field`** — the punchline instance: the transpiled
Rust curve25519 field code, packaged as the literal field-of-mathlib
structure (inverse = the Rust `invert` addition chain). -/
noncomputable instance instFieldFeQ : Field FeQ :=
{ instCommRingFeQ with
inv := qinv
exists_pair_ne := ⟨qzero, qone, qzero_ne_qone⟩
mul_inv_cancel := qmul_inv_cancel
inv_zero := qinv_qzero
nnqsmul := _
qsmul := _ }
/-! Denotation equations restated against the INSTANCE notation (+, *, -, ⁻¹,
0, 1 now resolve through the `Field FeQ` instance; definitionally these are
the q-operations, so the §5 lemmas transfer verbatim). Tagged `@[simp]` —
they make `denoteQ` a `simp`-transparent field homomorphism. -/
/-- MATH: denoteQ (x + y) = denoteQ x + denoteQ y (instance `+` = qadd). -/
@[simp] theorem denoteQ_add (x y : FeQ) :
denoteQ (x + y) = denoteQ x + denoteQ y := denoteQ_qadd x y
/-- MATH: denoteQ (x y) = denoteQ x denoteQ y (instance `-` = qsub). -/
@[simp] theorem denoteQ_sub (x y : FeQ) :
denoteQ (x - y) = denoteQ x - denoteQ y := denoteQ_qsub x y
/-- MATH: denoteQ (x · y) = denoteQ x · denoteQ y (instance `*` = qmul). -/
@[simp] theorem denoteQ_mul (x y : FeQ) :
denoteQ (x * y) = denoteQ x * denoteQ y := denoteQ_qmul x y
/-- MATH: denoteQ (x) = denoteQ x (instance `-` = qneg). -/
@[simp] theorem denoteQ_neg (x : FeQ) : denoteQ (-x) = -denoteQ x :=
denoteQ_qneg x
/-- MATH: denoteQ x⁻¹ = (denoteQ x)⁻¹ (instance `⁻¹` = qinv). -/
@[simp] theorem denoteQ_inv (x : FeQ) : denoteQ x⁻¹ = (denoteQ x)⁻¹ :=
denoteQ_qinv x
/-- MATH: denoteQ 0 = 0 (instance `0` = ⟦Rust ZERO⟧). -/
@[simp] theorem denoteQ_zero : denoteQ (0 : FeQ) = 0 := vzero_denote
/-- MATH: denoteQ 1 = 1 (instance `1` = ⟦Rust ONE⟧). -/
@[simp] theorem denoteQ_one : denoteQ (1 : FeQ) = 1 := vone_denote
/-! ## §8 The ring isomorphism `FeQ ≃+* 𝔽_p`, finiteness, axiom audit -/
/-- **The implementation is THE field 𝔽_p**: a ring isomorphism between the
quotiented Rust representation and mathlib's `ZMod (2²⁵⁵ 19)`.
MATH: FeQ ≅ 𝔽_p as rings (hence as fields):
forward map = denoteQ (read off the limbs mod p),
backward map = the class of `encode` (write the base-2⁵¹ digits),
mutually inverse by `denote_encode`, homomorphic by §5.
WHY NEEDED: this single object packages the whole development — a reader
who trusts mathlib's `ZMod` only needs this term and its axiom audit
below to conclude the Rust field code is correct. -/
noncomputable def feQRingEquiv : FeQ ≃+* Fp where
toFun := denoteQ
invFun y := ⟦(⟨encode y, (encode_bnd y).mono (by norm_num)⟩ : VFe)⟧
left_inv x := feq_ext.mpr (denote_encode (denoteQ x))
right_inv y := denote_encode y
map_mul' := denoteQ_mul
map_add' := denoteQ_add
/-- Sanity check (compile-time): the `Field FeQ` instance really is in scope —
"the Rust code is a mathlib field" type-checks. -/
noncomputable example : Field FeQ := inferInstance
/-- `FeQ` is FINITE (transport `Fintype 𝔽_p` along the isomorphism):
together with the instance above, the Rust code is literally a FINITE
FIELD in mathlib's vocabulary. -/
noncomputable instance : Fintype FeQ :=
Fintype.ofEquiv Fp feQRingEquiv.symm.toEquiv
/- AXIOM AUDIT. Expected (and verified) output, for the isomorphism AND for
the `Field` instance itself:
'CurveFieldProofs.feQRingEquiv' depends on axioms:
[propext, Classical.choice, Quot.sound]
'CurveFieldProofs.instFieldFeQ' depends on axioms:
[propext, Classical.choice, Quot.sound]
— Lean's three standard axioms only: no sorry, no native_decide, no custom
axiom (in particular none of the external-function axioms of
gen/CurveField/FunsExternal.lean). -/
#print axioms feQRingEquiv
#print axioms instFieldFeQ
end CurveFieldProofs

View file

@ -0,0 +1,205 @@
/- ───────────────────────────────────────────────────────────────────────────
Proofs/Field.lean — Field packaging, part 1 of 2 (part 2 = multiplicative
inverses, which land in InvertSpec.lean and FieldMain.lean).
CONTEXT. The Rust crate curve25519/solana-ed25519 implements arithmetic in
F_p, p = 2^255 - 19, on 5 radix-2^51 u64 limbs (`FieldElement51`, in
curve25519/solana-ed25519/src/backend/serial/u64/field.rs, driven by
src/field.rs). That code was transpiled mechanically (Charon + Aeneas) to
gen/CurveField/{Types,Funs}.lean. The denotation ⟪a⟫ ∈ F_p of a limb
vector a and the limb-bound invariant `Bnd` live in Proofs/Denote.lean.
THIS FILE supplies the purely mathematical scaffolding that the main
theorem (Proofs/FieldMain.lean: `fieldImplementation`) needs around the
per-operation specs:
* 𝔽_p IS a field: p = 2²⁵⁵ 19 is prime (Proofs/P25519.lean, axiom-free),
so mathlib's `ZMod.instField` applies. Registering `Fact (Nat.Prime P)`
is what unlocks that instance (`P_prime`, the two instances below).
* The denotation ⟪·⟫ : Fe → 𝔽_p is SURJECTIVE on bounded elements (via the
canonical `encode`, which writes y < p in base 2⁵¹), so the transpiled
type covers all of 𝔽_p — without this, "implements the field" would be
vacuous on unreachable elements (`encode` … `denote_surjective`).
* The transpiled ops realize the field ops of 𝔽_p through ⟪·⟫ (proved in
the *Spec.lean files this file imports; re-packaged in FieldMain.lean).
* `spec_exists` converts total-correctness triples `x ⦃ post ⦄` into plain
existentials `∃ r, x = ok r ∧ post r`, the form used by FieldMain.lean.
There is NO Rust analog for this file: it is meta-level mathematics about
the transpiled code, not transpiled code itself.
Imports: ConstSpecs/AddSpec/SubNegSpec/MulSpec (the operation specs) and
P25519 (primality). Imported by: InvertSpec.lean (needs the `Field Fp`
instance for `⁻¹` and Fermat) and, through it, FieldMain.lean.
─────────────────────────────────────────────────────────────────────── -/
import Proofs.ConstSpecs
import Proofs.AddSpec
import Proofs.SubNegSpec
import Proofs.MulSpec
import Proofs.P25519
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 4000000
set_option maxRecDepth 8000
namespace CurveFieldProofs
/-! ## 𝔽_p is a field -/
/- MATH: Nat.Prime P where P = 2^255 - 19 (the numeral defined in
Proofs/Denote.lean). This is `p25519_prime` from Proofs/P25519.lean — a
fully kernel-checked Pratt/Lucas primality certificate, no `native_decide`,
no axioms — restated with the literal `2^255 - 19` rewritten to `P` so it
matches the form mathlib's instance machinery will look for.
WHY NEEDED: primality of the modulus is THE reason `ZMod P` is a field
(inverses exist); everything in InvertSpec/FieldMain rests on it. -/
theorem P_prime : Nat.Prime P := by
have h := p25519_prime -- the certificate, for 2^255 - 19
have : (2:)^255 - 19 = P := by norm_num [P] -- the numeral really is P
rwa [this] at h -- transport the certificate to P
/- Register the primality as a typeclass `Fact`, the hook mathlib uses to
activate `Field (ZMod P)` (instance `ZMod.instField`). Without this
instance, `Fp` would only be a commutative ring and `⟪a⟫⁻¹` would not be
available. WHY NEEDED: unlocks `Field Fp` for every later proof. -/
instance : Fact (Nat.Prime P) := ⟨P_prime⟩
/- MATH: P ≠ 0. A small side instance some mathlib lemmas about `ZMod.val`
require (e.g. `ZMod.val_lt` used in `val_lt_P` below). -/
instance : NeZero P := ⟨by norm_num [P]⟩
/-- 𝔽_p = ZMod p with p prime is a field (mathlib instance).
This `example` is a compile-time sanity check that the two instances
above really do trigger mathlib's `ZMod.instField`; it generates no
code and is never referenced (FieldMain.lean re-exposes the instance
as the abbrev `Fp_field`). -/
noncomputable example : Field Fp := inferInstance
/-! ## Surjectivity of the denotation
The representation is REDUNDANT: many limb vectors denote the same field
element, and arbitrary `Fe`s (limbs up to 2⁶⁴) may not even satisfy the
operations' preconditions. To state "the code implements all of 𝔽_p" we must
therefore exhibit, for every y ∈ 𝔽_p, at least one WELL-BOUNDED limb vector
denoting y. `encode` constructs the canonical one: the base-2⁵¹ digits of
the unique representative y.val ∈ [0, p). -/
/-- Build a `U64` from a natural number (mod 2⁶⁴).
Aeneas's `U64` wraps a 64-bit `BitVec`; `.val : Nat` is its mathematical
value. Rust analog: a `u64` literal / `as u64` cast.
WHY NEEDED: `encode` must manufacture concrete machine limbs. -/
def mkU64 (n : ) : U64 := ⟨BitVec.ofNat 64 n⟩
/- MATH: forall n < 2^64, (mkU64 n).val = n — the round-trip is exact as
long as the input fits in 64 bits (BitVec.ofNat reduces mod 2^64, and the
hypothesis makes the reduction a no-op).
WHY NEEDED: lets `encode_bnd`/`denote_encode` compute with the limb values
of `encode y` as plain naturals. -/
theorem mkU64_val (n : ) (h : n < 2^64) : (mkU64 n).val = n := by
show (BitVec.ofNat 64 n).toNat = n
simp only [BitVec.toNat_ofNat] -- .val of ofNat is n % 2^64
omega -- n < 2^64 kills the mod
/-- Canonical (reduced, base-2⁵¹) representative of a field element:
limb i = the i-th base-2⁵¹ digit of y.val (the canonical natural < p).
MATH: encode y = [ y mod 2^51, (y / 2^51) mod 2^51, (y / 2^102) mod 2^51,
(y / 2^153) mod 2^51, y / 2^204 ]
so limbsVal (encode y) = y.val exactly (no mod p reduction needed,
since y.val < p < 2^255). The top limb needs no mask: y.val / 2^204 <
2^51 because y.val < p < 2^255.
Rust analog (conceptually): `FieldElement51::from_bytes` of the little-
endian encoding of y — here built directly as digits, which is simpler
to reason about. `Array.make 5#usize [...]` mirrors the transpiled
representation `FieldElement51 = Array U64 5` (a Rust `[u64; 5]`).
WHY NEEDED: the witness for `denote_surjective`. -/
def encode (y : Fp) : Fe :=
Array.make 5#usize
[ mkU64 (y.val % 2^51),
mkU64 (y.val / 2^51 % 2^51),
mkU64 (y.val / 2^102 % 2^51),
mkU64 (y.val / 2^153 % 2^51),
mkU64 (y.val / 2^204) ]
/- MATH: the underlying limb list of `encode y` is literally the 5-digit
list above (`rfl`: true by unfolding the definitions).
WHY NEEDED: `Bnd`/`feVal` are stated via the limb LIST (`Bnd_eq`,
`feVal_eq` in Proofs/Denote.lean), so the next two proofs need the list
in explicit form. -/
theorem encode_list (y : Fp) :
(↑(encode y) : List U64)
= [ mkU64 (y.val % 2^51), mkU64 (y.val / 2^51 % 2^51),
mkU64 (y.val / 2^102 % 2^51), mkU64 (y.val / 2^153 % 2^51),
mkU64 (y.val / 2^204) ] := rfl
/- MATH: forall y in F_p, y.val < P — the canonical representative is
reduced. Pure mathlib (`ZMod.val_lt`, needs `NeZero P` above); restated
here for convenient repeated use.
WHY NEEDED: gives the size bound that makes all of `encode`'s digits and
their recombination fit (P < 2^255 = (2^51)^5). -/
theorem val_lt_P (y : Fp) : y.val < P := ZMod.val_lt y
/- MATH: forall y in F_p, Bnd (encode y) (2^51) — every limb of the
canonical representative is < 2^51 (it is a base-2^51 digit; the top limb
because y.val < P < 2^255). This is even stronger than the 2^52 "valid
output" bound used downstream.
WHY NEEDED: surjectivity must produce BOUNDED witnesses, otherwise the
operations' preconditions could never be met on them. -/
theorem encode_bnd (y : Fp) : Bnd (encode y) (2^51) := by
have h := val_lt_P y
have hP : P < 2^255 := by norm_num [P]
-- switch from the opaque `Fe` to the explicit 5-element limb list
rw [Bnd_eq _ _ _ _ _ _ _ (encode_list y)]
-- 5 goals, one per limb: each digit is < 2^51 by omega
-- (mod-2^51 digits trivially; the top limb via y.val < P < 2^255)
refine ⟨?_, ?_, ?_, ?_, ?_⟩ <;>
(rw [mkU64_val _ (by omega)]; omega)
/- MATH: forall y in F_p, ⟪encode y⟫ = y — decode ∘ encode = id.
LaTeX: $\forall y,\ \llbracket \mathrm{encode}\,y \rrbracket = y$.
Proof: the base-2^51 digits recombine to exactly y.val (omega), and
casting y.val back into ZMod P is the identity.
WHY NEEDED: the second half of the surjectivity witness. -/
theorem denote_encode (y : Fp) : ⟪encode y⟫ = y := by
have h := val_lt_P y
have hP : P < 2^255 := by norm_num [P]
-- step 1: the natural-number value of the limbs is exactly y.val
have hval : feVal (encode y) = y.val := by
rw [feVal_eq _ _ _ _ _ _ (encode_list y)]
simp only [limbsVal]
-- each digit fits in u64, so mkU64 is value-preserving on it
rw [mkU64_val _ (by omega), mkU64_val _ (by omega), mkU64_val _ (by omega),
mkU64_val _ (by omega), mkU64_val _ (by omega)]
-- digit recombination: d0 + d1·2^51 + d2·2^102 + d3·2^153 + d4·2^204 = y.val
omega
-- step 2: (y.val : ZMod P) = y (cast of the canonical representative)
simp [denote, hval, ZMod.natCast_val, ZMod.cast_id]
/-- Every element of 𝔽_p is the denotation of a (well-bounded) `Fe`.
MATH: forall y : F_p, exists a : Fe, Bnd(a, 2^52) and ⟪a⟫ = y.
LaTeX: $\forall y \in \mathbb{F}_p\ \exists a,\
\mathrm{Bnd}(a,2^{52}) \wedge \llbracket a\rrbracket = y$.
The witness is `encode y` (bounded by 2⁵¹, weakened to the standard
"valid element" bound 2⁵² via `Bnd.mono`).
WHY NEEDED: this is the `surj` field of `IsFieldImplementation`
(FieldMain.lean) — it makes "implements 𝔽_p" mean ALL of 𝔽_p. -/
theorem denote_surjective : ∀ y : Fp, ∃ a : Fe, Bnd a (2^52) ∧ ⟪a⟫ = y :=
fun y => ⟨encode y, (encode_bnd y).mono (by norm_num), denote_encode y⟩
/-! ## The triple → existential bridge (library: `Std.WP.spec_imp_exists`) -/
/- MATH: if x ⦃ post ⦄ (total correctness: x does not panic AND its result
satisfies post), then exists r, x = ok r and post r.
This merely re-exports the Aeneas library lemma `Std.WP.spec_imp_exists`
with the triple written in this project's notation.
WHY NEEDED: the `run_*` theorems and `IsFieldImplementation` fields in
FieldMain.lean are phrased as plain existentials over `= ok r` (readable
without knowing the WP calculus); this is the converter. -/
theorem spec_exists {α} {x : Result α} {p : α → Prop}
(h : x ⦃ r => p r ⦄) : ∃ r, x = ok r ∧ p r :=
Std.WP.spec_imp_exists h
end CurveFieldProofs

View file

@ -0,0 +1,451 @@
/- ───────────────────────────────────────────────────────────────────────────
Proofs/FieldMain.lean — MAIN RESULT: the transpiled `FieldElement51` code
implements the field 𝔽_p, p = 2²⁵⁵ 19.
CONTEXT. The Rust crate curve25519/solana-ed25519 implements F_p on
5 radix-2^51 u64 limbs (src/field.rs + src/backend/serial/u64/field.rs);
Charon+Aeneas transpiled it mechanically to gen/CurveField/{Types,Funs}.lean.
The denotation ⟪a⟫ = (a0 + a1·2^51 + a2·2^102 + a3·2^153 + a4·2^204) mod p
and the limb-bound invariant `Bnd a c` ("all 5 limbs < c") are defined in
Proofs/Denote.lean; per-operation specs in Add/SubNeg/Mul/Square/Const/
Invert-Spec.lean; the Field-Fp instance and surjectivity in Field.lean.
WHY THIS SHAPE. A literal `Field Fe` instance is mathematically impossible:
* the representation is REDUNDANT — a field element has many limb
representations, so e.g. `mul_comm` is FALSE as an equality of limb
vectors (only the denotations agree);
* the operations are PARTIAL — machine arithmetic can overflow, so every
transpiled op returns in the `Result` monad (`ok r` = success, `fail` =
panic/overflow) and is only guaranteed on bounded inputs.
So "the transpiled code is a field" is formalized the standard way for
verified implementations, through the surjective denotation:
* 𝔽_p (= `ZMod P`) IS a field — mathlib instance + our
axiom-free primality certificate for p (Proofs/P25519.lean);
* the denotation ⟪·⟫ : Fe → 𝔽_p is surjective on bounded elements;
* every transpiled operation TOTALLY (no panic) realizes the
corresponding field operation of 𝔽_p through ⟪·⟫, under the
documented limb-bound invariant (the dalek 2⁵⁴ discipline, which the
Rust code itself asserts via debug_assert! → `massert`);
* consequently every field axiom holds for the implementation up to
denotation — proved below as the `impl_*` corollaries, each of which
RUNS the actual transpiled functions.
THIS FILE contains, in order:
1. `run_*` combinators — one per operation, converting the spec triples
`x ⦃ post ⦄` into plain existentials `∃ r, x = ok r ∧ Bnd r _ ∧ ⟪r⟫ = _`
(via `spec_exists`), with bounds normalized to the uniform 2⁵²/2⁵⁴
discipline;
2. `IsFieldImplementation` — the certificate Prop bundling surjectivity
and the eight operation contracts;
3. `fieldImplementation` — THE MAIN THEOREM: the certificate holds;
4. the `impl_*` corollaries — every field axiom, at implementation level.
AXIOM HYGIENE: `#print axioms fieldImplementation` reports only Lean's
three standard axioms [propext, Classical.choice, Quot.sound] — no sorry,
no native_decide, no custom axiom (the 4 axioms modeling externals in
gen/CurveField/FunsExternal.lean are outside the dependency cone).
Nothing in gen/ (the transpiled code) was modified.
Imports: InvertSpec.lean (which transitively pulls in every other Proofs/
file). Imported by: nothing — this is the root of the development.
─────────────────────────────────────────────────────────────────────── -/
import Proofs.InvertSpec
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 4000000
namespace CurveFieldProofs
/-! ## Runners: totality + facts for each transpiled op
One `run_*` theorem per operation. Each takes the operation's spec triple
(from the *Spec.lean files), passes it through `spec_exists`
(Proofs/Field.lean) to obtain `∃ r, op = ok r ∧ …` — "the machine code
RUNS without panicking and returns r" — and normalizes the output bound
to the uniform validity discipline:
Valid (= Bnd · 2⁵²) ──any op──▶ output Bnd ≤ 2⁵² (add: 2⁵³)
so the results can be chained: any output is again a legal input (after the
trivial weakening 2⁵² ≤ 2⁵⁴, `valid54` below). -/
/- RUST ANALOG: `FieldElement51::ZERO` (constant [0,0,0,0,0]),
curve25519/solana-ed25519/src/backend/serial/u64/field.rs.
MATH: fe_zero = ok z with Bnd(z, 2^52) and ⟪z⟫ = 0.
WHY NEEDED: the additive identity of the implementation (`zero_ok`). -/
theorem run_zero : ∃ z, fe_zero = ok z ∧ Bnd z (2^52) ∧ ⟪z⟫ = 0 := by
obtain ⟨z, hz, _, h1, h2⟩ := spec_exists zero_spec -- ConstSpecs.lean
exact ⟨z, hz, h1.mono (by norm_num), h2⟩ -- weaken 2^51 → 2^52
/- RUST ANALOG: `FieldElement51::ONE` (constant [1,0,0,0,0]),
curve25519/solana-ed25519/src/backend/serial/u64/field.rs.
MATH: fe_one = ok o with Bnd(o, 2^52) and ⟪o⟫ = 1.
WHY NEEDED: the multiplicative identity of the implementation (`one_ok`). -/
theorem run_one : ∃ o, fe_one = ok o ∧ Bnd o (2^52) ∧ ⟪o⟫ = 1 := by
obtain ⟨o, ho, h1, h2⟩ := spec_exists one_spec -- ConstSpecs.lean
exact ⟨o, ho, h1.mono (by norm_num), h2⟩ -- weaken 2^51 → 2^52
/- RUST ANALOG: `impl Add for FieldElement51` (the `+` operator: plain
limbwise addition, NO reduction),
curve25519/solana-ed25519/src/backend/serial/u64/field.rs.
MATH: Bnd(a,2^52) and Bnd(b,2^52) ==>
fe_add a b = ok r, Bnd(r, 2^53), ⟪r⟫ = ⟪a⟫ + ⟪b⟫.
Limbwise addition merely doubles the bound (2^52+2^52 = 2^53 < 2^64, so
no u64 overflow); the value is EXACT, the denotation adds mod p.
The base `add_spec` (AddSpec.lean) needs per-limb no-overflow hypotheses
and yields the generic bound law "Bnd a c → Bnd b c → Bnd r (2c)"; this
runner instantiates both at c = 2^52.
WHY NEEDED: `add_ok`, and the additive `impl_*` laws. -/
theorem run_add {a b : Fe} (ha : Bnd a (2^52)) (hb : Bnd b (2^52)) :
∃ r, fe_add a b = ok r ∧ Bnd r (2^53) ∧ ⟪r⟫ = ⟪a⟫ + ⟪b⟫ := by
-- name the 5 limbs of each argument and turn `Bnd` into per-limb bounds
obtain ⟨x0, x1, x2, x3, x4, hla⟩ := Fe.exists_limbs a
obtain ⟨y0, y1, y2, y3, y4, hlb⟩ := Fe.exists_limbs b
have hba := (Bnd_eq a _ _ _ _ _ _ hla).mp ha
have hbb := (Bnd_eq b _ _ _ _ _ _ hlb).mp hb
-- run add_spec; each pairwise sum < 2^53 < 2^64 is closed by omega
obtain ⟨r, hr, _, hval, hbnd⟩ :=
spec_exists (add_spec a b x0 x1 x2 x3 x4 y0 y1 y2 y3 y4 hla hlb
⟨by omega, by omega, by omega, by omega, by omega⟩)
-- bound: instantiate the "doubles any common bound" law at 2^52
refine ⟨r, hr, by simpa using hbnd (2^52) ha hb, ?_⟩
-- value: feVal r = feVal a + feVal b, then push through the mod-p cast
simp [denote, hval]
/- RUST ANALOG: `impl Sub for FieldElement51` (the `-` operator: add 16p
limbwise before subtracting to avoid u64 underflow, then reduce),
curve25519/solana-ed25519/src/backend/serial/u64/field.rs.
MATH: Bnd(a,2^54) and Bnd(b,2^54) ==>
fe_sub a b = ok r, Bnd(r, 2^52), ⟪r⟫ = ⟪a⟫ ⟪b⟫.
WHY NEEDED: `sub_ok` (subtraction is definable from neg+add, but the Rust
API exposes it as a primitive, so the certificate covers it directly). -/
theorem run_sub {a b : Fe} (ha : Bnd a (2^54)) (hb : Bnd b (2^54)) :
∃ r, fe_sub a b = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫ - ⟪b⟫ := by
-- sub_spec (SubNegSpec.lean) already has the desired post; just supply limbs
obtain ⟨x0, x1, x2, x3, x4, hla⟩ := Fe.exists_limbs a
obtain ⟨y0, y1, y2, y3, y4, hlb⟩ := Fe.exists_limbs b
exact spec_exists (sub_spec a b x0 x1 x2 x3 x4 y0 y1 y2 y3 y4 hla hlb ha hb)
/- RUST ANALOG: `FieldElement51::negate` (computes 16p a limbwise, reduces),
curve25519/solana-ed25519/src/backend/serial/u64/field.rs.
MATH: Bnd(a,2^54) ==> fe_neg a = ok r, Bnd(r, 2^52), ⟪r⟫ = ⟪a⟫.
WHY NEEDED: `neg_ok`; additive inverses (`impl_add_neg`). -/
theorem run_neg {a : Fe} (ha : Bnd a (2^54)) :
∃ r, fe_neg a = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = -⟪a⟫ := by
obtain ⟨x0, x1, x2, x3, x4, hla⟩ := Fe.exists_limbs a
exact spec_exists (neg_spec a x0 x1 x2 x3 x4 hla ha)
/- RUST ANALOG: `impl Mul for FieldElement51` (the `*` operator: radix-2^51
schoolbook with 19-folding — 2^255 ≡ 19 (mod p) lets the high half fold
back as ×19 — u128 accumulators, carry chain),
curve25519/solana-ed25519/src/backend/serial/u64/field.rs.
MATH: Bnd(a,2^54) and Bnd(b,2^54) ==>
fe_mul a b = ok r, Bnd(r, 2^52), ⟪r⟫ = ⟪a⟫ · ⟪b⟫.
(mul_spec' actually gives the sharper bound 2^51 + 2^13; weakened here to
the uniform 2^52.) Totality includes the two in-code debug_assert!s.
WHY NEEDED: `mul_ok`, and the multiplicative `impl_*` laws. -/
theorem run_mul {a b : Fe} (ha : Bnd a (2^54)) (hb : Bnd b (2^54)) :
∃ r, fe_mul a b = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫ * ⟪b⟫ := by
obtain ⟨r, hr, h1, h2⟩ := spec_exists (mul_spec' a b ha hb)
exact ⟨r, hr, h1.mono (by norm_num), h2⟩ -- 2^51 + 2^13 ≤ 2^52
/- RUST ANALOG: `FieldElement51::invert` (x^(p2) by the pow22501 addition
chain — 254 squarings + 11 multiplications),
curve25519/solana-ed25519/src/field.rs:239-248.
MATH: Bnd(a,2^54) ==> fe_invert a = ok r, Bnd(r, 2^52), ⟪r⟫ = ⟪a⟫⁻¹
— including ⟪a⟫ = 0, where both sides are 0 (mathlib's 0⁻¹ = 0 and the
Rust code's invert(0) = 0 agree). Proved in Proofs/InvertSpec.lean
(Fermat's little theorem + the chain's exponent bookkeeping).
WHY NEEDED: `inv_ok` — the field-defining operation. -/
theorem run_invert {a : Fe} (ha : Bnd a (2^54)) :
∃ r, fe_invert a = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫⁻¹ :=
spec_exists (invert_spec a ha)
/-! ## The field-implementation certificate -/
/-- The transpiled curve25519 field code implements the field 𝔽_p through the
denotation ⟪·⟫ on limb-bounded elements: all operations are total (no
panics / overflows) on the invariant and realize the field structure.
This `structure … : Prop` is just a named conjunction of nine claims —
a CERTIFICATE. Field by field:
* `surj` — ⟪·⟫ hits all of 𝔽_p with bounded representatives, so the
remaining clauses speak about every field element, not just
the reachable ones;
* `zero_ok`/`one_ok` — the constants evaluate (they are `Result`s too:
Rust consts become monadic thunks under Aeneas) to bounded
elements denoting 0 and 1;
* `add_ok`/`sub_ok`/`neg_ok`/`mul_ok`/`inv_ok` — on bounded inputs the
op returns `ok r` (NO PANIC — every machine-arithmetic
side condition holds) with `r` again bounded and
⟪r⟫ = the corresponding 𝔽_p operation on the inputs.
Together with `Field Fp` (mathlib, P prime) this is the standard meaning
of "this code implements 𝔽_p": a literal `Field Fe` instance cannot
exist (redundant representation, partial ops — see the file header), so
the field laws transfer through ⟪·⟫ instead — see the `impl_*`
corollaries below, which derive each axiom in executable form.
WHY NEEDED: this is the STATEMENT of the main theorem. -/
structure IsFieldImplementation : Prop where
/-- 𝔽_p is reachable: every field element has a bounded representative.
MATH: forall y in F_p, exists a, Bnd(a,2^52) and ⟪a⟫ = y.
(Witness: `encode` — Proofs/Field.lean.) -/
surj : ∀ y : Fp, ∃ a : Fe, Bnd a (2^52) ∧ ⟪a⟫ = y
/-- 0 and 1 are correctly implemented (and distinct: see `zero_ne_one`). -/
zero_ok : ∃ z, fe_zero = ok z ∧ Bnd z (2^52) ∧ ⟪z⟫ = 0
/-- Rust: `FieldElement51::ONE`. MATH: fe_one = ok o, Bnd(o,2^52), ⟪o⟫ = 1. -/
one_ok : ∃ o, fe_one = ok o ∧ Bnd o (2^52) ∧ ⟪o⟫ = 1
/-- addition (limbwise, unreduced — hence the 2⁵³ output bound) -/
add_ok : ∀ a b, Bnd a (2^52) → Bnd b (2^52) →
∃ r, fe_add a b = ok r ∧ Bnd r (2^53) ∧ ⟪r⟫ = ⟪a⟫ + ⟪b⟫
/-- subtraction (the +16p underflow trick, then reduce — Rust `impl Sub`). -/
sub_ok : ∀ a b, Bnd a (2^54) → Bnd b (2^54) →
∃ r, fe_sub a b = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫ - ⟪b⟫
/-- negation (16p a, then reduce — Rust `FieldElement51::negate`). -/
neg_ok : ∀ a, Bnd a (2^54) →
∃ r, fe_neg a = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = -⟪a⟫
/-- multiplication (radix-2⁵¹ schoolbook, 19-folding — Rust `impl Mul`). -/
mul_ok : ∀ a b, Bnd a (2^54) → Bnd b (2^54) →
∃ r, fe_mul a b = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫ * ⟪b⟫
/-- multiplicative inverse (x^(p2); maps 0 to 0, matching 𝔽_p's 0⁻¹ = 0) -/
inv_ok : ∀ a, Bnd a (2^54) →
∃ r, fe_invert a = ok r ∧ Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫⁻¹
/-- **The transpiled code implements the field 𝔽_p.**
THE MAIN THEOREM of the development. Each clause of the certificate is
discharged by the corresponding `run_*` runner above (which in turn
packages the per-operation machine-code proofs of the *Spec.lean files),
and surjectivity by `denote_surjective` (Proofs/Field.lean).
`#print axioms CurveFieldProofs.fieldImplementation` yields exactly
[propext, Classical.choice, Quot.sound] — Lean's standard axioms only. -/
theorem fieldImplementation : IsFieldImplementation where
surj := denote_surjective
zero_ok := run_zero
one_ok := run_one
add_ok := fun _ _ ha hb => run_add ha hb
sub_ok := fun _ _ ha hb => run_sub ha hb
neg_ok := fun _ ha => run_neg ha
mul_ok := fun _ _ ha hb => run_mul ha hb
inv_ok := fun _ ha => run_invert ha
/-- 𝔽_p is a field (mathlib instance; p prime by Proofs/P25519.lean).
Re-exported here under a stable name so a reader of the main theorem
sees both halves of the claim side by side: the TARGET 𝔽_p is a field
(this abbrev), and the CODE implements it (`fieldImplementation`).
`noncomputable` because field inversion on `ZMod P` is classical —
irrelevant here, we never execute it. -/
noncomputable abbrev Fp_field : Field Fp := inferInstance
/-! ## The field axioms, at the implementation level
Each `impl_*` theorem runs the actual transpiled operations and states the
corresponding field law up to denotation. They are direct consequences of
the `run_*` specs + the field structure of 𝔽_p. Throughout, `Valid a` means
`Bnd a (2^52)` (the bound every operation re-establishes).
Note the shape: a law like commutativity CANNOT be `fe_mul a b = fe_mul b a`
(the limb vectors generally differ!), nor can it ignore totality. So each
law asserts (1) all the involved operation calls return `ok` — the code
actually runs — and (2) the final denotations agree. Covered axioms:
impl_zero_ne_one, impl_add_comm, impl_add_assoc, impl_zero_add,
impl_add_neg, impl_mul_comm, impl_mul_assoc, impl_one_mul,
impl_mul_inv_cancel, impl_left_distrib
— exactly the `Field` axioms of mathlib (right-distributivity and `mul_one`
etc. follow from commutativity, included here via impl_mul_comm). -/
/- The working invariant: a "valid" field element has all limbs < 2^52 —
the bound every operation's output satisfies (add: 2^53, see add_ok) and
`encode` satisfies, so valid elements are closed under the API.
WHY NEEDED: gives the impl_* laws a single, chainable precondition. -/
abbrev Valid (a : Fe) : Prop := Bnd a (2^52)
/- MATH: Bnd(a,2^52) ==> Bnd(a,2^54) — trivial weakening (`Bnd.mono`).
WHY NEEDED: sub/neg/mul/invert take inputs at the 2^54 (dalek) bound;
this adapter lets them consume `Valid` elements. -/
theorem valid54 {a : Fe} (h : Valid a) : Bnd a (2^54) := h.mono (by norm_num)
/-- 0 ≠ 1 (the implementation is a nontrivial ring).
MATH: forall z o, fe_zero = ok z and fe_one = ok o ==> ⟪z⟫ ≠ ⟪o⟫.
Phrased over ANY successful evaluation of the constants (they are
deterministic, so z/o are forced to the `run_zero`/`run_one` witnesses).
WHY NEEDED: `Field` requires nontriviality; here it holds because
0 ≠ 1 in ZMod P (P > 1). -/
theorem impl_zero_ne_one :
∀ z o, fe_zero = ok z → fe_one = ok o → ⟪z⟫ ≠ ⟪o⟫ := by
intro z o hz ho
obtain ⟨z', hz', _, hz0⟩ := run_zero
obtain ⟨o', ho', _, ho1⟩ := run_one
-- determinism: ok z = ok z' forces z = z' (same for o)
rw [hz'] at hz; cases hz
rw [ho'] at ho; cases ho
rw [hz0, ho1]
exact zero_ne_one -- 0 ≠ 1 in the field 𝔽_p
/-- Commutativity of implemented addition.
MATH: Valid a, Valid b ==> fe_add a b = ok r1, fe_add b a = ok r2,
⟪r1⟫ = ⟪r2⟫.
(r1 = r2 as limb vectors happens to hold for add, but the law is stated
denotationally for uniformity with mul.) -/
theorem impl_add_comm {a b : Fe} (ha : Valid a) (hb : Valid b) :
∃ r1 r2, fe_add a b = ok r1 ∧ fe_add b a = ok r2 ∧ ⟪r1⟫ = ⟪r2⟫ := by
obtain ⟨r1, h1, _, hv1⟩ := run_add ha hb -- run a + b
obtain ⟨r2, h2, _, hv2⟩ := run_add hb ha -- run b + a
exact ⟨r1, r2, h1, h2, by rw [hv1, hv2, add_comm]⟩ -- add_comm in 𝔽_p
/-- Associativity of implemented addition ((a+b)+c ≃ a+(b+c)).
Note 2⁵³+2⁵² < 2⁶⁴: the unreduced intermediate still cannot overflow.
MATH: Valid a, b, c ==> all four adds return ok and
⟪(a+b)+c⟫ = ⟪a+(b+c)⟫.
The outer additions take one 2⁵³-bounded and one 2⁵²-bounded argument —
outside `run_add`'s uniform precondition — so the proof re-invokes the
base `add_spec` (AddSpec.lean) at the mixed bounds; the per-limb
no-overflow side conditions 2⁵³ + 2⁵² < 2⁶⁴ close by `omega`.
WHY NEEDED: associativity is a `Field` axiom; it also documents that one
unreduced add can be safely chained into another. -/
theorem impl_add_assoc {a b c : Fe} (ha : Valid a) (hb : Valid b) (hc : Valid c) :
∃ rab rab_c rbc ra_bc,
fe_add a b = ok rab ∧ fe_add rab c = ok rab_c ∧
fe_add b c = ok rbc ∧ fe_add a rbc = ok ra_bc ∧
⟪rab_c⟫ = ⟪ra_bc⟫ := by
-- inner additions: a+b and b+c via the uniform runner
obtain ⟨rab, h1, hb1, hv1⟩ := run_add ha hb
obtain ⟨rbc, h3, hb3, hv3⟩ := run_add hb hc
-- rab : Bnd 2^53, c : 2^52 — rerun the limbwise argument at mixed bounds
obtain ⟨x0, x1, x2, x3, x4, hla⟩ := Fe.exists_limbs rab
obtain ⟨y0, y1, y2, y3, y4, hlb⟩ := Fe.exists_limbs c
have hba := (Bnd_eq rab _ _ _ _ _ _ hla).mp hb1
have hbb := (Bnd_eq c _ _ _ _ _ _ hlb).mp hc
obtain ⟨rab_c, h2, _, hval2, _⟩ :=
spec_exists (add_spec rab c x0 x1 x2 x3 x4 y0 y1 y2 y3 y4 hla hlb
⟨by omega, by omega, by omega, by omega, by omega⟩)
-- symmetrically for a + rbc (a : 2^52, rbc : 2^53)
obtain ⟨u0, u1, u2, u3, u4, hlu⟩ := Fe.exists_limbs a
obtain ⟨v0, v1, v2, v3, v4, hlv⟩ := Fe.exists_limbs rbc
have hbu := (Bnd_eq a _ _ _ _ _ _ hlu).mp ha
have hbv := (Bnd_eq rbc _ _ _ _ _ _ hlv).mp hb3
obtain ⟨ra_bc, h4, _, hval4, _⟩ :=
spec_exists (add_spec a rbc u0 u1 u2 u3 u4 v0 v1 v2 v3 v4 hlu hlv
⟨by omega, by omega, by omega, by omega, by omega⟩)
refine ⟨rab, rab_c, rbc, ra_bc, h1, h2, h3, h4, ?_⟩
-- turn the exact limb-value equations into denotation equations
have e2 : ⟪rab_c⟫ = ⟪rab⟫ + ⟪c⟫ := by
simp [denote, hval2]
have e4 : ⟪ra_bc⟫ = ⟪a⟫ + ⟪rbc⟫ := by
simp [denote, hval4]
-- finish with associativity in 𝔽_p
rw [e2, e4, hv1, hv3, add_assoc]
/-- 0 + a ≃ a.
MATH: Valid a ==> fe_zero = ok z, fe_add z a = ok r, ⟪r⟫ = ⟪a⟫
— the implemented 0 is a left additive identity (right identity follows
with `impl_add_comm`). -/
theorem impl_zero_add {a : Fe} (ha : Valid a) :
∃ z r, fe_zero = ok z ∧ fe_add z a = ok r ∧ ⟪r⟫ = ⟪a⟫ := by
obtain ⟨z, hz, hzb, hz0⟩ := run_zero -- materialize the 0 constant
obtain ⟨r, hr, _, hv⟩ := run_add hzb ha -- run z + a
exact ⟨z, r, hz, hr, by rw [hv, hz0, zero_add]⟩ -- 0 + x = x in 𝔽_p
/-- a + (a) ≃ 0.
MATH: Valid a ==> fe_neg a = ok n, fe_add a n = ok r, ⟪r⟫ = 0
— every element has an additive inverse, computed by the actual
`negate` code (the 16p a trick, SubNegSpec.lean). -/
theorem impl_add_neg {a : Fe} (ha : Valid a) :
∃ n r, fe_neg a = ok n ∧ fe_add a n = ok r ∧ ⟪r⟫ = 0 := by
obtain ⟨n, hn, hnb, hnv⟩ := run_neg (valid54 ha) -- n with ⟪n⟫ = ⟪a⟫
obtain ⟨r, hr, _, hv⟩ := run_add ha hnb -- run a + n
exact ⟨n, r, hn, hr, by rw [hv, hnv, add_neg_cancel]⟩ -- x + (x) = 0
/-- Commutativity of implemented multiplication.
MATH: Valid a, Valid b ==> fe_mul a b = ok r1, fe_mul b a = ok r2,
⟪r1⟫ = ⟪r2⟫.
Note r1 and r2 are generally DIFFERENT limb vectors (the schoolbook
carry chains differ) — only the denotations coincide; this is exactly
why the laws are stated through ⟪·⟫. -/
theorem impl_mul_comm {a b : Fe} (ha : Valid a) (hb : Valid b) :
∃ r1 r2, fe_mul a b = ok r1 ∧ fe_mul b a = ok r2 ∧ ⟪r1⟫ = ⟪r2⟫ := by
obtain ⟨r1, h1, _, hv1⟩ := run_mul (valid54 ha) (valid54 hb) -- run a·b
obtain ⟨r2, h2, _, hv2⟩ := run_mul (valid54 hb) (valid54 ha) -- run b·a
exact ⟨r1, r2, h1, h2, by rw [hv1, hv2, mul_comm]⟩ -- mul_comm in 𝔽_p
/-- Associativity of implemented multiplication.
MATH: Valid a, b, c ==> all four muls return ok and
⟪(a·b)·c⟫ = ⟪a·(b·c)⟫.
Chaining works because each mul output (Bnd 2⁵²) is again a legal
mul input after `valid54` — the closure property of the invariant. -/
theorem impl_mul_assoc {a b c : Fe} (ha : Valid a) (hb : Valid b) (hc : Valid c) :
∃ rab rab_c rbc ra_bc,
fe_mul a b = ok rab ∧ fe_mul rab c = ok rab_c ∧
fe_mul b c = ok rbc ∧ fe_mul a rbc = ok ra_bc ∧
⟪rab_c⟫ = ⟪ra_bc⟫ := by
-- run the four multiplications, feeding each output bound into the next
obtain ⟨rab, h1, hb1, hv1⟩ := run_mul (valid54 ha) (valid54 hb)
obtain ⟨rab_c, h2, _, hv2⟩ := run_mul (valid54 hb1) (valid54 hc)
obtain ⟨rbc, h3, hb3, hv3⟩ := run_mul (valid54 hb) (valid54 hc)
obtain ⟨ra_bc, h4, _, hv4⟩ := run_mul (valid54 ha) (valid54 hb3)
refine ⟨rab, rab_c, rbc, ra_bc, h1, h2, h3, h4, ?_⟩
-- rewrite all four denotations, close with mul_assoc in 𝔽_p
rw [hv2, hv4, hv1, hv3, mul_assoc]
/-- 1 * a ≃ a.
MATH: Valid a ==> fe_one = ok o, fe_mul o a = ok r, ⟪r⟫ = ⟪a⟫
— the implemented 1 is a left multiplicative identity (right identity
follows with `impl_mul_comm`). -/
theorem impl_one_mul {a : Fe} (ha : Valid a) :
∃ o r, fe_one = ok o ∧ fe_mul o a = ok r ∧ ⟪r⟫ = ⟪a⟫ := by
obtain ⟨o, ho, hob, ho1⟩ := run_one -- the 1 constant
obtain ⟨r, hr, _, hv⟩ := run_mul (valid54 hob) (valid54 ha) -- run o·a
exact ⟨o, r, ho, hr, by rw [hv, ho1, one_mul]⟩ -- 1·x = x in 𝔽_p
/-- a · a⁻¹ ≃ 1 for a ≢ 0 — multiplicative inverses exist.
MATH: Valid a and ⟪a⟫ ≠ 0 ==> fe_invert a = ok i, fe_mul a i = ok r,
⟪r⟫ = 1.
LaTeX: $\llbracket a\rrbracket \ne 0 \Rightarrow
\llbracket a \cdot \mathrm{invert}(a)\rrbracket = 1$.
THE field axiom — the one that distinguishes 𝔽_p from a mere ring, and
the pay-off of InvertSpec.lean: `i` is computed by the real addition-
chain code, `r` by the real multiplication code, and Fermat guarantees
the product denotes 1. (For ⟪a⟫ = 0 inversion still RUNS and returns
the 0 element — see `inv_ok` — but of course no r with ⟪r⟫ = 1 exists.) -/
theorem impl_mul_inv_cancel {a : Fe} (ha : Valid a) (h0 : ⟪a⟫ ≠ 0) :
∃ i r, fe_invert a = ok i ∧ fe_mul a i = ok r ∧ ⟪r⟫ = 1 := by
obtain ⟨i, hi, hib, hiv⟩ := run_invert (valid54 ha) -- i with ⟪i⟫ = ⟪a⟫⁻¹
obtain ⟨r, hr, _, hv⟩ := run_mul (valid54 ha) (valid54 hib) -- run a·i
exact ⟨i, r, hi, hr, by rw [hv, hiv, mul_inv_cancel₀ h0]⟩ -- x·x⁻¹ = 1
/-- Left distributivity: a·(b+c) ≃ a·b + a·c.
MATH: Valid a, b, c ==> all five ops return ok and
⟪a·(b+c)⟫ = ⟪a·b + a·c⟫.
(Right distributivity follows with `impl_mul_comm`.) The inner sum
b+c is only Bnd 2⁵³ — still a legal mul input after weakening to 2⁵⁴,
which is exactly why mul's precondition is the generous dalek bound. -/
theorem impl_left_distrib {a b c : Fe} (ha : Valid a) (hb : Valid b) (hc : Valid c) :
∃ rbc r_left rab rac r_right,
fe_add b c = ok rbc ∧ fe_mul a rbc = ok r_left ∧
fe_mul a b = ok rab ∧ fe_mul a c = ok rac ∧
fe_add rab rac = ok r_right ∧
⟪r_left⟫ = ⟪r_right⟫ := by
-- left side: rbc = b+c (Bnd 2^53), then a·rbc (weaken 2^53 ≤ 2^54)
obtain ⟨rbc, h1, hb1, hv1⟩ := run_add hb hc
obtain ⟨r_left, h2, _, hv2⟩ := run_mul (valid54 ha) (hb1.mono (by norm_num))
-- right side: a·b, a·c, then their sum
obtain ⟨rab, h3, hb3, hv3⟩ := run_mul (valid54 ha) (valid54 hb)
obtain ⟨rac, h4, hb4, hv4⟩ := run_mul (valid54 ha) (valid54 hc)
obtain ⟨r_right, h5, _, hv5⟩ := run_add hb3 hb4
refine ⟨rbc, r_left, rab, rac, r_right, h1, h2, h3, h4, h5, ?_⟩
-- rewrite all denotations, close with left_distrib in 𝔽_p
rw [hv2, hv1, hv5, hv3, hv4, left_distrib]
end CurveFieldProofs

View file

@ -0,0 +1,243 @@
/- ───────────────────────────────────────────────────────────────────────────
Proofs/InvertSpec.lean — Spec for the transpiled `invert`: x ↦ x^(p2) via
the pow22501 addition chain, which by Fermat's little theorem (p prime,
Proofs/P25519.lean) is the multiplicative inverse in 𝔽_p (with the mathlib
convention 0⁻¹ = 0, which the Rust code also satisfies: invert(0) = 0).
CONTEXT. The Rust crate curve25519/solana-ed25519 implements F_p,
p = 2^255 - 19, on 5 radix-2^51 u64 limbs (`FieldElement51`). Inversion
never divides: since the multiplicative group of F_p has order p 1,
Fermat gives a^(p1) = 1 for a ≠ 0, hence a · a^(p2) = 1, i.e.
a^(p2) = a⁻¹. The Rust code (curve25519/solana-ed25519/src/field.rs)
computes x^(p2) with a fixed 254-squaring / 11-multiplication addition
chain (`pow22501` + `pow2k` + `mul`), transpiled by Charon+Aeneas into
gen/CurveField/Funs.lean (`field.FieldElement51.pow22501`, `.invert`).
THIS FILE proves the two top-of-chain specs:
* `pow22501_spec` — the helper returns (x^(2^2501), x^11);
* `invert_spec` — `invert` is total under the 2⁵⁴ limb invariant and
denotes ⟪x⟫⁻¹ (covering ⟪x⟫ = 0 as well).
plus three small `'`-wrappers around the mul/square/pow2k specs so the
`step*`/`let*` proof automation can apply them without explicit limb lists.
ROLE IN THE MAIN THEOREM. `invert_spec` is exactly what FieldMain.lean
packages as `run_invert` / the `inv_ok` field of `IsFieldImplementation`,
and what makes `impl_mul_inv_cancel` (existence of multiplicative
inverses — the defining axiom of a FIELD as opposed to a ring) true for
the implementation.
Imports: MulSpec/SquareSpec (the verified mul / square / pow2k machine
code) and Field (the `Field Fp` instance — needed for `⁻¹` and Fermat,
via Mathlib.FieldTheory.Finite.Basic's `ZMod.pow_card_sub_one_eq_one`).
Imported by: FieldMain.lean (the summit).
─────────────────────────────────────────────────────────────────────── -/
import Proofs.MulSpec
import Proofs.SquareSpec
import Proofs.Field
import Mathlib.FieldTheory.Finite.Basic
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 8000000
set_option maxRecDepth 8000
namespace CurveFieldProofs
/-! ## Step-friendly wrappers (no explicit limb lists in the hypotheses)
The base specs in MulSpec/SquareSpec take the 5 limbs of every argument as
explicit variables (`x0 … x4`, with a hypothesis `↑a = [x0,…,x4]`), because
their proofs compute limb by limb. The `step*`/`let*` automation that walks
a monadic body cannot invent those variables, so we re-state each spec with
the limbs existentially repackaged (via `Fe.exists_limbs`: every transpiled
`Fe`, being a Rust `[u64; 5]`, HAS some 5 limbs). `@[step]` registers each
wrapper with the automation. -/
/- RUST ANALOG: `impl Mul for FieldElement51` (the `*` operator),
curve25519/solana-ed25519/src/backend/serial/u64/field.rs (schoolbook
radix-2^51 multiplication with 19-folding) — verified in MulSpec.lean.
MATH: forall a b : Fe, Bnd(a,2^54) and Bnd(b,2^54) ==>
fe_mul a b = ok r with Bnd(r, 2^51+2^13) and ⟪r⟫ = ⟪a⟫·⟪b⟫.
LaTeX: $\mathrm{Bnd}(a,2^{54}) \wedge \mathrm{Bnd}(b,2^{54}) \Rightarrow
\llbracket r\rrbracket = \llbracket a\rrbracket\llbracket b\rrbracket$.
WHY NEEDED: `pow22501`'s body performs 9 multiplications; each application
inside `step*` uses this hypothesis-light form. -/
@[step]
theorem mul_spec' (a b : Fe) (hba : Bnd a (2^54)) (hbb : Bnd b (2^54)) :
fe_mul a b ⦃ r => Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ * ⟪b⟫ ⦄ := by
obtain ⟨x0, x1, x2, x3, x4, ha⟩ := Fe.exists_limbs a -- materialize a's limbs
obtain ⟨y0, y1, y2, y3, y4, hb⟩ := Fe.exists_limbs b -- materialize b's limbs
exact mul_spec a b x0 x1 x2 x3 x4 y0 y1 y2 y3 y4 ha hb hba hbb
/- RUST ANALOG: `FieldElement51::square`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs (implemented as
`pow2k(1)`) — verified in SquareSpec.lean.
MATH: Bnd(a,2^54) ==> fe_square a = ok r, Bnd(r, 2^51+2^13),
⟪r⟫ = ⟪a⟫·⟪a⟫.
WHY NEEDED: the first three steps of the pow22501 chain are squarings. -/
@[step]
theorem square_spec' (a : Fe) (hba : Bnd a (2^54)) :
fe_square a ⦃ r => Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ * ⟪a⟫ ⦄ := by
obtain ⟨x0, x1, x2, x3, x4, ha⟩ := Fe.exists_limbs a
exact square_spec a x0 x1 x2 x3 x4 ha hba
/- RUST ANALOG: `FieldElement51::pow2k` (k successive squarings, k ≥ 1),
curve25519/solana-ed25519/src/backend/serial/u64/field.rs — verified in
SquareSpec.lean (loop invariant over k).
MATH: Bnd(a,2^54) and 1 ≤ k ==> fe_pow2k a k = ok r,
Bnd(r, 2^51+2^13), ⟪r⟫ = ⟪a⟫ ^ (2^k).
(k = 0 is excluded: the Rust body `debug_assert!(k > 0)` panics on it,
and indeed pow2k(0) would not return a^1 but loop zero times — the
precondition mirrors the code's own contract.)
WHY NEEDED: the chain's big shifts (×2^5, ×2^10, …, ×2^100) are pow2k
calls; `invert` itself ends with a pow2k(5). -/
@[step]
theorem pow2k_spec' (a : Fe) (k : U32) (hba : Bnd a (2^54)) (hk : 1 ≤ k.val) :
fe_pow2k a k ⦃ r => Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ ^ (2^k.val) ⦄ := by
obtain ⟨x0, x1, x2, x3, x4, ha⟩ := Fe.exists_limbs a
exact pow2k_spec a k x0 x1 x2 x3 x4 ha hba hk
/-- Discharge: linear arithmetic, or a `Bnd` weakening from any hypothesis.
Every step of the chain needs its inputs bounded by 2⁵⁴, but the previous
step only guarantees 2⁵¹ + 2¹³; this side-condition tactic closes such
goals either by `scalar_tac` (linear arithmetic over machine integers) or
by weakening an existing `Bnd _ c` hypothesis with `Bnd.mono` and
`c ≤ 2⁵⁴` by `norm_num`. Passed as the discharger to `step*`/`let*`.
WHY NEEDED: keeps the 22-step chain proof to a single `step* by bnd`. -/
macro "bnd" : tactic =>
`(tactic| (first
| scalar_tac
| exact Bnd.mono (by assumption) (by norm_num)))
/-! ## The pow22501 addition chain -/
/- RUST ANALOG: `FieldElement51::pow22501`,
curve25519/solana-ed25519/src/field.rs:141-175 (transpiled as
`field.FieldElement51.pow22501`, gen/CurveField/Funs.lean).
MATH: Bnd(a,2^54) ==> pow22501 a = ok (t19, t3) with
Bnd(t19, 2^52), Bnd(t3, 2^52),
⟪t19⟫ = ⟪a⟫ ^ (2^250 1) and ⟪t3⟫ = ⟪a⟫ ^ 11.
THE ADDITION CHAIN (writing x = ⟪a⟫; squaring doubles the exponent,
pow2k(k) multiplies it by 2^k, mul adds exponents). Exponent bookkeeping,
matching the temporaries of the Rust source line by line (the transpiler
names t0.square().square()'s intermediate `fe`):
t0 = x^2 square x
fe = x^4 square t0
t1 = x^8 square fe
t2 = x * t1 = x^9 exps 0 + 8
t3 = t0 * t2 = x^11 exps 2 + 9 (output 2)
t4 = t3^2 = x^22 square
t5 = t2 * t4 = x^31 = x^(2^5 1) exps 9 + 22
t6 = t5^(2^5) = x^(2^10 2^5) pow2k 5
t7 = t6 * t5 = x^(2^10 1) fill low 5 bits
t8 = t7^(2^10) = x^(2^20 2^10) pow2k 10
t9 = t8 * t7 = x^(2^20 1)
t10 = t9^(2^20) = x^(2^40 2^20) pow2k 20
t11 = t10 * t9 = x^(2^40 1)
t12 = t11^(2^10) = x^(2^50 2^10) pow2k 10
t13 = t12 * t7 = x^(2^50 1)
t14 = t13^(2^50) = x^(2^100 2^50) pow2k 50
t15 = t14 * t13 = x^(2^100 1)
t16 = t15^(2^100) = x^(2^200 2^100) pow2k 100
t17 = t16 * t15 = x^(2^200 1)
t18 = t17^(2^50) = x^(2^250 2^50) pow2k 50
t19 = t18 * t13 = x^(2^250 1) (output 1)
i.e. the classic "all-ones exponent" ladder: an exponent 2^n 1 (n ones
in binary), shifted left k places by pow2k, then ORed with a smaller
all-ones block by one multiplication.
Bounds: every mul/square/pow2k output is < 2^51 + 2^13 ≤ 2^52 ≤ 2^54, so
each step's output is a legal input for the next — that is the entire
panic-freedom argument, threaded automatically by the `bnd` discharger.
WHY NEEDED: `invert` (below) and the Rust `pow_p58` both build on this
helper; ⟪t19⟫ = x^(2^2501) and ⟪t3⟫ = x^11 are exactly the two facts
`invert_spec` combines into x^(p2). -/
theorem pow22501_spec (a : Fe) (hba : Bnd a (2^54)) :
field.FieldElement51.pow22501 a ⦃ rr =>
Bnd rr.1 (2^52) ∧ Bnd rr.2 (2^52) ∧
⟪rr.1⟫ = ⟪a⟫ ^ (2^250 - 1) ∧ ⟪rr.2⟫ = ⟪a⟫ ^ 11 ⦄ := by
-- expose the transpiled 22-step monadic body
unfold field.FieldElement51.pow22501
-- walk all 22 squarings/pow2ks/muls with the @[step] specs above;
-- every 2^54-bound side condition is discharged by `bnd`
step* by bnd
-- post-condition: two bounds (Bnd.mono weakening) + two exponent equations
refine ⟨by bnd, by bnd, ?_, ?_⟩ <;>
· simp_all only []
-- collapse the chain: every post is ⟪·⟫ = (earlier)^e or a product;
-- rewrite them all, then close by exponent arithmetic.
simp_all [← pow_mul, ← pow_add, ← pow_succ]
try ring_nf
try norm_num
/- RUST ANALOG: `FieldElement51::invert`,
curve25519/solana-ed25519/src/field.rs:239-248 (transpiled as
`field.FieldElement51.invert` = the abbrev `fe_invert`,
gen/CurveField/Funs.lean):
let (t19, t3) = self.pow22501(); // t19 = x^(2^2501), t3 = x^11
let t20 = t19.pow2k(5); // t20 = x^(2^2552^5)
let t21 = &t20 * &t3; // t21 = x^(2^25532+11) = x^(2^25521)
MATH: forall a : Fe, Bnd(a,2^54) ==>
fe_invert a = ok r, Bnd(r, 2^52), ⟪r⟫ = ⟪a⟫⁻¹ in F_p.
LaTeX: $\mathrm{Bnd}(a,2^{54}) \Rightarrow
\llbracket \mathrm{invert}\,a\rrbracket = \llbracket a\rrbracket^{-1}$.
Exponent: (2^250 1)·2^5 + 11 = 2^255 32 + 11 = 2^255 21 = p 2
(since p = 2^255 19). Then:
* if ⟪a⟫ ≠ 0: Fermat's little theorem (p prime — Proofs/P25519.lean via
the `Fact` instance in Proofs/Field.lean) gives a^(p1) = 1, hence
a · a^(p2) = a^(p1) = 1, hence a^(p2) = a⁻¹;
* if ⟪a⟫ = 0: 0^(p2) = 0 (p2 > 0), and mathlib defines 0⁻¹ = 0 in any
field, so the equation ⟪r⟫ = ⟪a⟫⁻¹ holds UNCONDITIONALLY — matching the
documented Rust behavior "This function returns zero on input zero".
Note this only fixes the exponent arithmetic and Fermat; that mul/pow2k
really compute products/powers (95 machine ops each, carries, 19-folding)
was proved once and for all in MulSpec/SquareSpec.
WHY NEEDED: this is the totality + correctness of field inversion —
packaged by FieldMain.lean as `run_invert` / `inv_ok`, the ingredient
that upgrades "commutative ring" to "field" (`impl_mul_inv_cancel`). -/
theorem invert_spec (a : Fe) (hba : Bnd a (2^54)) :
fe_invert a ⦃ r => Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫⁻¹ ⦄ := by
-- expose the 3-step transpiled body
unfold fe_invert field.FieldElement51.invert
-- (t19, t3) ← pow22501 a with ⟪t19⟫ = ⟪a⟫^(2^2501), ⟪t3⟫ = ⟪a⟫^11
let* ⟨ t19, t3, h1, h2, h3, h4 ⟩ ← pow22501_spec by bnd
-- t20 ← pow2k t19 5 with ⟪t20⟫ = ⟪t19⟫^(2^5)
let* ⟨ t20, t20_post1, t20_post2 ⟩ ← pow2k_spec' by bnd
-- r ← mul t20 t3 with ⟪r⟫ = ⟪t20⟫·⟪t3⟫
let* ⟨ r, r_post1, r_post2 ⟩ ← mul_spec' by bnd
refine ⟨by bnd, ?_⟩
-- ⟪r⟫ = (⟪a⟫^(2^2501))^(2^5) · ⟪a⟫^11 = ⟪a⟫^(2^25521) = ⟪a⟫^(P2) = ⟪a⟫⁻¹
rw [r_post2, t20_post2, h3, h4]
-- merge powers: (x^m)^n = x^(m·n), x^m · x^n = x^(m+n)
rw [← pow_mul, ← pow_add]
-- the exponent really is p 2 (pure numeral arithmetic)
have hexp : (2^250 - 1) * 2^5 + 11 = P - 2 := by
norm_num [P]
rw [hexp]
by_cases h0 : ⟪a⟫ = 0
· -- zero case: 0^(P2) = 0 = 0⁻¹ (mathlib convention, P2 > 0)
rw [h0]
rw [zero_pow (by norm_num [P]), inv_zero]
· -- Fermat: a^(P1) = 1, hence a · a^(P2) = 1, hence a^(P2) = a⁻¹.
have h1 : ⟪a⟫ ^ (P - 1) = 1 := ZMod.pow_card_sub_one_eq_one h0
have hmul : ⟪a⟫ * ⟪a⟫ ^ (P - 2) = 1 := by
have hsplit : ⟪a⟫ * ⟪a⟫ ^ (P - 2) = ⟪a⟫ ^ (P - 1) := by
conv_rhs => rw [show P - 1 = (P - 2) + 1 by norm_num [P]]
rw [pow_succ]
ring
rw [hsplit, h1]
-- cancel a on the left of a · a^(P2) = 1 = a · a⁻¹
exact mul_left_cancel₀ h0 (by rw [hmul, mul_inv_cancel₀ h0])
end CurveFieldProofs

View file

@ -0,0 +1,542 @@
/- ─────────────────────────────────────────────────────────────────────────────
Proofs/MulSpec.lean — total correctness of field MULTIPLICATION
WHAT THIS FILE PROVES (one big theorem, `mul_spec`)
ASCII: forall a b : Fe, Bnd(a, 2^54) and Bnd(b, 2^54) ==>
fe_mul a b = ok r with Bnd(r, 2^51 + 2^13)
and [[r]] = [[a]] * [[b]] in F_p, p = 2^255 - 19
LaTeX: $\forall a\,b,\ \mathrm{Bnd}(a,2^{54})\wedge\mathrm{Bnd}(b,2^{54})
\Rightarrow \exists r,\ \mathrm{fe\_mul}\,a\,b = \mathrm{ok}\,r \wedge
\llbracket r\rrbracket=\llbracket a\rrbracket\cdot\llbracket b\rrbracket$
Here `Bnd x c` = "all 5 limbs of x are < c" and [[x]] = ⟪x⟫ =
(x0 + x1·2^51 + x2·2^102 + x3·2^153 + x4·2^204) mod p — both defined in
Proofs/Denote.lean. "fe_mul a b = ok r" (written `fe_mul a b ⦃ post ⦄`)
means TOTAL correctness: the Rust code never panics/overflows on such
inputs, *including* its ten `debug_assert!`s, which Charon keeps as
`massert` obligations that we must PROVE (they are theorems, not
assumptions).
RUST ANALOG
`impl Mul<&'a FieldElement51> for &FieldElement51 { fn mul }`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:115-213, with its
nested helper `m` (widening 64×64→128 multiply, field.rs:119), the local
constant `LOW_51_BIT_MASK = (1u64 << 51) - 1` (field.rs:172) and the ten
`debug_assert!(limb < 1 << 54)` checks (field.rs:162-166). The mechanical
Lean model of that code is the generated function
`Shared0FieldElement51.Insts.CoreOpsArithMulSharedAFieldElement51FieldElement51.mul`
in gen/CurveField/Funs.lean, aliased `fe_mul` in Proofs/Denote.lean.
THE ALGORITHM BEING VERIFIED (radix-2^51 schoolbook multiply, 19-folded)
Writing A = Sum_i x_i 2^(51 i) and B = Sum_j y_j 2^(51 j), the full product
is Sum_{i,j} x_i y_j 2^(51(i+j)) — ten powers 2^0 .. 2^(51·8). Because
2^255 = p + 19, i.e. 2^255 ≡ 19 (mod p), every high term with i+j ≥ 5 is
folded down: x_i y_j 2^(51(i+j)) ≡ 19 · x_i y_j 2^(51(i+j-5)). The code
therefore precomputes b1_19 = 19·y1, …, b4_19 = 19·y4 (u64; fits since
19·2^54 < 2^64) and accumulates five u128 columns (field.rs:144-148):
c0 = x0·y0 + 19·(x4·y1 + x3·y2 + x2·y3 + x1·y4)
c1 = x1·y0 + x0·y1 + 19·(x4·y2 + x3·y3 + x2·y4)
c2 = x2·y0 + x1·y1 + x0·y2 + 19·(x4·y3 + x3·y4)
c3 = x3·y0 + x2·y1 + x1·y2 + x0·y3 + 19·(x4·y4)
c4 = x4·y0 + x3·y1 + x2·y2 + x1·y3 + x0·y4
With limbs < 2^54 each column is < (1+i + 19·(4-i))·2^108 ≤ 77·2^108 < 2^115,
far below the u128 limit 2^128. Then a single carry pass (field.rs:175-188)
normalizes: c_{k+1} += c_k >> 51, out[k] = c_k & mask; the final carry
(multiples of 2^255) re-enters as out[0] += 19·carry (field.rs:205), and one
mini-carry out[1] += out[0] >> 51; out[0] &= mask (field.rs:208-209) leaves
all limbs < 2^51 + 2^13.
PROOF ARCHITECTURE — a fully NAMED machine-checked symbolic execution
The generated body is a chain of ~95 fallible machine operations in the
`Result` monad. The script mirrors it step by step:
let* ⟨ x, x_post ⟩ ← spec_lemma by tac
is the Aeneas "progress" step: it consumes the next monadic operation,
applies the registered spec lemma for it (`m_spec`, `U128.add_spec`,
`Array.index_usize_spec`, …), names the result `x` and its postcondition
`x_post`, and discharges the lemma's precondition — i.e. the u64/u128
overflow side condition of that very operation — with the `by tac` block.
After each step an explicit bound fact `hv_… : ….val < n·2^108` (or an
identification `he_… : i = x3` of which limb a read returned) is recorded,
so that every later side condition is LINEAR arithmetic over already-named
quantities and `omega`/`scalar_tac` close it without nonlinear reasoning.
The final assembly has three layers:
(1) hkey — exact carry accounting:
feVal r + p·carry = c0 + 2^51·c1 + 2^102·c2 + 2^153·c3 + 2^204·c4
(pure div/mod bookkeeping of the carry pass; `omega`).
(2) hnc0hnc4 — each column c_k as a polynomial in the input limbs
(substitute all step postconditions, then `ring`).
(3) hAB — the F_p identity A·B = Sum_k c_k·2^(51 k), via
`linear_combination D * h255` where h255 : (2:F_p)^255 = 19
and D is the explicit wrap-around polynomial
D = (x1·y4 + x2·y3 + x3·y2 + x4·y1)
+ 2^51 ·(x2·y4 + x3·y3 + x4·y2)
+ 2^102·(x3·y4 + x4·y3)
+ 2^153·(x4·y4)
= Sum_{i+j ≥ 5} x_i·y_j·2^(51(i+j-5)),
because over : A·B = Sum_k c_k·2^(51 k) + (2^255 - 19)·D.
Casting (1) into F_p kills the p·carry term ((p : F_p) = 0) and chaining it
with (3) yields ⟪r⟫ = ⟪a⟫·⟪b⟫.
ROLE IN THE MAIN THEOREM (Proofs/FieldMain.lean)
`mul_spec` is the multiplicative half of `fieldImplementation`: the
corollaries impl_mul_comm/assoc, impl_one_mul, impl_mul_inv_cancel and
impl_left_distrib all run `fe_mul` and rest on this theorem.
Imports: Proofs/SubNegSpec (transitively Denote/ReduceSpec: ⟪·⟫, Bnd,
two_pow_255_eq) and mathlib's `linear_combination` tactic.
Dependents: Proofs/SquareSpec.lean (same architecture, reuses the `dis`
macro), Proofs/InvertSpec.lean (mul steps of the pow22501 chain),
Proofs/Field.lean and Proofs/FieldMain.lean.
PROVENANCE: the script was generated by /tmp/gen_mul_proof.py to mirror the
generated body, then hand-tuned.
───────────────────────────────────────────────────────────────────────── -/
import Proofs.SubNegSpec
import Mathlib.Tactic.LinearCombination
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 8000000
set_option maxRecDepth 8000
namespace CurveFieldProofs
/-- Discharge tactic shared by most steps: substitute the equations introduced
so far, simplify array reads/writes (`Array.set_val_eq`) with all
hypotheses, then run `scalar_tac` (Aeneas's linear-arithmetic decision
procedure over machine integers). This is what closes each overflow side
condition once the `hv_*` bound facts have made it linear.
WHY NEEDED: keeps the ~95 `let*` steps below one-liners. -/
macro "dis" : tactic =>
`(tactic| (subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac))
/-- The u128 widening product `m(x, y) = (x as u128) * (y as u128)`.
Rust: nested `fn m(x: u64, y: u64) -> u128`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:119.
MATH: forall x y : u64, m x y = ok z with z.val = x.val * y.val — the
product of two u64 is < 2^64 · 2^64 = 2^128, so the u128 multiply that the
transpiler emits after the two casts can NEVER overflow; this lemma proves
that once and for all.
WHY NEEDED: every one of the 25 partial products in `mul` goes through
`m`; tagging the lemma `@[step]` registers it with the `let*` machinery. -/
@[step]
theorem m_spec (x y : U64) :
backend.serial.u64.field.MulShared0FieldElement51SharedAFieldElement51FieldElement51.mul.m
x y ⦃ z => z.val = x.val * y.val ⦄ := by
unfold
backend.serial.u64.field.MulShared0FieldElement51SharedAFieldElement51FieldElement51.mul.m
-- the two u64 inputs are < 2^64 by construction …
have hx : x.val < 2^64 := x.hBounds
have hy : y.val < 2^64 := y.hBounds
-- … hence the product fits in a u128: the only side condition of the body
have hxy : x.val * y.val < 2^128 := by
calc x.val * y.val < 2^64 * 2^64 := Nat.mul_lt_mul'' hx hy
_ = 2^128 := by norm_num
-- run the 3 ops (cast, cast, mul); `dis` discharges each side condition
step* by dis
/-- The mask constant in `mul` evaluates to 2⁵¹ 1.
Rust: `const LOW_51_BIT_MASK: u64 = (1u64 << 51) - 1;`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:172.
MATH: the constant's generated body (shift then subtract) succeeds and
returns 2251799813685247 = 2^51 - 1. `x & LOW_51_BIT_MASK` is therefore
`x mod 2^51` — the "keep the low limb" half of every carry step.
WHY NEEDED: the carry pass reads this constant once; its value must be
known exactly for the div/mod accounting in `hkey`. -/
@[step]
theorem mul_mask_spec :
backend.serial.u64.field.MulShared0FieldElement51SharedAFieldElement51FieldElement51.mul.LOW_51_BIT_MASK
⦃ m => m.val = 2251799813685247 ⦄ := by
unfold
backend.serial.u64.field.MulShared0FieldElement51SharedAFieldElement51FieldElement51.mul.LOW_51_BIT_MASK
step*
/-- Main multiplication theorem (see the file header for the full story).
Rust: `impl Mul<&FieldElement51> for &FieldElement51 { fn mul }`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:115-213.
MATH (ASCII):
Bnd(a,2^54) and Bnd(b,2^54) ==> fe_mul a b = ok r
with Bnd(r, 2^51 + 2^13) and [[r]] = [[a]]·[[b]] in F_p.
The limb lists [x0..x4] / [y0..y4] are taken as explicit arguments so that
every intermediate bound can be stated about a NAMED limb.
WHY NEEDED: sole support of the impl_mul_* field axioms in FieldMain;
also the workhorse inside InvertSpec's exponentiation chain. -/
theorem mul_spec (a b : Fe) (x0 x1 x2 x3 x4 y0 y1 y2 y3 y4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hb : (↑b : List U64) = [y0, y1, y2, y3, y4])
(hba : Bnd a (2^54)) (hbb : Bnd b (2^54)) :
fe_mul a b ⦃ r => Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ * ⟪b⟫ ⦄ := by
-- turn the abstract invariants into 5+5 named limb bounds x_i < 2^54, y_i < 2^54
rw [Bnd_eq a x0 x1 x2 x3 x4 _ ha] at hba
rw [Bnd_eq b y0 y1 y2 y3 y4 _ hb] at hbb
-- expose the generated body (fe_mul is a definitional alias)
unfold fe_mul
Shared0FieldElement51.Insts.CoreOpsArithMulSharedAFieldElement51FieldElement51.mul
-- ── b*_19 precomputations + limb loads (field.rs:138-141) ────────────────
-- each read `i… = y_j` is identified (he_*) and bounded (hv_*); each
-- 19·y_j u64 multiply needs 19·2^54 < 2^64 — discharged inside `dis`
let* ⟨ i, i_post ⟩ ← Array.index_usize_spec by dis
have he_i : i = y1 := by simp [i_post, hb]
have hv_i : i.val < 2^54 := by rw [he_i]; omega
let* ⟨ b1_19, b1_19_post ⟩ ← U64.mul_spec by dis
have hv_b1_19 : b1_19.val < 19 * 2^54 := by rw [b1_19_post]; omega
let* ⟨ i1, i1_post ⟩ ← Array.index_usize_spec by dis
have he_i1 : i1 = y2 := by simp [i1_post, hb]
have hv_i1 : i1.val < 2^54 := by rw [he_i1]; omega
let* ⟨ b2_19, b2_19_post ⟩ ← U64.mul_spec by dis
have hv_b2_19 : b2_19.val < 19 * 2^54 := by rw [b2_19_post]; omega
let* ⟨ i2, i2_post ⟩ ← Array.index_usize_spec by dis
have he_i2 : i2 = y3 := by simp [i2_post, hb]
have hv_i2 : i2.val < 2^54 := by rw [he_i2]; omega
let* ⟨ b3_19, b3_19_post ⟩ ← U64.mul_spec by dis
have hv_b3_19 : b3_19.val < 19 * 2^54 := by rw [b3_19_post]; omega
let* ⟨ i3, i3_post ⟩ ← Array.index_usize_spec by dis
have he_i3 : i3 = y4 := by simp [i3_post, hb]
have hv_i3 : i3.val < 2^54 := by rw [he_i3]; omega
let* ⟨ b4_19, b4_19_post ⟩ ← U64.mul_spec by dis
have hv_b4_19 : b4_19.val < 19 * 2^54 := by rw [b4_19_post]; omega
let* ⟨ i4, i4_post ⟩ ← Array.index_usize_spec by dis
have he_i4 : i4 = x0 := by simp [i4_post, ha]
have hv_i4 : i4.val < 2^54 := by rw [he_i4]; omega
let* ⟨ i5, i5_post ⟩ ← Array.index_usize_spec by dis
have he_i5 : i5 = y0 := by simp [i5_post, hb]
have hv_i5 : i5.val < 2^54 := by rw [he_i5]; omega
-- ── column c0 = a0*b0 + 19*(a4*b1 + a3*b2 + a2*b3 + a1*b4) (field.rs:144) ─
-- each `m` product is < 2^108 (or < 19·2^108 when one factor is a b*_19);
-- the running u128 sums stay < 77·2^108 < 2^128, so each add is in range
let* ⟨ i6, i6_post ⟩ ← m_spec by dis
have hv_i6 : i6.val < 2^108 := by
rw [i6_post]; have := Nat.mul_lt_mul'' hv_i4 hv_i5; omega
let* ⟨ i7, i7_post ⟩ ← Array.index_usize_spec by dis
have he_i7 : i7 = x4 := by simp [i7_post, ha]
have hv_i7 : i7.val < 2^54 := by rw [he_i7]; omega
let* ⟨ i8, i8_post ⟩ ← m_spec by dis
have hv_i8 : i8.val < 2^54 * (19 * 2^54) := by
rw [i8_post]; have := Nat.mul_lt_mul'' hv_i7 hv_b1_19; omega
let* ⟨ i9, i9_post ⟩ ← U128.add_spec by scalar_tac
have hv_i9 : i9.val < 20 * 2^108 := by rw [i9_post]; omega
let* ⟨ i10, i10_post ⟩ ← Array.index_usize_spec by dis
have he_i10 : i10 = x3 := by simp [i10_post, ha]
have hv_i10 : i10.val < 2^54 := by rw [he_i10]; omega
let* ⟨ i11, i11_post ⟩ ← m_spec by dis
have hv_i11 : i11.val < 2^54 * (19 * 2^54) := by
rw [i11_post]; have := Nat.mul_lt_mul'' hv_i10 hv_b2_19; omega
let* ⟨ i12, i12_post ⟩ ← U128.add_spec by scalar_tac
have hv_i12 : i12.val < 39 * 2^108 := by rw [i12_post]; omega
let* ⟨ i13, i13_post ⟩ ← Array.index_usize_spec by dis
have he_i13 : i13 = x2 := by simp [i13_post, ha]
have hv_i13 : i13.val < 2^54 := by rw [he_i13]; omega
let* ⟨ i14, i14_post ⟩ ← m_spec by dis
have hv_i14 : i14.val < 2^54 * (19 * 2^54) := by
rw [i14_post]; have := Nat.mul_lt_mul'' hv_i13 hv_b3_19; omega
let* ⟨ i15, i15_post ⟩ ← U128.add_spec by scalar_tac
have hv_i15 : i15.val < 58 * 2^108 := by rw [i15_post]; omega
let* ⟨ i16, i16_post ⟩ ← Array.index_usize_spec by dis
have he_i16 : i16 = x1 := by simp [i16_post, ha]
have hv_i16 : i16.val < 2^54 := by rw [he_i16]; omega
let* ⟨ i17, i17_post ⟩ ← m_spec by dis
have hv_i17 : i17.val < 2^54 * (19 * 2^54) := by
rw [i17_post]; have := Nat.mul_lt_mul'' hv_i16 hv_b4_19; omega
let* ⟨ c0, c0_post ⟩ ← U128.add_spec by scalar_tac
have hv_c0 : c0.val < 77 * 2^108 := by rw [c0_post]; omega
-- ── column c1 = a1*b0 + a0*b1 + 19*(a4*b2 + a3*b3 + a2*b4) (field.rs:145) ─
let* ⟨ i18, i18_post ⟩ ← m_spec by dis
have hv_i18 : i18.val < 2^108 := by
rw [i18_post]; have := Nat.mul_lt_mul'' hv_i16 hv_i5; omega
let* ⟨ i19, i19_post ⟩ ← m_spec by dis
have hv_i19 : i19.val < 2^108 := by
rw [i19_post]; have := Nat.mul_lt_mul'' hv_i4 hv_i; omega
let* ⟨ i20, i20_post ⟩ ← U128.add_spec by scalar_tac
have hv_i20 : i20.val < 2 * 2^108 := by rw [i20_post]; omega
let* ⟨ i21, i21_post ⟩ ← m_spec by dis
have hv_i21 : i21.val < 2^54 * (19 * 2^54) := by
rw [i21_post]; have := Nat.mul_lt_mul'' hv_i7 hv_b2_19; omega
let* ⟨ i22, i22_post ⟩ ← U128.add_spec by scalar_tac
have hv_i22 : i22.val < 21 * 2^108 := by rw [i22_post]; omega
let* ⟨ i23, i23_post ⟩ ← m_spec by dis
have hv_i23 : i23.val < 2^54 * (19 * 2^54) := by
rw [i23_post]; have := Nat.mul_lt_mul'' hv_i10 hv_b3_19; omega
let* ⟨ i24, i24_post ⟩ ← U128.add_spec by scalar_tac
have hv_i24 : i24.val < 40 * 2^108 := by rw [i24_post]; omega
let* ⟨ i25, i25_post ⟩ ← m_spec by dis
have hv_i25 : i25.val < 2^54 * (19 * 2^54) := by
rw [i25_post]; have := Nat.mul_lt_mul'' hv_i13 hv_b4_19; omega
let* ⟨ c1, c1_post ⟩ ← U128.add_spec by scalar_tac
have hv_c1 : c1.val < 59 * 2^108 := by rw [c1_post]; omega
-- ── column c2 = a2*b0 + a1*b1 + a0*b2 + 19*(a4*b3 + a3*b4) (field.rs:146) ─
let* ⟨ i26, i26_post ⟩ ← m_spec by dis
have hv_i26 : i26.val < 2^108 := by
rw [i26_post]; have := Nat.mul_lt_mul'' hv_i13 hv_i5; omega
let* ⟨ i27, i27_post ⟩ ← m_spec by dis
have hv_i27 : i27.val < 2^108 := by
rw [i27_post]; have := Nat.mul_lt_mul'' hv_i16 hv_i; omega
let* ⟨ i28, i28_post ⟩ ← U128.add_spec by scalar_tac
have hv_i28 : i28.val < 2 * 2^108 := by rw [i28_post]; omega
let* ⟨ i29, i29_post ⟩ ← m_spec by dis
have hv_i29 : i29.val < 2^108 := by
rw [i29_post]; have := Nat.mul_lt_mul'' hv_i4 hv_i1; omega
let* ⟨ i30, i30_post ⟩ ← U128.add_spec by scalar_tac
have hv_i30 : i30.val < 3 * 2^108 := by rw [i30_post]; omega
let* ⟨ i31, i31_post ⟩ ← m_spec by dis
have hv_i31 : i31.val < 2^54 * (19 * 2^54) := by
rw [i31_post]; have := Nat.mul_lt_mul'' hv_i7 hv_b3_19; omega
let* ⟨ i32, i32_post ⟩ ← U128.add_spec by scalar_tac
have hv_i32 : i32.val < 22 * 2^108 := by rw [i32_post]; omega
let* ⟨ i33, i33_post ⟩ ← m_spec by dis
have hv_i33 : i33.val < 2^54 * (19 * 2^54) := by
rw [i33_post]; have := Nat.mul_lt_mul'' hv_i10 hv_b4_19; omega
let* ⟨ c2, c2_post ⟩ ← U128.add_spec by scalar_tac
have hv_c2 : c2.val < 41 * 2^108 := by rw [c2_post]; omega
-- ── column c3 = a3*b0 + a2*b1 + a1*b2 + a0*b3 + 19*a4*b4 (field.rs:147) ──
let* ⟨ i34, i34_post ⟩ ← m_spec by dis
have hv_i34 : i34.val < 2^108 := by
rw [i34_post]; have := Nat.mul_lt_mul'' hv_i10 hv_i5; omega
let* ⟨ i35, i35_post ⟩ ← m_spec by dis
have hv_i35 : i35.val < 2^108 := by
rw [i35_post]; have := Nat.mul_lt_mul'' hv_i13 hv_i; omega
let* ⟨ i36, i36_post ⟩ ← U128.add_spec by scalar_tac
have hv_i36 : i36.val < 2 * 2^108 := by rw [i36_post]; omega
let* ⟨ i37, i37_post ⟩ ← m_spec by dis
have hv_i37 : i37.val < 2^108 := by
rw [i37_post]; have := Nat.mul_lt_mul'' hv_i16 hv_i1; omega
let* ⟨ i38, i38_post ⟩ ← U128.add_spec by scalar_tac
have hv_i38 : i38.val < 3 * 2^108 := by rw [i38_post]; omega
let* ⟨ i39, i39_post ⟩ ← m_spec by dis
have hv_i39 : i39.val < 2^108 := by
rw [i39_post]; have := Nat.mul_lt_mul'' hv_i4 hv_i2; omega
let* ⟨ i40, i40_post ⟩ ← U128.add_spec by scalar_tac
have hv_i40 : i40.val < 4 * 2^108 := by rw [i40_post]; omega
let* ⟨ i41, i41_post ⟩ ← m_spec by dis
have hv_i41 : i41.val < 2^54 * (19 * 2^54) := by
rw [i41_post]; have := Nat.mul_lt_mul'' hv_i7 hv_b4_19; omega
let* ⟨ c3, c3_post ⟩ ← U128.add_spec by scalar_tac
have hv_c3 : c3.val < 23 * 2^108 := by rw [c3_post]; omega
-- ── column c4 = a4*b0 + a3*b1 + a2*b2 + a1*b3 + a0*b4 (field.rs:148) ─────
-- no 19-folding here: i+j = 4 never wraps past 2^255
let* ⟨ i42, i42_post ⟩ ← m_spec by dis
have hv_i42 : i42.val < 2^108 := by
rw [i42_post]; have := Nat.mul_lt_mul'' hv_i7 hv_i5; omega
let* ⟨ i43, i43_post ⟩ ← m_spec by dis
have hv_i43 : i43.val < 2^108 := by
rw [i43_post]; have := Nat.mul_lt_mul'' hv_i10 hv_i; omega
let* ⟨ i44, i44_post ⟩ ← U128.add_spec by scalar_tac
have hv_i44 : i44.val < 2 * 2^108 := by rw [i44_post]; omega
let* ⟨ i45, i45_post ⟩ ← m_spec by dis
have hv_i45 : i45.val < 2^108 := by
rw [i45_post]; have := Nat.mul_lt_mul'' hv_i13 hv_i1; omega
let* ⟨ i46, i46_post ⟩ ← U128.add_spec by scalar_tac
have hv_i46 : i46.val < 3 * 2^108 := by rw [i46_post]; omega
let* ⟨ i47, i47_post ⟩ ← m_spec by dis
have hv_i47 : i47.val < 2^108 := by
rw [i47_post]; have := Nat.mul_lt_mul'' hv_i16 hv_i2; omega
let* ⟨ i48, i48_post ⟩ ← U128.add_spec by scalar_tac
have hv_i48 : i48.val < 4 * 2^108 := by rw [i48_post]; omega
let* ⟨ i49, i49_post ⟩ ← m_spec by dis
have hv_i49 : i49.val < 2^108 := by
rw [i49_post]; have := Nat.mul_lt_mul'' hv_i4 hv_i3; omega
let* ⟨ c4, c4_post ⟩ ← U128.add_spec by scalar_tac
have hv_c4 : c4.val < 5 * 2^108 := by rw [c4_post]; omega
-- ── the ten debug_assert!(limb < 2^54) (field.rs:162-166) ────────────────
-- Rust debug_assert! survives translation as `massert`; `massert_spec`
-- requires us to PROVE each asserted bound (from hba/hbb via scalar_tac) —
-- the asserts are verified, not assumed. i50 evaluates `1 << 54` = 2^54.
let* ⟨ i50, i50_post1, i50_post2 ⟩ ← U64.ShiftLeft_IScalar_spec by dis
have hv_i50 : i50.val = 2^54 := by
rw [i50_post1]; simp [Nat.shiftLeft_eq, U64.size, U64.numBits]
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
-- ── carry pass (field.rs:175-188). Pattern per limb k:
-- c_{k+1} += (c_k >> 51) as u64 as u128; out[k] = (c_k as u64) & mask.
-- The u128→u64→u128 cast round-trip is lossless exactly because
-- c_k/2^51 < 77·2^57 < 2^64 — that is what each hv_* div fact certifies. ──
-- carry c0 -> c11; limb 0
let* ⟨ i51, i51_post1, i51_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ i52, i52_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i53, i53_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_i53 : i53.val = c0.val / 2^51 := by
simp [i53_post, i52_post, i51_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ c11, c11_post ⟩ ← U128.add_spec by scalar_tac
have hv_c11 : c11.val < 60 * 2^108 := by
rw [c11_post]; omega
let* ⟨ i54, i54_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i55, i55_post ⟩ ← mul_mask_spec by dis
let* ⟨ i56, i56_post1, i56_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i56 : i56.val = c0.val % 2^51 := by
simp [i56_post1, i54_post, i55_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ out1, out1_post ⟩ ← Array.update_spec by scalar_tac
-- carry c11 -> c21; limb 1
let* ⟨ i57, i57_post1, i57_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ i58, i58_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i59, i59_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_i59 : i59.val = c11.val / 2^51 := by
simp [i59_post, i58_post, i57_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ c21, c21_post ⟩ ← U128.add_spec by scalar_tac
have hv_c21 : c21.val < 42 * 2^108 := by
rw [c21_post]; omega
let* ⟨ i60, i60_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i61, i61_post1, i61_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i61 : i61.val = c11.val % 2^51 := by
simp [i61_post1, i60_post, i55_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ out2, out2_post ⟩ ← Array.update_spec by scalar_tac
-- carry c21 -> c31; limb 2
let* ⟨ i62, i62_post1, i62_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ i63, i63_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i64, i64_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_i64 : i64.val = c21.val / 2^51 := by
simp [i64_post, i63_post, i62_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ c31, c31_post ⟩ ← U128.add_spec by scalar_tac
have hv_c31 : c31.val < 24 * 2^108 := by
rw [c31_post]; omega
let* ⟨ i65, i65_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i66, i66_post1, i66_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i66 : i66.val = c21.val % 2^51 := by
simp [i66_post1, i65_post, i55_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ out3, out3_post ⟩ ← Array.update_spec by scalar_tac
-- carry c31 -> c41; limb 3
let* ⟨ i67, i67_post1, i67_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ i68, i68_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i69, i69_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_i69 : i69.val = c31.val / 2^51 := by
simp [i69_post, i68_post, i67_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ c41, c41_post ⟩ ← U128.add_spec by scalar_tac
have hv_c41 : c41.val < 6 * 2^108 := by
rw [c41_post]; omega
let* ⟨ i70, i70_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i71, i71_post1, i71_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i71 : i71.val = c31.val % 2^51 := by
simp [i71_post1, i70_post, i55_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ out4, out4_post ⟩ ← Array.update_spec by scalar_tac
-- last limb: carry out of c41 (field.rs:187-188); carry counts 2^255-units
let* ⟨ i72, i72_post1, i72_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ carry, carry_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_carry : carry.val = c41.val / 2^51 := by
simp [carry_post, i72_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ i73, i73_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i74, i74_post1, i74_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i74 : i74.val = c41.val % 2^51 := by
simp [i74_post1, i73_post, i55_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ out5, out5_post ⟩ ← Array.update_spec by scalar_tac
-- ── fold the final carry * 19 into limb 0 (field.rs:205, since 2^255 ≡ 19),
-- then the mini-carry into limb 1 (field.rs:208-209).
-- No overflow: carry < 6·2^57, so 19·carry < 2^62 and
-- out[0] + 19·carry < 2^51 + 2^62 < 2^64. ─────────────────────────────
let* ⟨ i75, i75_post ⟩ ← U64.mul_spec by scalar_tac
let* ⟨ i76, i76_post ⟩ ← Array.index_usize_spec by scalar_tac
have hv_i76 : i76.val = c0.val % 2^51 := by
simp [i76_post, out5_post, out4_post, out3_post, out2_post, out1_post,
Array.set_val_eq, hv_i56]
let* ⟨ i77, i77_post ⟩ ← U64.add_spec by scalar_tac
let* ⟨ out6, out6_post ⟩ ← Array.update_spec by scalar_tac
let* ⟨ i78, i78_post ⟩ ← Array.index_usize_spec by scalar_tac
have hv_i78 : i78.val = i77.val := by
simp [i78_post, out6_post, out5_post, out4_post, out3_post, out2_post,
out1_post, Array.set_val_eq]
let* ⟨ i79, i79_post1, i79_post2 ⟩ ← U64.ShiftRight_IScalar_spec by scalar_tac
let* ⟨ i80, i80_post ⟩ ← Array.index_usize_spec by scalar_tac
have hv_i80 : i80.val = c11.val % 2^51 := by
simp [i80_post, out6_post, out5_post, out4_post, out3_post, out2_post,
out1_post, Array.set_val_eq, hv_i61]
let* ⟨ i81, i81_post ⟩ ← U64.add_spec by scalar_tac
let* ⟨ out7, out7_post ⟩ ← Array.update_spec by scalar_tac
let* ⟨ i82, i82_post ⟩ ← Array.index_usize_spec by scalar_tac
have hv_i82 : i82.val = i77.val := by
simp [i82_post, out7_post, out6_post, out5_post, out4_post, out3_post,
out2_post, out1_post, Array.set_val_eq]
let* ⟨ i83, i83_post1, i83_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i83 : i83.val = i77.val % 2^51 := by
simp [i83_post1, hv_i82, i55_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ out8, out8_post ⟩ ← Array.update_spec by scalar_tac
-- ── symbolic execution done; assemble the postcondition ──────────────────
-- the result limb list: r = [i77 mod 2^51, (c11 mod 2^51) + i77/2^51,
-- c21 mod 2^51, c31 mod 2^51, c41 mod 2^51]
have hout : (↑out8 : List U64) = [i83, i81, i66, i71, i74] := by
simp [out8_post, out7_post, out6_post, out5_post, out4_post, out3_post,
out2_post, out1_post, Array.set_val_eq, Array.repeat_val,
List.replicate_succ]
have hv_i79 : i79.val = i77.val / 2^51 := by
simp [i79_post1, hv_i78]
-- output bound Bnd r (2^51 + 2^13): four limbs are `mod 2^51` < 2^51, and
-- limb 1 = (c11 mod 2^51) + i77/2^51 < 2^51 + 2^13 since i77 < 2^51 + 19·6·2^57
refine ⟨(Bnd_eq _ _ _ _ _ _ _ hout).mpr
⟨by omega, by omega, by omega, by omega, by omega⟩, ?_⟩
-- ── layer (1): exact accounting of the carry pass ──────────────────────
-- feVal r + p·carry = Σ_k c_k·2^(51k): the chain shifted out carry·2^255
-- and re-injected 19·carry, a net difference of exactly (2^255-19)·carry = p·carry.
-- Pure div/mod arithmetic on the named facts — omega closes it.
have hkey : feVal out8 + P * carry.val
= c0.val + 2^51*c1.val + 2^102*c2.val + 2^153*c3.val + 2^204*c4.val := by
rw [feVal_eq _ _ _ _ _ _ hout]; simp only [limbsVal, P]; omega
-- ── layer (2): product expansions of the five columns ──────────────────
-- substitute every step postcondition, then `ring` rearranges to the
-- schoolbook column polynomial in the input limbs x_i, y_j
have hnc0 : c0.val = x0.val*y0.val + 19*(x4.val*y1.val + x3.val*y2.val + x2.val*y3.val + x1.val*y4.val) := by
simp only [c0_post, i15_post, i12_post, i9_post, i6_post, i8_post, i11_post, i14_post, i17_post, b1_19_post, b2_19_post, b3_19_post, b4_19_post,
he_i, he_i1, he_i2, he_i3, he_i4, he_i5, he_i7, he_i10,
he_i13, he_i16]
ring
have hnc1 : c1.val = x1.val*y0.val + x0.val*y1.val + 19*(x4.val*y2.val + x3.val*y3.val + x2.val*y4.val) := by
simp only [c1_post, i24_post, i22_post, i20_post, i18_post, i19_post, i21_post, i23_post, i25_post, b2_19_post, b3_19_post, b4_19_post,
he_i, he_i1, he_i2, he_i3, he_i4, he_i5, he_i7, he_i10,
he_i13, he_i16]
ring
have hnc2 : c2.val = x2.val*y0.val + x1.val*y1.val + x0.val*y2.val + 19*(x4.val*y3.val + x3.val*y4.val) := by
simp only [c2_post, i32_post, i30_post, i28_post, i26_post, i27_post, i29_post, i31_post, i33_post, b3_19_post, b4_19_post,
he_i, he_i1, he_i2, he_i3, he_i4, he_i5, he_i7, he_i10,
he_i13, he_i16]
ring
have hnc3 : c3.val = x3.val*y0.val + x2.val*y1.val + x1.val*y2.val + x0.val*y3.val + 19*(x4.val*y4.val) := by
simp only [c3_post, i40_post, i38_post, i36_post, i34_post, i35_post, i37_post, i39_post, i41_post, b4_19_post,
he_i, he_i1, he_i2, he_i3, he_i4, he_i5, he_i7, he_i10,
he_i13, he_i16]
ring
have hnc4 : c4.val = x4.val*y0.val + x3.val*y1.val + x2.val*y2.val + x1.val*y3.val + x0.val*y4.val := by
simp only [c4_post, i48_post, i46_post, i44_post, i42_post, i43_post, i45_post, i47_post, i49_post,
he_i, he_i1, he_i2, he_i3, he_i4, he_i5, he_i7, he_i10,
he_i13, he_i16]
-- c4 needs no 19-rearrangement; `try ring` closes (or no-ops) the goal
try ring
-- ── layer (3): 𝔽_p bridge: A·B = Σ cᵢ·2⁵¹ⁱ using 2²⁵⁵ = 19 ───────────────
-- h255 : (2 : F_p)^255 = 19, cast from two_pow_255_eq (Proofs/Denote.lean)
have h255 : (2:Fp)^255 = 19 := by
have h := two_pow_255_eq; push_cast at h; simpa using h
-- over : A·B Σ c_k·2^(51k) = (2^255 19)·D with the wrap-around poly
-- D = Σ_{i+j≥5} x_i·y_j·2^(51(i+j5)) (spelled out literally below);
-- `linear_combination D * h255` asks `ring` to certify exactly that identity
have hAB : ((feVal a : ) : Fp) * ((feVal b : ) : Fp)
= ((c0.val : ) : Fp) + 2^51*(c1.val : ) + 2^102*(c2.val : )
+ 2^153*(c3.val : ) + 2^204*(c4.val : ) := by
rw [feVal_eq a x0 x1 x2 x3 x4 ha, feVal_eq b y0 y1 y2 y3 y4 hb]
simp only [limbsVal, hnc0, hnc1, hnc2, hnc3, hnc4]
push_cast
linear_combination ((x1.val:Fp)*(y4.val:Fp) + (x2.val:Fp)*(y3.val:Fp)
+ (x3.val:Fp)*(y2.val:Fp) + (x4.val:Fp)*(y1.val:Fp)
+ 2^51*((x2.val:Fp)*(y4.val:Fp) + (x3.val:Fp)*(y3.val:Fp) + (x4.val:Fp)*(y2.val:Fp))
+ 2^102*((x3.val:Fp)*(y4.val:Fp) + (x4.val:Fp)*(y3.val:Fp))
+ 2^153*((x4.val:Fp)*(y4.val:Fp))) * h255
-- ── conclude: cast layer (1) into F_p, where (p : F_p) = 0 kills p·carry,
-- then chain with layer (3): ⟪r⟫ = Σ c_k·2^(51k) = ⟪a⟫·⟪b⟫ ─────────────
have hc := congrArg (Nat.cast : → Fp) hkey
push_cast at hc
have hp0 : ((P : ) : Fp) = 0 := ZMod.natCast_self P
rw [hp0] at hc
simp only [zero_mul, add_zero] at hc
simp only [denote]
linear_combination hc - hAB
end CurveFieldProofs

View file

@ -0,0 +1,552 @@
/-
═══════════════════════════════════════════════════════════════════════════════
Proofs/P25519.lean — primality of the Curve25519 base-field modulus
p = 2^255 19, via Lucas/Pratt certificates
═══════════════════════════════════════════════════════════════════════════════
WHAT THIS FILE PROVES
`p25519_prime : Nat.Prime (2 ^ 255 - 19)` — the 255-bit modulus of the field
F_p implemented by the Rust crate is a prime number. Axiom-free, with no
`native_decide`: every numeric fact is checked by the Lean KERNEL (`decide`)
through a purpose-built binary modular-exponentiation function (`powMod`).
WHY THE FIELD VERIFICATION NEEDS THIS FILE
* Mathlib only provides the `Field (ZMod P)` instance — i.e. "F_p really is a
field, with all field axioms" — from `Fact (Nat.Prime P)`. The main theorem
(Proofs/FieldMain.lean, `fieldImplementation`) states that the transpiled
Rust code implements exactly that field, so primality is a prerequisite for
even *stating* the result. Proofs/Field.lean consumes `p25519_prime` to
build `P_prime : Nat.Prime P` and the `Fact`/`NeZero` instances.
* Rust analog (indirect — this file contains no transpiled code):
`FieldElement::invert`, curve25519/solana-ed25519/src/field.rs:239-248,
computes x^(p2) and its doc comment justifies this with
"x^(p-2)·x = x^(p-1) = 1 (mod p)" — Fermat's little theorem, which is
only valid because p is prime. Proofs/InvertSpec.lean formalizes exactly
that argument and needs the primality proved here.
PLACE IN THE IMPORT GRAPH
Leaf: imports only mathlib (LucasPrimality, ZMod, norm_num-prime).
Imported by Proofs/Field.lean, and through it by Proofs/InvertSpec.lean and
Proofs/FieldMain.lean.
THE PROOF TECHNIQUE, FOR THE LAY READER (Lucas test / Pratt certificates)
How do you convince a proof CHECKER that a 255-bit number n is prime without
trial division up to 2^127? Use the classical Lucas test (the basis of
"Pratt certificates", the textbook proof that PRIMES ∈ NP):
if some witness g satisfies
(1) g^(n1) ≡ 1 (mod n) (Fermat condition)
(2) g^((n1)/q) ≢ 1 (mod n) for EVERY prime q dividing n1,
then n is prime.
Why this works: (1) says the multiplicative order of g modulo n divides n1;
if that order were a PROPER divisor of n1 it would divide (n1)/q for some
prime q | n1, contradicting (2). So g has order exactly n1 in the unit
group of Z/n. But that group has only φ(n) ≤ n1 elements, so an element of
order n1 can exist only if φ(n) = n1 — which happens precisely when n is
prime. Mathlib packages this as `lucas_primality`.
The catch: condition (2) needs the COMPLETE prime factorization of n1, and
each prime factor q must itself be certified prime — recursively, by the
same test. The recursion bottoms out at factors small enough for mathlib's
`norm_num` prime checker. The published factor tree used below (any
factoring tool reproduces it; the kernel re-verifies every product):
p 1 = 2^2 · 3 · 65147 · q1, p = 2^255 19
q1 = 740582127325613583022312264370627\
88676166966415465897661863160754340907 (236 bits)
q1 1 = 2 · 3 · 353 · 57467 · 132049 · 1923133 · q2 · q3
q2 = 31757755568855353
q2 1 = 2^3 · 3 · 31 · 107 · 223 · 4153 · 430751 (all small)
q3 = 75445702479781427272750846543864801
q3 1 = 2^5 · 3^2 · 5^2 · 75707 · q4 · q5
q4 = 72106336199
q4 1 = 2 · 13 · q6
q6 = 2773320623, q6 1 = 2 · 2437 · 569003 (all small)
q5 = 1919519569386763
q5 1 = 2 · 3 · 7 · 19 · 47^2 · 127 · q7
q7 = 8574133, q7 1 = 2^2 · 3 · 7 · 103 · 991 (all small)
The certificate theorems below appear leaves-first:
q7, q6, q4, q5, q2, q3, q1, and finally p itself.
WHY `powMod` EXISTS
Checking condition (1) for p means verifying a congruence with a 255-bit
exponent. `decide` on `(2 : ZMod n) ^ (n1) = 1` directly is hopeless: `^`
on `ZMod n` unfolds to n1 ≈ 2^255 repeated multiplications. Instead we
define square-and-multiply on raw `Nat` (`powModAux`), prove ONCE that it
computes `a ^ k % n` (`powModAux_eq`), and then every certificate condition
becomes a closed equation `powMod a k n = 1` (or `≠ 1`) between `Nat`
literals. Lean's kernel evaluates `Nat` literal arithmetic (·, %, /) with
GMP big-integer primitives, so each such `decide` costs ~256 squarings of
≤255-bit numbers — milliseconds, entirely inside the trusted kernel.
-/
import Mathlib.NumberTheory.LucasPrimality
import Mathlib.Data.ZMod.Basic
import Mathlib.Tactic.NormNum.Prime
-- Elaborating `decide` on the huge decimal literals below builds deep numeral
-- terms; raise the elaborator's recursion limit so they go through.
set_option maxRecDepth 8000
-- All helpers and per-node certificates live in their own namespace; only the
-- final `p25519_prime` (stated about `2 ^ 255 - 19` itself) is exported at top
-- level for Proofs/Field.lean.
namespace P25519
-- ─────────────────────────────────────────────────────────────────────────────
-- Kernel-checkable modular exponentiation
-- ─────────────────────────────────────────────────────────────────────────────
/-- Fuel-based binary modular exponentiation, kernel-reducible (GMP-fast `decide`).
MATH (for sufficient fuel; made precise by `powModAux_eq`):
`powModAux fuel a k n = a^k mod n`.
Algorithm: square-and-multiply, consuming the binary digits of `k` from the
low end —
k = 0 ↦ 1 mod n
k = 2m ↦ (a² mod n)^m mod n
k = 2m+1 ↦ ((a² mod n)^m mod n) · a mod n
Every intermediate is reduced mod n, so no value ever exceeds n² (≈510 bits
here) — this is what keeps kernel evaluation fast.
WHY THE `fuel` ARGUMENT: recursion is on `fuel` (plain structural recursion),
not on `k`. Recursing on `k/2 < k` would be well-founded recursion, which
Lean compiles to `WellFounded.fix` — a fixpoint the kernel cannot unfold
during `decide`. With fuel, the kernel just peels one constructor per step.
WHY NEEDED: this is the workhorse that lets the kernel verify 255-bit
Fermat-witness congruences in milliseconds (see file header). -/
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
LaTeX: $\forall\,\mathit{fuel}\,a\,k\,n,\ k < 2^{\mathit{fuel}}
\Rightarrow \mathrm{powModAux}\ \mathit{fuel}\ a\ k\ n = a^k \bmod 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 = …` that `lucas_primality` needs.
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`.
The fuel is fixed at 256: enough for any exponent below 2^256, in particular
for every exponent `(n1)/q` appearing in the certificates (n ≤ p < 2^255).
WHY NEEDED: the single entry point all certificate side-conditions are stated
through, so each becomes one GMP-fast kernel `decide`. -/
def powMod (a k n : ) : := powModAux 256 a k n
/- Bridge from the `Nat` computation into `ZMod n`, where `lucas_primality`
lives.
MATH: k < 2^256 ==> (a : ZMod n)^k = (powMod a k n : ZMod n)
i.e. casting `a` to Z/n and exponentiating there agrees with computing
`a^k mod n` over the naturals and casting the result. Follows from
`powModAux_eq` plus the fact that the cast Nat → ZMod n is a ring
homomorphism that kills `% n`.
WHY NEEDED: the two lemmas below (`pow_eq_one_of_powMod`,
`pow_ne_one_of_powMod`) are corollaries of this 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]
/- Positive direction — discharges the FERMAT condition (1) of the Lucas test.
MATH: k < 2^256 and powMod a k n = 1 ==> (a : ZMod n)^k = 1.
The hypothesis `powMod a k n = 1` is a closed `Nat` equation the kernel
checks by `decide`; this lemma lifts it to the `ZMod n` equation that
`lucas_primality` consumes. -/
theorem pow_eq_one_of_powMod (a k n : ) (hk : k < 2 ^ 256) (h : powMod a k n = 1) :
(a : ZMod n) ^ k = 1 := by
rw [cast_pow_eq a k n hk, h, Nat.cast_one]
/- Negative direction — discharges the ORDER condition (2) of the Lucas test.
MATH: k < 2^256, 1 < n, powMod a k n ≠ 1, powMod a k n < n
==> (a : ZMod n)^k ≠ 1.
Subtlety: distinct naturals can become EQUAL in Z/n (they may differ by a
multiple of n), so `powMod a k n ≠ 1` alone is not enough. The extra
hypotheses pin both sides into the canonical range [0, n): the computed
residue is < n (true by construction, but cheaper to re-`decide` than to
prove generically) and 1 < n. Within that range the cast Nat → ZMod n is
injective (`ZMod.natCast_eq_natCast_iff'` + `Nat.mod_eq_of_lt`), so
inequality transfers.
WHY NEEDED: one application per prime factor q of n1, with
k = (n1)/q — this is what forces the witness to have full order n1. -/
theorem pow_ne_one_of_powMod (a k n : ) (hk : k < 2 ^ 256) (hn : 1 < n)
(h1 : powMod a k n ≠ 1) (h2 : powMod a k n < n) :
(a : ZMod n) ^ k ≠ 1 := by
-- replace the ZMod power by the cast of the computed Nat residue
rw [cast_pow_eq a k n hk]
intro hcon
-- equality of casts in ZMod n means equality of the residues mod n…
rw [show (1 : ZMod n) = ((1 : ) : ZMod n) by rw [Nat.cast_one],
ZMod.natCast_eq_natCast_iff'] at hcon
-- …and both residues are already < n, so they are equal as naturals
rw [Nat.mod_eq_of_lt h2, Nat.mod_eq_of_lt hn] at hcon
exact h1 hcon
-- ─────────────────────────────────────────────────────────────────────────────
-- The certificate chain, leaves first (factor tree in the file header).
--
-- Every theorem instantiates mathlib's
-- lucas_primality (n) (g : ZMod n) (h1) (h2) : Nat.Prime n
-- with a concrete witness g, discharging
-- h1 : g^(n1) = 1 in ZMod n via `pow_eq_one_of_powMod`
-- (its two `by decide`s check: n1 < 2^256, and the powMod equation)
-- h2 : ∀ q prime, q n1 → g^((n1)/q) ≠ 1 via `pow_ne_one_of_powMod`
-- (its four `by decide`s check: (n1)/q < 2^256, 1 < n,
-- powMod g ((n1)/q) n ≠ 1, and powMod … < n).
--
-- For h2 the published factorization of n1 is stated as a NESTED product
-- 2^e * (f1 * (f2 * (…))) and verified by one `decide` (a single big-number
-- multiplication). `rcases (Nat.Prime.dvd_mul hq).mp` then peels the factors
-- left to right: a prime q dividing the product divides the head factor or
-- the tail. Dividing the head pins q to a concrete prime via
-- `Nat.prime_dvd_prime_iff_eq` ("a prime divides a prime iff they are
-- equal"); for prime-power heads like 2^2 we first strip the exponent with
-- `hq.dvd_of_dvd_pow`. Head factors small enough are certified prime by
-- `norm_num`; large ones by the earlier theorems of this chain — that
-- reference IS the recursion of the Pratt certificate.
-- ─────────────────────────────────────────────────────────────────────────────
/- Leaf q7 of the factor tree: 8574133 is prime (needed for q5 below).
Witness g = 2; 8574133 1 = 2^2 · 3 · 7 · 103 · 991, all `norm_num`-small.
This first certificate is annotated line by line; the six that follow are
structurally identical. -/
theorem prime_8574133 : Nat.Prime 8574133 := by
-- pick the witness g = 2 and split into the two Lucas obligations
refine lucas_primality 8574133 ((2 : ) : ZMod 8574133) ?_ ?_
-- (1) Fermat: 2^(n1) ≡ 1 (mod n) — one kernel powMod computation
· exact pow_eq_one_of_powMod 2 (8574133 - 1) 8574133 (by decide) (by decide)
-- (2) full order: any prime q | n1 must leave 2^((n1)/q) ≢ 1 (mod n)
· intro q hq hqd
-- kernel-verified factorization of n1, nested for left-to-right peeling
have hfac : (8574133 : ) - 1 = 2 ^ 2 * (3 * (7 * (103 * (991)))) := by decide
rw [hfac] at hqd
-- q | 2^2 · rest: either q | 2^2 (then q = 2) or q divides the rest
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 2 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp (hq.dvd_of_dvd_pow h)
subst he
-- 2^((n1)/2) ≢ 1 (mod n), checked by the kernel
exact pow_ne_one_of_powMod 2 ((8574133 - 1) / 2) 8574133 (by decide) (by decide) (by decide) (by decide)
-- q | 3 · rest: peel the factor 3
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 3 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((8574133 - 1) / 3) 8574133 (by decide) (by decide) (by decide) (by decide)
-- peel the factor 7
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 7 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((8574133 - 1) / 7) 8574133 (by decide) (by decide) (by decide) (by decide)
-- peel the factor 103
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 103 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((8574133 - 1) / 103) 8574133 (by decide) (by decide) (by decide) (by decide)
-- only the last factor 991 remains
have he : q = 991 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp hqd
subst he
exact pow_ne_one_of_powMod 2 ((8574133 - 1) / 991) 8574133 (by decide) (by decide) (by decide) (by decide)
/- Leaf q6 of the factor tree: 2773320623 is prime (needed for q4 below).
Witness g = 5; 2773320623 1 = 2 · 2437 · 569003, all `norm_num`-small.
(g = 2 would fail here: 2 is a quadratic residue mod this prime, so
2^((n1)/2) ≡ 1 and the q = 2 order check breaks; hence the witness 5.) -/
theorem prime_2773320623 : Nat.Prime 2773320623 := by
refine lucas_primality 2773320623 ((5 : ) : ZMod 2773320623) ?_ ?_
-- Fermat condition, then one order check per prime factor of n1
· exact pow_eq_one_of_powMod 5 (2773320623 - 1) 2773320623 (by decide) (by decide)
· intro q hq hqd
have hfac : (2773320623 : ) - 1 = 2 * (2437 * (569003)) := by decide
rw [hfac] at hqd
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 2 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 5 ((2773320623 - 1) / 2) 2773320623 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 2437 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 5 ((2773320623 - 1) / 2437) 2773320623 (by decide) (by decide) (by decide) (by decide)
have he : q = 569003 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp hqd
subst he
exact pow_ne_one_of_powMod 5 ((2773320623 - 1) / 569003) 2773320623 (by decide) (by decide) (by decide) (by decide)
/- Node q4 of the factor tree: 72106336199 is prime (needed for q3 below).
Witness g = 7; 72106336199 1 = 2 · 13 · 2773320623.
First RECURSIVE step of the Pratt certificate: the large factor q6 is
certified by `prime_2773320623` above instead of `norm_num`. -/
theorem prime_72106336199 : Nat.Prime 72106336199 := by
refine lucas_primality 72106336199 ((7 : ) : ZMod 72106336199) ?_ ?_
· exact pow_eq_one_of_powMod 7 (72106336199 - 1) 72106336199 (by decide) (by decide)
· intro q hq hqd
have hfac : (72106336199 : ) - 1 = 2 * (13 * (2773320623)) := by decide
rw [hfac] at hqd
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 2 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 7 ((72106336199 - 1) / 2) 72106336199 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 13 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 7 ((72106336199 - 1) / 13) 72106336199 (by decide) (by decide) (by decide) (by decide)
-- last factor: q6 = 2773320623, prime by the recursive certificate above
have he : q = 2773320623 := (Nat.prime_dvd_prime_iff_eq hq prime_2773320623).mp hqd
subst he
exact pow_ne_one_of_powMod 7 ((72106336199 - 1) / 2773320623) 72106336199 (by decide) (by decide) (by decide) (by decide)
/- Node q5 of the factor tree: 1919519569386763 is prime (needed for q3 below).
Witness g = 2; q5 1 = 2 · 3 · 7 · 19 · 47^2 · 127 · 8574133.
Note the prime-power factor 47^2: only ONE order check is needed per
distinct prime (the test divides n1 by q once), so the branch for 47
strips the square with `hq.dvd_of_dvd_pow` first. The large factor
q7 = 8574133 is certified by `prime_8574133`. -/
theorem prime_1919519569386763 : Nat.Prime 1919519569386763 := by
refine lucas_primality 1919519569386763 ((2 : ) : ZMod 1919519569386763) ?_ ?_
· exact pow_eq_one_of_powMod 2 (1919519569386763 - 1) 1919519569386763 (by decide) (by decide)
· intro q hq hqd
have hfac : (1919519569386763 : ) - 1 = 2 * (3 * (7 * (19 * (47 ^ 2 * (127 * (8574133)))))) := by decide
rw [hfac] at hqd
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 2 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((1919519569386763 - 1) / 2) 1919519569386763 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 3 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((1919519569386763 - 1) / 3) 1919519569386763 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 7 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((1919519569386763 - 1) / 7) 1919519569386763 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 19 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((1919519569386763 - 1) / 19) 1919519569386763 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 47 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp (hq.dvd_of_dvd_pow h)
subst he
exact pow_ne_one_of_powMod 2 ((1919519569386763 - 1) / 47) 1919519569386763 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 127 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((1919519569386763 - 1) / 127) 1919519569386763 (by decide) (by decide) (by decide) (by decide)
-- last factor: q7 = 8574133, prime by the recursive certificate above
have he : q = 8574133 := (Nat.prime_dvd_prime_iff_eq hq prime_8574133).mp hqd
subst he
exact pow_ne_one_of_powMod 2 ((1919519569386763 - 1) / 8574133) 1919519569386763 (by decide) (by decide) (by decide) (by decide)
/- Leaf q2 of the factor tree: 31757755568855353 is prime (needed for q1).
Witness g = 10; q2 1 = 2^3 · 3 · 31 · 107 · 223 · 4153 · 430751,
all `norm_num`-small — no recursion needed for this node. -/
theorem prime_31757755568855353 : Nat.Prime 31757755568855353 := by
refine lucas_primality 31757755568855353 ((10 : ) : ZMod 31757755568855353) ?_ ?_
· exact pow_eq_one_of_powMod 10 (31757755568855353 - 1) 31757755568855353 (by decide) (by decide)
· intro q hq hqd
have hfac : (31757755568855353 : ) - 1 = 2 ^ 3 * (3 * (31 * (107 * (223 * (4153 * (430751)))))) := by decide
rw [hfac] at hqd
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 2 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp (hq.dvd_of_dvd_pow h)
subst he
exact pow_ne_one_of_powMod 10 ((31757755568855353 - 1) / 2) 31757755568855353 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 3 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 10 ((31757755568855353 - 1) / 3) 31757755568855353 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 31 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 10 ((31757755568855353 - 1) / 31) 31757755568855353 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 107 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 10 ((31757755568855353 - 1) / 107) 31757755568855353 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 223 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 10 ((31757755568855353 - 1) / 223) 31757755568855353 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 4153 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 10 ((31757755568855353 - 1) / 4153) 31757755568855353 (by decide) (by decide) (by decide) (by decide)
have he : q = 430751 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp hqd
subst he
exact pow_ne_one_of_powMod 10 ((31757755568855353 - 1) / 430751) 31757755568855353 (by decide) (by decide) (by decide) (by decide)
/- Node q3 of the factor tree: the 116-bit 75445702479781427272750846543864801
is prime (needed for q1). Witness g = 7;
q3 1 = 2^5 · 3^2 · 5^2 · 75707 · q4 · q5 with the two large factors
q4 = 72106336199 and q5 = 1919519569386763 certified recursively above. -/
theorem prime_75445702479781427272750846543864801 : Nat.Prime 75445702479781427272750846543864801 := by
refine lucas_primality 75445702479781427272750846543864801 ((7 : ) : ZMod 75445702479781427272750846543864801) ?_ ?_
· exact pow_eq_one_of_powMod 7 (75445702479781427272750846543864801 - 1) 75445702479781427272750846543864801 (by decide) (by decide)
· intro q hq hqd
have hfac : (75445702479781427272750846543864801 : ) - 1 = 2 ^ 5 * (3 ^ 2 * (5 ^ 2 * (75707 * (72106336199 * (1919519569386763))))) := by decide
rw [hfac] at hqd
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 2 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp (hq.dvd_of_dvd_pow h)
subst he
exact pow_ne_one_of_powMod 7 ((75445702479781427272750846543864801 - 1) / 2) 75445702479781427272750846543864801 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 3 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp (hq.dvd_of_dvd_pow h)
subst he
exact pow_ne_one_of_powMod 7 ((75445702479781427272750846543864801 - 1) / 3) 75445702479781427272750846543864801 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 5 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp (hq.dvd_of_dvd_pow h)
subst he
exact pow_ne_one_of_powMod 7 ((75445702479781427272750846543864801 - 1) / 5) 75445702479781427272750846543864801 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 75707 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 7 ((75445702479781427272750846543864801 - 1) / 75707) 75445702479781427272750846543864801 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
-- factor q4 = 72106336199: prime by the recursive certificate above
· have he : q = 72106336199 := (Nat.prime_dvd_prime_iff_eq hq prime_72106336199).mp h
subst he
exact pow_ne_one_of_powMod 7 ((75445702479781427272750846543864801 - 1) / 72106336199) 75445702479781427272750846543864801 (by decide) (by decide) (by decide) (by decide)
-- last factor q5 = 1919519569386763: prime by the recursive certificate
have he : q = 1919519569386763 := (Nat.prime_dvd_prime_iff_eq hq prime_1919519569386763).mp hqd
subst he
exact pow_ne_one_of_powMod 7 ((75445702479781427272750846543864801 - 1) / 1919519569386763) 75445702479781427272750846543864801 (by decide) (by decide) (by decide) (by decide)
/- Node q1 of the factor tree: the 236-bit cofactor of p 1 is prime.
Witness g = 2;
q1 1 = 2 · 3 · 353 · 57467 · 132049 · 1923133 · q2 · q3,
with q2 = 31757755568855353 and q3 = 75445702479781427272750846543864801
certified recursively above. This is the last node below the root. -/
theorem prime_74058212732561358302231226437062788676166966415465897661863160754340907 : Nat.Prime 74058212732561358302231226437062788676166966415465897661863160754340907 := by
refine lucas_primality 74058212732561358302231226437062788676166966415465897661863160754340907 ((2 : ) : ZMod 74058212732561358302231226437062788676166966415465897661863160754340907) ?_ ?_
· exact pow_eq_one_of_powMod 2 (74058212732561358302231226437062788676166966415465897661863160754340907 - 1) 74058212732561358302231226437062788676166966415465897661863160754340907 (by decide) (by decide)
· intro q hq hqd
have hfac : (74058212732561358302231226437062788676166966415465897661863160754340907 : ) - 1 = 2 * (3 * (353 * (57467 * (132049 * (1923133 * (31757755568855353 * (75445702479781427272750846543864801))))))) := by decide
rw [hfac] at hqd
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 2 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((74058212732561358302231226437062788676166966415465897661863160754340907 - 1) / 2) 74058212732561358302231226437062788676166966415465897661863160754340907 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 3 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((74058212732561358302231226437062788676166966415465897661863160754340907 - 1) / 3) 74058212732561358302231226437062788676166966415465897661863160754340907 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 353 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((74058212732561358302231226437062788676166966415465897661863160754340907 - 1) / 353) 74058212732561358302231226437062788676166966415465897661863160754340907 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 57467 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((74058212732561358302231226437062788676166966415465897661863160754340907 - 1) / 57467) 74058212732561358302231226437062788676166966415465897661863160754340907 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 132049 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((74058212732561358302231226437062788676166966415465897661863160754340907 - 1) / 132049) 74058212732561358302231226437062788676166966415465897661863160754340907 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 1923133 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((74058212732561358302231226437062788676166966415465897661863160754340907 - 1) / 1923133) 74058212732561358302231226437062788676166966415465897661863160754340907 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
-- factor q2: prime by the recursive certificate above
· have he : q = 31757755568855353 := (Nat.prime_dvd_prime_iff_eq hq prime_31757755568855353).mp h
subst he
exact pow_ne_one_of_powMod 2 ((74058212732561358302231226437062788676166966415465897661863160754340907 - 1) / 31757755568855353) 74058212732561358302231226437062788676166966415465897661863160754340907 (by decide) (by decide) (by decide) (by decide)
-- last factor q3: prime by the recursive certificate above
have he : q = 75445702479781427272750846543864801 := (Nat.prime_dvd_prime_iff_eq hq prime_75445702479781427272750846543864801).mp hqd
subst he
exact pow_ne_one_of_powMod 2 ((74058212732561358302231226437062788676166966415465897661863160754340907 - 1) / 75445702479781427272750846543864801) 74058212732561358302231226437062788676166966415465897661863160754340907 (by decide) (by decide) (by decide) (by decide)
/- ROOT of the factor tree: p = 2^255 19 itself, written out in decimal
(57896044618658097711785492504343953926634992332820282019728792003956564819949).
Witness g = 2 (2 is in fact a primitive root mod p);
p 1 = 2^2 · 3 · 65147 · q1, with the 236-bit q1 certified just above.
Each `powMod` check here exponentiates with a ~255-bit exponent modulo the
255-bit p — still milliseconds thanks to GMP-backed kernel `Nat` arithmetic. -/
theorem prime_57896044618658097711785492504343953926634992332820282019728792003956564819949 : Nat.Prime 57896044618658097711785492504343953926634992332820282019728792003956564819949 := by
refine lucas_primality 57896044618658097711785492504343953926634992332820282019728792003956564819949 ((2 : ) : ZMod 57896044618658097711785492504343953926634992332820282019728792003956564819949) ?_ ?_
· exact pow_eq_one_of_powMod 2 (57896044618658097711785492504343953926634992332820282019728792003956564819949 - 1) 57896044618658097711785492504343953926634992332820282019728792003956564819949 (by decide) (by decide)
· intro q hq hqd
have hfac : (57896044618658097711785492504343953926634992332820282019728792003956564819949 : ) - 1 = 2 ^ 2 * (3 * (65147 * (74058212732561358302231226437062788676166966415465897661863160754340907))) := by decide
rw [hfac] at hqd
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 2 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp (hq.dvd_of_dvd_pow h)
subst he
exact pow_ne_one_of_powMod 2 ((57896044618658097711785492504343953926634992332820282019728792003956564819949 - 1) / 2) 57896044618658097711785492504343953926634992332820282019728792003956564819949 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 3 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((57896044618658097711785492504343953926634992332820282019728792003956564819949 - 1) / 3) 57896044618658097711785492504343953926634992332820282019728792003956564819949 (by decide) (by decide) (by decide) (by decide)
rcases (Nat.Prime.dvd_mul hq).mp hqd with h | hqd
· have he : q = 65147 := (Nat.prime_dvd_prime_iff_eq hq (by norm_num)).mp h
subst he
exact pow_ne_one_of_powMod 2 ((57896044618658097711785492504343953926634992332820282019728792003956564819949 - 1) / 65147) 57896044618658097711785492504343953926634992332820282019728792003956564819949 (by decide) (by decide) (by decide) (by decide)
-- last factor q1: prime by the recursive certificate above
have he : q = 74058212732561358302231226437062788676166966415465897661863160754340907 := (Nat.prime_dvd_prime_iff_eq hq prime_74058212732561358302231226437062788676166966415465897661863160754340907).mp hqd
subst he
exact pow_ne_one_of_powMod 2 ((57896044618658097711785492504343953926634992332820282019728792003956564819949 - 1) / 74058212732561358302231226437062788676166966415465897661863160754340907) 57896044618658097711785492504343953926634992332820282019728792003956564819949 (by decide) (by decide) (by decide) (by decide)
end P25519
-- ─────────────────────────────────────────────────────────────────────────────
-- Exported result
-- ─────────────────────────────────────────────────────────────────────────────
/-- The Curve25519 field prime `2 ^ 255 - 19` is prime.
MATH (ASCII): Nat.Prime (2^255 - 19)
LaTeX: $2^{255} - 19$ is prime.
This is the only theorem of this file used downstream: Proofs/Field.lean turns
it into `P_prime : Nat.Prime P` (where `P` abbreviates the same number) and the
`Fact (Nat.Prime P)` instance, which activates mathlib's `Field (ZMod P)` —
the target structure of the main theorem `fieldImplementation`
(Proofs/FieldMain.lean) — and feeds Fermat's little theorem to the inverse
spec (Proofs/InvertSpec.lean), mirroring the comment on
`FieldElement::invert` in curve25519/solana-ed25519/src/field.rs:239-248. -/
theorem p25519_prime : Nat.Prime (2 ^ 255 - 19) := by
-- rewrite 2^255 19 into the decimal literal the root certificate is about
have h : (2 : ) ^ 255 - 19 = 57896044618658097711785492504343953926634992332820282019728792003956564819949 := by decide
rw [h]
exact P25519.prime_57896044618658097711785492504343953926634992332820282019728792003956564819949

View file

@ -0,0 +1,169 @@
/- ──────────────────────────────────────────────────────────────────────────────
Proofs/ReduceSpec.lean — total-correctness spec of the "weak reduction"
WHAT THIS FILE CONTAINS
Spec for the transpiled `FieldElement51::reduce`:
it never panics, its output limbs are < 2⁵¹ + 19·2¹³ (⊂ 2⁵²), and it
preserves the value modulo p (exact Nat relation: it subtracts (l4 ≫ 51)·p).
It also proves two tiny -level rewrite lemmas (`nat_and_mask`, `nat_shift_div`)
that turn the hardware bit operations `&&&`/`>>>` into `%`/`/`, the form the
`omega` decision procedure understands.
RUST ANALOG
`FieldElement51::reduce`, curve25519/solana-ed25519/src/backend/serial/u64/field.rs:290-323
(its local constant `LOW_51_BIT_MASK`, field.rs:291). The transpiled bodies live in
gen/CurveField/Funs.lean as `backend.serial.u64.field.FieldElement51.reduce` and
`...reduce.LOW_51_BIT_MASK`.
THE ALGORITHM (what the Rust does)
A field element is 5 limbs l0..l4 in radix 2⁵¹:
feVal l = l0 + l1·2⁵¹ + l2·2¹⁰² + l3·2¹⁵³ + l4·2²⁰⁴.
`reduce` performs one parallel carry pass ("weak reduction" — it shrinks limbs back
under ~2⁵¹ but does NOT canonicalize below p):
c_i := l_i >> 51 (the carry-out of limb i; c_i < 2¹³ since l_i < 2⁶⁴)
m_i := l_i & (2⁵¹ 1) (the low 51 bits, i.e. l_i mod 2⁵¹)
r0 := m0 + 19·c4, r_{i+1} := m_{i+1} + c_i (i = 0..3).
Each carry c_i moves from weight 2^(51·i)·2⁵¹ = 2^(51·(i+1)) to limb i+1 — except c4,
whose weight 2²⁵⁵ does not exist in the representation; since 2²⁵⁵ ≡ 19 (mod p) it is
folded back into limb 0 as 19·c4. The value therefore drops by exactly
c4·(2²⁵⁵ 19) = p·(l4 div 2⁵¹), giving the EXACT accounting proved below:
feVal r + p·(l4 div 2⁵¹) = feVal l.
No u64 addition can overflow: m_i < 2⁵¹ and 19·c4 < 19·2¹³, so every output limb is
< 2⁵¹ + 19·2¹³ < 2⁵² ≪ 2⁶⁴ — this is simultaneously the panic-freedom argument and
the restored limb-bound invariant `Bnd r (2⁵¹ + 19·2¹³)`.
ROLE IN THE MAIN THEOREM (Proofs/FieldMain.lean: fieldImplementation)
`sub` and `negate` (Proofs/SubNegSpec.lean) end with a call to `reduce`; their specs
compose `reduce_spec` (via the packaged `reduce_make_spec`) with their own limb
arithmetic. The exact (not merely mod-p) value equation is what lets those callers
finish their accounting purely over with `omega` and cast to 𝔽_p only once.
The `nat_and_mask`/`nat_shift_div` simp lemmas are reused by every carry-chain proof
(MulSpec, SquareSpec) since `mul`/`pow2k` inline the same mask/shift idiom.
FILE RELATIONS
Imports Proofs/Denote.lean (Fe, feVal, limbsVal, Bnd, P). Imported by
Proofs/SubNegSpec.lean and Proofs/AddSpec.lean, hence (transitively) by everything
up to Proofs/FieldMain.lean.
────────────────────────────────────────────────────────────────────────────── -/
import Proofs.Denote
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 4000000
namespace CurveFieldProofs
/-- The mask constant evaluates to 2⁵¹ 1.
Rust: `const LOW_51_BIT_MASK: u64 = (1u64 << 51) - 1;` inside `FieldElement51::reduce`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:291.
MATH: reduce.LOW_51_BIT_MASK = ok m with m = 2^51 - 1 (= 2251799813685247).
WHY NEEDED: in the Aeneas model even this constant is a *fallible* computation
(`1#u64 <<< 51` then `- 1#u64` — each could in principle overflow), so proving the
field theorem requires proving it succeeds and pinning its value. The `@[step]`
attribute registers it with the `step` tactic, so the symbolic execution of
`reduce`'s body picks it up automatically when it reaches the mask read. -/
@[step]
theorem reduce_mask_spec :
backend.serial.u64.field.FieldElement51.reduce.LOW_51_BIT_MASK
⦃ m => m.val = 2^51 - 1 ⦄ := by
unfold backend.serial.u64.field.FieldElement51.reduce.LOW_51_BIT_MASK
-- symbolically execute the two machine ops (shift, subtract); no side conditions remain
step*
/-- Convert `&&& (2^51-1)` and `>>> 51` on `` to `%`/`/` so `omega` can reason.
Stated with the *literal* (2251799813685247 = 2⁵¹1) since simp normalizes
`2^51 - 1` to it before these lemmas get a chance to fire.
Rust analog: the expression `x & LOW_51_BIT_MASK` (field.rs:310-314 and the same
idiom in `mul`/`square`); at the `.val : ` level the U64 bitwise-and becomes 's
`&&&` (`Nat.land`).
MATH: n AND (2^51 - 1) = n mod 2^51 — masking the low 51 bits IS reduction
mod 2^51 (standard two-power identity `Nat.and_two_pow_sub_one_eq_mod`).
WHY NEEDED: `omega` (the linear-arithmetic closer of every carry proof) knows
`/` and `%` but not bitwise ops. Tagging with `@[simp, scalar_tac_simps]` makes
both `simp_all` and `scalar_tac` eliminate `&&&` on sight, here and in
MulSpec/SquareSpec. -/
@[simp, scalar_tac_simps]
theorem nat_and_mask (n : ) : n &&& 2251799813685247 = n % 2251799813685248 := by
have := Nat.and_two_pow_sub_one_eq_mod n 51
norm_num at this
simpa using this
/-- Companion to `nat_and_mask` for the right shift.
Rust analog: the carry extraction `limbs[i] >> 51` (field.rs:304-308 and the same
idiom in `mul`/`square`).
MATH: n >> 51 = n div 2^51 (2251799813685248 = 2⁵¹; `Nat.shiftRight_eq_div_pow`).
WHY NEEDED: same as `nat_and_mask` — rewrites the hardware shift into the division
that `omega` can reason about linearly. -/
@[simp, scalar_tac_simps]
theorem nat_shift_div (n : ) : n >>> 51 = n / 2251799813685248 := by
simp [Nat.shiftRight_eq_div_pow]
/-- `reduce` spec: total (no panic), output bounded, value preserved mod p.
Rust: `FieldElement51::reduce`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:290-323.
MATH (note: NO precondition — `reduce` is total on arbitrary u64 limbs):
ASCII: forall l : Fe with limbs [l0..l4],
reduce l = ok r with Bnd(r, 2^51 + 19*2^13)
and feVal r + p * (l4 div 2^51) = feVal l.
LaTeX: $\forall l,\ \exists r,\ \mathrm{reduce}(l) = \mathrm{ok}\ r \wedge
\mathrm{Bnd}(r, 2^{51} + 19\cdot 2^{13}) \wedge
\llbracket r\rrbracket_{\mathbb N} + p\lfloor l_4/2^{51}\rfloor
= \llbracket l\rrbracket_{\mathbb N}$.
The value clause is EXACT arithmetic, not a congruence mod p: the only multiple
of p that `reduce` removes is c4·p where c4 = l4 div 2⁵¹ (the carry out of the top
limb, folded back as 19·c4 because 2²⁵⁵ ≡ 19 mod p — see the file header). Crucially
it is stated WITHOUT subtraction (`feVal r + P·c4 = feVal l`, not
`feVal r = feVal l P·c4`): -subtraction truncates at 0 and breaks linear
reasoning, whereas this purely additive form is a linear Diophantine equation in
the `%`/`/` terms of the input limbs — exactly the fragment `omega` decides.
The bound 2⁵¹ + 19·2¹³ is sharp for limb 0 (mask < 2⁵¹ plus 19·c4 with c4 < 2¹³);
limbs 14 satisfy the stronger < 2⁵¹ + 2¹³. Callers weaken it to `Bnd r (2^52)`
via `Bnd.mono`.
WHY NEEDED: `sub_spec`/`neg_spec` (SubNegSpec.lean) run `reduce` on their raw
"a + 16p b" limbs; this spec provides both their panic-freedom (one conjunct of
the main theorem's totality claim) and the value bookkeeping that turns into
⟪r⟫ = ⟪a⟫ ⟪b⟫ after the single cast to 𝔽_p. -/
theorem reduce_spec (l : Fe) (l0 l1 l2 l3 l4 : U64)
(hl : (↑l : List U64) = [l0, l1, l2, l3, l4]) :
fe_reduce l ⦃ r =>
Bnd r (2^51 + 19 * 2^13) ∧
feVal r + P * (l4.val / 2^51) = feVal l ⦄ := by
-- expose the transpiled monadic body (gen/CurveField/Funs.lean)
unfold fe_reduce backend.serial.u64.field.FieldElement51.reduce
-- step*: symbolically execute the whole program, one machine op per step (5 shifts,
-- 5 masks, the ×19, 5 carry adds, plus the interleaved array reads/writes — ~45 ops).
-- For each op it applies the registered @[step] spec (U64.add_spec, reduce_mask_spec,
-- Array.index_usize_spec, ...), names the result, and discharges every overflow /
-- index-in-bounds side condition with the supplied `by` block:
-- subst_vars — substitute the equations of earlier steps,
-- simp [...] — evaluate array get-after-set chains,
-- scalar_tac — close the remaining linear bound (e.g. mask + 19·carry < 2⁶⁴).
step* by (subst_vars
try simp [Array.set_val_eq, *]
try scalar_tac)
-- Final postcondition: normalize the set-chain + all value equations to
-- %/÷ arithmetic over the input limbs (simp_all also uses the inaccessible
-- step*-generated hypotheses), then close with omega.
-- After simp_all (which fires nat_and_mask/nat_shift_div) the goal is the pure
-- linear identity over :
-- Σᵢ (lᵢ % 2⁵¹)·2^(51i) + 19·(l4/2⁵¹) + Σᵢ₌₀..₃ (lᵢ/2⁵¹)·2^(51(i+1))
-- + (2²⁵⁵19)·(l4/2⁵¹) = Σᵢ lᵢ·2^(51i)
-- which follows from lᵢ = (lᵢ % 2⁵¹) + 2⁵¹·(lᵢ/2⁵¹); scalar_tac ends in omega.
simp_all [Array.set_val_eq, P, limbsVal, Bnd, feVal]
scalar_tac
end CurveFieldProofs

View file

@ -0,0 +1,288 @@
/- ─────────────────────────────────────────────────────────────────────────────
Proofs/Square2Spec.lean — total correctness of `square2` (compute 2·a²)
WHAT THIS FILE PROVES
`square2_spec` (and its step-friendly wrapper `square2_spec'`):
ASCII: Bnd(a, 2^54) ==> square2 a = ok r with
Bnd(r, 2^53) and [[r]] = 2 * ([[a]] * [[a]])
LaTeX: $\mathrm{Bnd}(a,2^{54}) \Rightarrow
\llbracket \mathrm{square2}(a)\rrbracket
= 2\,\llbracket a\rrbracket^2$
where Bnd/[[·]] = ⟪·⟫ are the invariant and denotation of Proofs/Denote.lean
and p = 2^255 - 19. As everywhere, `f x ⦃ post ⦄` is TOTAL correctness:
no panic/overflow — in particular the five u64 `*= 2` multiplications of the
doubling loop are PROVED in range, not assumed.
RUST ANALOG
`FieldElement51::square2`, curve25519/solana-ed25519/src/backend/serial/u64/
field.rs:566-573:
let mut square = self.pow2k(1);
for i in 0..5 { square.0[i] *= 2; }
square
i.e. one radix-2^51 squaring (pow2k with k = 1, verified in
Proofs/SquareSpec.lean) followed by a limbwise doubling — point doubling
(`ProjectivePoint::double`, curve_models.rs:381-397) calls it for 2·Z².
Charon splits the Rust `for` loop into two generated items in
gen/CurveField/Funs.lean (same shape as `add_assign`'s loop in
Proofs/AddSpec.lean):
* `…FieldElement51.square2_loop.body (iter, square)` — ONE iteration,
returning a `ControlFlow` value: it calls `Iterator::next` on the
`Range<usize>` iterator and either answers `.done square` (range
exhausted) or doubles limb i (`Array.index_usize`, `* 2#u64`,
`Array.update`) and answers `.cont (iter1, a)`;
* `…FieldElement51.square2_loop` — `Aeneas.Std.loop` applied to that body;
and `…FieldElement51.square2` itself is `pow2k self 1#u32` bound into the
loop started at the literal range { start := 0, end := 5 }.
(No `fe_square2` alias exists in Proofs/Denote.lean; we use the full
generated name throughout — Denote.lean is not modified.)
WHY THE DOUBLING LOOP CANNOT OVERFLOW
`pow2k` outputs limbs < 2^51 + 2^13 (pow2k_spec, Proofs/SquareSpec.lean), so
each u64 product limb·2 is < 2^52 + 2^14 < 2^64: the `*= 2` (a genuine U64
multiplication in the generated code, `i1 * 2#u64`) is always in range.
The output limbs are < 2^52 + 2^14 ≤ 2^53, which is the (comfortable) bound
we expose — still strictly below the 2^54 input invariant of mul/sub/square,
so square2's result can feed any downstream field op directly.
PROOF ARCHITECTURE
Exactly the AddSpec playbook for the one other `for i in 0..5` loop in this
crate, specialized to one array instead of two:
* `square2_loop_spec` unrolls the loop 5-fold by hand: 5 × (apply
`loop_step` (Proofs/AddSpec.lean), substitute the generated body, run
`range_next_lt_spec` + index/mul/update steps — the mul's side condition
is closed from the < 2^63 limb hypothesis) and a 6th `loop_step` where
`range_next_ge_spec` (5 ≥ 5) makes the body return `done`. The result
array is the input overwritten at 0..4 with the doubled limbs;
collapsing the five set operations (`Array.set_val_eq`) gives the
limb-exact postcondition r_i = 2·s_i.
* `square2_spec` chains pow2k_spec (k = 1, reused as-is via `spec_bind`)
into the loop spec (`spec_mono`), then repackages:
- bound: r_i = 2·s_i < 2·(2^51 + 2^13) ≤ 2^53 (omega);
- value: feVal r = 2·feVal s EXACTLY over (the doubling is linear
in the limbs — omega), cast into 𝔽_p, then
⟪r⟫ = 2·⟪s⟫ = 2·⟪a⟫^(2^1) = 2·(⟪a⟫·⟪a⟫) — the pow2k exponent 2^1 is
bridged to the product form by `ring`, as in square_spec.
* `square2_spec'` is the `@[step]`-registered wrapper with the limbs
hidden (destructured internally via `Fe.exists_limbs`), so the `step` /
`let*` machinery of downstream proofs (e.g. point doubling) can consume
square2 in one step.
ROLE IN THE MAIN THEOREM
Not a field axiom itself: square2 is an EdDSA-level optimization
(2·Z² in `ProjectivePoint::double` saves one full mul). Verifying it here,
with the same invariant discipline as the eleven core ops, makes the
point-doubling code symbolically executable later.
Imports: Proofs/SquareSpec (pow2k_spec; transitively MulSpec's architecture
and AddSpec's loop_step / range_next_*_spec machinery).
───────────────────────────────────────────────────────────────────────── -/
import Proofs.SquareSpec
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 4000000
set_option maxRecDepth 8000
set_option linter.unusedSimpArgs false
namespace CurveFieldProofs
-- the weakest-precondition layer: spec_mono / spec_bind / spec_ok used below
open Aeneas.Std.WP
/-- Limb-level spec for the doubling loop of `square2`: total (no u64
overflow) when all input limbs are < 2⁶³, and the output limbs are
exactly the doubled input limbs.
Rust: the `for i in 0..5 { square.0[i] *= 2; }` loop of
`FieldElement51::square2`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:568-570
(generated as `…FieldElement51.square2_loop` = `Aeneas.Std.loop` applied
to `…square2_loop.body`, started at the range { start := 0, end := 5 }).
MATH:
ASCII: forall s : Fe, (s_i < 2^63 for i = 0..4) ==>
square2_loop {0, 5} s = ok r with r_i = 2 * s_i for all i.
LaTeX: $\forall i,\ s_i < 2^{63} \Rightarrow \exists r,\
\mathrm{loop}(s) = \mathrm{ok}\ r \wedge \forall i,\ r_i = 2 s_i$.
The hypothesis is exactly the panic condition of the Rust `*= 2` on u64
(s_i·2 < 2^64 iff s_i < 2^63); the caller instantiates it with the much
stronger pow2k output bound 2^51 + 2^13.
WHY NEEDED: the strongest (limb-exact) description of the loop, consumed
by `square2_spec` below; as in AddSpec, keeping the 6-fold `loop_step`
unrolling separate from the feVal/Bnd repackaging keeps both readable. -/
theorem square2_loop_spec (s : Fe) (s0 s1 s2 s3 s4 : U64)
(hs : (↑s : List U64) = [s0, s1, s2, s3, s4])
(hbnd : s0.val < 2^63 ∧ s1.val < 2^63 ∧ s2.val < 2^63 ∧
s3.val < 2^63 ∧ s4.val < 2^63) :
backend.serial.u64.field.FieldElement51.square2_loop
{ start := 0#usize, «end» := 5#usize } s
⦃ r => ∃ r0 r1 r2 r3 r4 : U64,
(↑r : List U64) = [r0, r1, r2, r3, r4] ∧
r0.val = 2 * s0.val ∧ r1.val = 2 * s1.val ∧ r2.val = 2 * s2.val ∧
r3.val = 2 * s3.val ∧ r4.val = 2 * s4.val ⦄ := by
obtain ⟨hbnd0, hbnd1, hbnd2, hbnd3, hbnd4⟩ := hbnd
-- expose the loop combinator (gen/CurveField/Funs.lean)
unfold backend.serial.u64.field.FieldElement51.square2_loop
-- Iteration 1 (i = 0)
-- Pattern repeated for each of the 5 iterations (cf. add_limbs_spec):
-- loop_step — peel one iteration of the loop combinator,
-- simp only [..body] — substitute the loop body's definition,
-- step with range_next_lt_spec — Iterator::next yields some i, range
-- advances by one,
-- step — read square[i] (x_k),
-- step — u64 multiply by 2#u64; its overflow side
-- condition is closed by hbnd_i (x_k < 2^63),
-- step — write the doubled limb back (array update t_k),
-- spec_ok — the body returns `cont` with the updated state.
-- hv_k records v_k = s_k · 2; hd_k records the updated array for the next
-- round's get-after-set bookkeeping.
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o1, iter1, ho1, hs1, he1⟩
simp only [ho1]
step as ⟨x1, hx1⟩
simp [hs] at hx1
step as ⟨v0, hv0⟩
rw [hx1] at hv0
step as ⟨t1, hd1⟩
try simp only [spec_ok]
-- Iteration 2 (i = 1) — the simp at hx2 additionally rewrites through
-- iteration 1's array update (hd1 + Array.set_val_eq: get-after-set) so the
-- read still refers to the ORIGINAL limb s1.
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o2, iter2, ho2, hs2, he2⟩
simp only [ho2]
step as ⟨x2, hx2⟩
simp [hd1, Array.set_val_eq, hs, hs1, he1] at hx2
step as ⟨v1, hv1⟩
rw [hx2] at hv1
step as ⟨t2, hd2⟩
try simp only [spec_ok]
-- Iteration 3 (i = 2)
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o3, iter3, ho3, hs3, he3⟩
simp only [ho3]
step as ⟨x3, hx3⟩
simp [hd1, hd2, Array.set_val_eq, hs, hs1, he1, hs2, he2] at hx3
step as ⟨v2, hv2⟩
rw [hx3] at hv2
step as ⟨t3, hd3⟩
try simp only [spec_ok]
-- Iteration 4 (i = 3)
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o4, iter4, ho4, hs4, he4⟩
simp only [ho4]
step as ⟨x4, hx4⟩
simp [hd1, hd2, hd3, Array.set_val_eq, hs, hs1, he1, hs2, he2, hs3, he3] at hx4
step as ⟨v3, hv3⟩
rw [hx4] at hv3
step as ⟨t4, hd4⟩
try simp only [spec_ok]
-- Iteration 5 (i = 4)
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_lt_spec as ⟨o5, iter5, ho5, hs5, he5⟩
simp only [ho5]
step as ⟨x5, hx5⟩
simp [hd1, hd2, hd3, hd4, Array.set_val_eq, hs, hs1, he1, hs2, he2, hs3, he3,
hs4, he4] at hx5
step as ⟨v4, hv4⟩
rw [hx5] at hv4
step as ⟨t5, hd5⟩
try simp only [spec_ok]
-- Iteration 6 (range exhausted: 5 ≥ 5) — next returns none, body answers
-- `done` with the accumulated element
apply loop_step
simp only [backend.serial.u64.field.FieldElement51.square2_loop.body]
step with range_next_ge_spec as ⟨o6, iter6, ho6, hr6⟩
simp only [ho6]
try simp only [spec_ok]
-- Final: exhibit the limbs — the result array is s's array overwritten at
-- 0..4 with v0..v4; collapsing the five set operations (Array.set_val_eq)
-- gives [v0,...,v4]; each value equation v_k = 2·s_k is hv_k (scalar_tac
-- normalizes the (2#u64).val literal and the multiplication order).
refine ⟨v0, v1, v2, v3, v4, ?_, by scalar_tac, by scalar_tac, by scalar_tac,
by scalar_tac, by scalar_tac⟩
simp [hd1, hd2, hd3, hd4, hd5, Array.set_val_eq, hs, hs1, hs2, hs3, hs4]
/-- Main spec for `square2`: under the 2⁵⁴ invariant, no panic, output limbs
< 2⁵³, and the denotation is twice the square in 𝔽_p.
Rust: `FieldElement51::square2`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:566-573
(`pow2k(1)` followed by `for i in 0..5 { square.0[i] *= 2; }`).
MATH (ASCII): Bnd(a, 2^54) ==> square2 a = ok r with
Bnd(r, 2^53) and [[r]] = 2 * ([[a]] * [[a]]).
LaTeX: $\llbracket r\rrbracket = 2\,\llbracket a\rrbracket^2$.
PROOF: chain pow2k_spec at k = 1 (Proofs/SquareSpec.lean) — yielding s
with Bnd(s, 2^51 + 2^13) and ⟪s⟫ = ⟪a⟫^(2^1) — into `square2_loop_spec`
(2^51 + 2^13 < 2^63 keeps every `*= 2` in u64 range). The doubled limbs
give, EXACTLY over , feVal r = 2·feVal s (the radix-2^51 value is linear
in the limbs), hence ⟪r⟫ = 2·⟪s⟫ after the cast into 𝔽_p; the exponent
bridge ⟪a⟫^(2^1) = ⟪a⟫·⟪a⟫ is `ring`, exactly as in square_spec. Bounds:
r_i = 2·s_i < 2^52 + 2^14 ≤ 2^53 — comfortably re-usable, since every
downstream op only asks for < 2^54.
WHY NEEDED: 2·Z² in point doubling (`ProjectivePoint::double`,
curve_models.rs:381-397, generated right below square2 in Funs.lean). -/
theorem square2_spec (a : Fe) (x0 x1 x2 x3 x4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hba : Bnd a (2^54)) :
backend.serial.u64.field.FieldElement51.square2 a
⦃ r => Bnd r (2^53) ∧ ⟪r⟫ = 2 * (⟪a⟫ * ⟪a⟫) ⦄ := by
unfold backend.serial.u64.field.FieldElement51.square2
-- run pow2k at the literal 1#u32 (its debug_assert!(k > 0) is discharged
-- inside pow2k_spec); s carries Bnd(s, 2^51+2^13) and ⟪s⟫ = ⟪a⟫^(2^1)
apply spec_bind (pow2k_spec a 1#u32 x0 x1 x2 x3 x4 ha hba (by scalar_tac))
rintro s ⟨hbs, hvs⟩
-- name the limbs of the squared element and turn its invariant into the
-- five explicit inequalities s_i < 2^51 + 2^13
obtain ⟨s0, s1, s2, s3, s4, hsl⟩ := Fe.exists_limbs s
rw [Bnd_eq s s0 s1 s2 s3 s4 _ hsl] at hbs
-- run the doubling loop: 2^51 + 2^13 < 2^63, so no u64 overflow
apply spec_mono (square2_loop_spec s s0 s1 s2 s3 s4 hsl
⟨by omega, by omega, by omega, by omega, by omega⟩)
rintro r ⟨r0, r1, r2, r3, r4, hrl, h0, h1, h2, h3, h4⟩
-- value over : doubling every limb doubles the radix-2^51 value EXACTLY
have hval : feVal r = 2 * feVal s := by
rw [feVal_eq r r0 r1 r2 r3 r4 hrl, feVal_eq s s0 s1 s2 s3 s4 hsl]
simp only [limbsVal]
omega
refine ⟨?_, ?_⟩
-- bound: r_i = 2·s_i < 2·(2^51 + 2^13) = 2^52 + 2^14 ≤ 2^53
· rw [Bnd_eq r r0 r1 r2 r3 r4 _ hrl]
refine ⟨by omega, by omega, by omega, by omega, by omega⟩
-- denotation: cast the exact equation into 𝔽_p, then bridge ⟪a⟫^(2^1)
-- to ⟪a⟫·⟪a⟫ (cf. square_spec)
· have hr2 : ⟪r⟫ = 2 * ⟪s⟫ := by
simp only [denote]
rw [hval]
push_cast
ring
have h1v : (1#u32).val = 1 := by scalar_tac
rw [hr2, hvs, h1v]
ring
/-- Step-friendly wrapper for `square2_spec`: same statement with the limbs
hidden (no `ha` hypothesis), registered with `@[step]`.
MATH: identical to square2_spec —
Bnd(a, 2^54) ==> square2 a = ok r with Bnd(r, 2^53) and
[[r]] = 2 * ([[a]] * [[a]]).
PROOF: destructure the limbs internally via `Fe.exists_limbs` and apply
`square2_spec`.
WHY NEEDED: the `step`/`let*` machinery matches spec lemmas against the
goal syntactically; a lemma whose hypotheses mention existentially-found
limbs x0..x4 cannot be applied automatically, this one can. Downstream
proofs about point doubling consume square2 through this lemma in one
`let*` step. -/
@[step]
theorem square2_spec' (a : Fe) (hba : Bnd a (2^54)) :
backend.serial.u64.field.FieldElement51.square2 a
⦃ r => Bnd r (2^53) ∧ ⟪r⟫ = 2 * (⟪a⟫ * ⟪a⟫) ⦄ := by
obtain ⟨x0, x1, x2, x3, x4, ha⟩ := Fe.exists_limbs a
exact square2_spec a x0 x1 x2 x3 x4 ha hba
end CurveFieldProofs

View file

@ -0,0 +1,593 @@
/- ─────────────────────────────────────────────────────────────────────────────
Proofs/SquareSpec.lean — total correctness of SQUARING: `pow2k` and `square`
WHAT THIS FILE PROVES
`pow2k_spec` (k ≥ 1):
ASCII: Bnd(a, 2^54) ==> fe_pow2k a k = ok r with
Bnd(r, 2^51 + 2^13) and [[r]] = [[a]] ^ (2^k) in F_p
LaTeX: $\mathrm{Bnd}(a,2^{54}) \wedge k\ge 1 \Rightarrow
\llbracket \mathrm{pow2k}(a,k)\rrbracket
= \llbracket a\rrbracket^{2^k}$
`square_spec`:
ASCII: Bnd(a, 2^54) ==> fe_square a = ok r with
Bnd(r, 2^51 + 2^13) and [[r]] = [[a]] * [[a]]
where Bnd/[[·]] = ⟪·⟫ are the invariant and denotation of Proofs/Denote.lean
and p = 2^255 - 19. As everywhere, `f x ⦃ post ⦄` is TOTAL correctness:
no panic/overflow, including the Rust `debug_assert!`s, which Charon keeps
as `massert` obligations that we must PROVE.
RUST ANALOG
`FieldElement51::pow2k`, curve25519/solana-ed25519/src/backend/serial/u64/
field.rs:454-559 (helper `m`: 460-462; `LOW_51_BIT_MASK`: 511;
`debug_assert!(k > 0)`: 456; the five `debug_assert!(a[i] < 1 << 54)`:
505-509), and `FieldElement51::square` = `self.pow2k(1)`, field.rs:562-564.
Charon splits the Rust `loop { … }` (field.rs:466-556) into two generated
items in gen/CurveField/Funs.lean:
* `…FieldElement51.pow2k_loop.body (k, a)` — ONE iteration, returning a
`ControlFlow` value: `.done r` models `break` (the `if k == 0 { break }`
after `k -= 1`), `.cont (k1, r)` models falling through to the next
iteration with the decremented counter k1 and the squared limbs r;
* `…FieldElement51.pow2k_loop` — `Aeneas.Std.loop` applied to that body.
`fe_pow2k` / `fe_square` are the aliases from Proofs/Denote.lean.
THE ALGORITHM (one loop iteration = one radix-2^51 squaring, 19-folded)
Squaring specializes the schoolbook multiply of Proofs/MulSpec.lean: by the
symmetry x_i·x_j = x_j·x_i, cross products are computed once and doubled,
and the wrap-around 2^255 ≡ 19 (mod p) folds high columns down. With
a3_19 = 19·x3 and a4_19 = 19·x4 precomputed in u64 (field.rs:480-481), the
five u128 columns (field.rs:488-492) are
c0 = x0·x0 + 2·(x1·a4_19 + x2·a3_19) = x0² + 38·x1·x4 + 38·x2·x3
c1 = x3·a3_19 + 2·(x0·x1 + x2·a4_19) = 19·x3² + 2·x0·x1 + 38·x2·x4
c2 = x1·x1 + 2·(x0·x2 + x4·a3_19) = x1² + 2·x0·x2 + 38·x3·x4
c3 = x4·a4_19 + 2·(x0·x3 + x1·x2) = 19·x4² + 2·x0·x3 + 2·x1·x2
c4 = x2·x2 + 2·(x0·x4 + x1·x3) = x2² + 2·x0·x4 + 2·x1·x3
followed by exactly the same carry pass as `mul` (field.rs:515-548):
c_{k+1} += c_k >> 51, a[k] = c_k & mask, the final carry re-enters as
a[0] += 19·carry, one mini-carry into a[1], leaving all limbs < 2^51 + 2^13.
PROOF ARCHITECTURE
`pow2k_body_spec` mirrors Proofs/MulSpec.lean line by line: a fully NAMED
symbolic execution (`let* ⟨ x, x_post ⟩ ← spec_lemma by tac` consumes one
machine op, names its result and postcondition, and discharges its overflow
side condition), interleaved with explicit bound facts `hv_*` so every side
condition is LINEAR for `omega`/`scalar_tac`. The same three-layer final
assembly applies:
(1) hkey — exact carry accounting:
feVal r + p·carry = Σ_k c_k·2^(51 k);
(2) hnc0hnc4 — each column c_k as a polynomial in x0..x4 (`ring`);
(3) hAA — the F_p identity A·A = Σ_k c_k·2^(51 k) via
`linear_combination D * h255`, h255 : (2:F_p)^255 = 19, with
the squaring wrap-around polynomial
D = 2·x1·x4 + 2·x2·x3
+ 2^51 ·(2·x2·x4 + x3·x3)
+ 2^102·(2·x3·x4)
+ 2^153·(x4·x4)
= Sum_{i+j ≥ 5} x_i·x_j·2^(51(i+j-5)),
because over : A·A = Sum_k c_k·2^(51 k) + (2^255 - 19)·D.
On top of the body spec, the LOOP is handled by fuel induction
(`pow2k_loop_spec_aux`): `Aeneas.Std.loop` carries no termination measure,
so we induct on an external bound n ≥ k, peeling one iteration per step
with `loop_step` (Proofs/AddSpec.lean). Each iteration squares the value
and decrements k, so k iterations compute ((a²)²…)² = a^(2^k); the output
bound 2^51 + 2^13 ≤ 2^54 (Bnd.mono) re-establishes the input invariant for
the next round. Finally `square = pow2k(·, 1)` gives
⟪square a⟫ = ⟪a⟫^(2^1) = ⟪a⟫·⟪a⟫.
ROLE IN THE MAIN THEOREM (Proofs/FieldMain.lean)
`pow2k_spec`/`square_spec` drive Proofs/InvertSpec.lean: the pow22501
addition chain computing x^(p-2) is 14 pow2k/mul steps, each verified with
these lemmas; via Fermat this yields impl_mul_inv_cancel, one of the field
axioms of `fieldImplementation`.
Imports: Proofs/MulSpec (architecture + the `dis` macro), Proofs/AddSpec
(`loop_step`). Dependents: Proofs/InvertSpec.lean.
───────────────────────────────────────────────────────────────────────── -/
import Proofs.MulSpec
import Proofs.AddSpec
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 8000000
set_option maxRecDepth 8000
set_option linter.unusedSimpArgs false
namespace CurveFieldProofs
-- the weakest-precondition layer: spec_mono / spec_bind / spec_ok used below
open Aeneas.Std.WP
/-- The u128 widening product `pow2k.m(x, y) = (x as u128) * (y as u128)`.
Rust: nested `fn m(x: u64, y: u64) -> u128` inside `pow2k`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:460-462
(a separate copy of the identical helper in `mul`, so it gets its own
generated definition and its own spec lemma — cf. m_spec in MulSpec.lean).
MATH: pow2k.m x y = ok z with z.val = x.val * y.val; never overflows
since x·y < 2^64·2^64 = 2^128.
WHY NEEDED: all 13 partial products of the squaring go through it;
`@[step]` registers it with the `let*` machinery. -/
@[step]
theorem pow2k_m_spec (x y : U64) :
backend.serial.u64.field.FieldElement51.pow2k.m x y
⦃ z => z.val = x.val * y.val ⦄ := by
unfold backend.serial.u64.field.FieldElement51.pow2k.m
-- u64 inputs are < 2^64, so the u128 product cannot overflow
have hx : x.val < 2^64 := x.hBounds
have hy : y.val < 2^64 := y.hBounds
have hxy : x.val * y.val < 2^128 := by
calc x.val * y.val < 2^64 * 2^64 := Nat.mul_lt_mul'' hx hy
_ = 2^128 := by norm_num
-- run the 3 ops (cast, cast, mul); `dis` discharges each side condition
step* by dis
/-- The mask constant in `pow2k` evaluates to 2⁵¹ 1.
Rust: `const LOW_51_BIT_MASK: u64 = (1u64 << 51) - 1;`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:511.
MATH: the constant body returns 2251799813685247 = 2^51 - 1, so
`x & LOW_51_BIT_MASK = x mod 2^51` — the "keep the low limb" half of each
carry step. WHY NEEDED: exact value feeds the div/mod accounting (hkey). -/
@[step]
theorem pow2k_mask_spec :
backend.serial.u64.field.FieldElement51.pow2k.LOW_51_BIT_MASK
⦃ m => m.val = 2251799813685247 ⦄ := by
unfold backend.serial.u64.field.FieldElement51.pow2k.LOW_51_BIT_MASK
step*
/-- One iteration of the `pow2k` loop body: it squares the field element
(limbs < 2⁵¹ + 2¹³ afterwards) and decrements `k`, breaking iff `k = 1`.
Rust: the body of `loop { … }` in `FieldElement51::pow2k`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:466-556
(generated as `…pow2k_loop.body`, Charon span 480:16-492:86).
MATH (ASCII), for limbs [x0..x4] with Bnd(a,2^54) and k ≥ 1:
body (k, a) = ok cf where either
k = 1 and cf = done r (Rust `break` path), or
k ≥ 2 and cf = cont (k-1, r) (next iteration),
and in both cases Bnd(r, 2^51 + 2^13) and [[r]] = [[a]]*[[a]].
The `ControlFlow` disjunction is exactly the Rust
`k -= 1; if k == 0 { break; }` protocol made explicit.
WHY NEEDED: the induction step of `pow2k_loop_spec_aux`; its totality
(~80 machine ops + 5 massert) is one conjunct of pow2k's panic-freedom. -/
theorem pow2k_body_spec (k : U32) (a : Fe) (x0 x1 x2 x3 x4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hba : Bnd a (2^54)) (hk : 1 ≤ k.val) :
backend.serial.u64.field.FieldElement51.pow2k_loop.body k a ⦃ cf =>
(k.val = 1 ∧ ∃ r : Fe, cf = .done r ∧ Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ * ⟪a⟫)
(2 ≤ k.val ∧ ∃ (k1 : U32) (r : Fe), cf = .cont (k1, r) ∧ k1.val = k.val - 1 ∧
Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ * ⟪a⟫) ⦄ := by
-- turn the abstract invariant into 5 named limb bounds x_i < 2^54
rw [Bnd_eq a x0 x1 x2 x3 x4 _ ha] at hba
unfold backend.serial.u64.field.FieldElement51.pow2k_loop.body
-- ── limb loads + 19·a3, 19·a4 precomputations (field.rs:480-481) ─────────
-- each read is identified (he_*) and bounded (hv_*); 19·2^54 < 2^64
let* ⟨ i, i_post ⟩ ← Array.index_usize_spec by dis
have he_i : i = x3 := by simp [i_post, ha]
have hv_i : i.val < 2^54 := by rw [he_i]; omega
let* ⟨ a3_19, a3_19_post ⟩ ← U64.mul_spec by dis
have hv_a3_19 : a3_19.val < 19 * 2^54 := by rw [a3_19_post]; omega
let* ⟨ i1, i1_post ⟩ ← Array.index_usize_spec by dis
have he_i1 : i1 = x4 := by simp [i1_post, ha]
have hv_i1 : i1.val < 2^54 := by rw [he_i1]; omega
let* ⟨ a4_19, a4_19_post ⟩ ← U64.mul_spec by dis
have hv_a4_19 : a4_19.val < 19 * 2^54 := by rw [a4_19_post]; omega
let* ⟨ i2, i2_post ⟩ ← Array.index_usize_spec by dis
have he_i2 : i2 = x0 := by simp [i2_post, ha]
have hv_i2 : i2.val < 2^54 := by rw [he_i2]; omega
-- ── column c0 = a0·a0 + 2·(a1·(19·a4) + a2·(19·a3)) (field.rs:488) ───────
-- partial products < 2^108 (or < 19·2^108); the ×2 is a u128 multiply;
-- the running sums stay < 77·2^108 < 2^128, so every u128 op is in range
let* ⟨ i3, i3_post ⟩ ← pow2k_m_spec by dis
have hv_i3 : i3.val < 2^108 := by
rw [i3_post]; have := Nat.mul_lt_mul'' hv_i2 hv_i2; omega
let* ⟨ i4, i4_post ⟩ ← Array.index_usize_spec by dis
have he_i4 : i4 = x1 := by simp [i4_post, ha]
have hv_i4 : i4.val < 2^54 := by rw [he_i4]; omega
let* ⟨ i5, i5_post ⟩ ← pow2k_m_spec by dis
have hv_i5 : i5.val < 2^54 * (19 * 2^54) := by
rw [i5_post]; have := Nat.mul_lt_mul'' hv_i4 hv_a4_19; omega
let* ⟨ i6, i6_post ⟩ ← Array.index_usize_spec by dis
have he_i6 : i6 = x2 := by simp [i6_post, ha]
have hv_i6 : i6.val < 2^54 := by rw [he_i6]; omega
let* ⟨ i7, i7_post ⟩ ← pow2k_m_spec by dis
have hv_i7 : i7.val < 2^54 * (19 * 2^54) := by
rw [i7_post]; have := Nat.mul_lt_mul'' hv_i6 hv_a3_19; omega
let* ⟨ i8, i8_post ⟩ ← U128.add_spec by scalar_tac
have hv_i8 : i8.val < 38 * 2^108 := by rw [i8_post]; omega
let* ⟨ i9, i9_post ⟩ ← U128.mul_spec by scalar_tac
have hv_i9 : i9.val < 76 * 2^108 := by rw [i9_post]; omega
let* ⟨ c0, c0_post ⟩ ← U128.add_spec by scalar_tac
have hv_c0 : c0.val < 77 * 2^108 := by rw [c0_post]; omega
-- ── column c1 = a3·(19·a3) + 2·(a0·a1 + a2·(19·a4)) (field.rs:489) ───────
let* ⟨ i10, i10_post ⟩ ← pow2k_m_spec by dis
have hv_i10 : i10.val < 2^54 * (19 * 2^54) := by
rw [i10_post]; have := Nat.mul_lt_mul'' hv_i hv_a3_19; omega
let* ⟨ i11, i11_post ⟩ ← pow2k_m_spec by dis
have hv_i11 : i11.val < 2^108 := by
rw [i11_post]; have := Nat.mul_lt_mul'' hv_i2 hv_i4; omega
let* ⟨ i12, i12_post ⟩ ← pow2k_m_spec by dis
have hv_i12 : i12.val < 2^54 * (19 * 2^54) := by
rw [i12_post]; have := Nat.mul_lt_mul'' hv_i6 hv_a4_19; omega
let* ⟨ i13, i13_post ⟩ ← U128.add_spec by scalar_tac
have hv_i13 : i13.val < 20 * 2^108 := by rw [i13_post]; omega
let* ⟨ i14, i14_post ⟩ ← U128.mul_spec by scalar_tac
have hv_i14 : i14.val < 40 * 2^108 := by rw [i14_post]; omega
let* ⟨ c1, c1_post ⟩ ← U128.add_spec by scalar_tac
have hv_c1 : c1.val < 59 * 2^108 := by rw [c1_post]; omega
-- ── column c2 = a1·a1 + 2·(a0·a2 + a4·(19·a3)) (field.rs:490) ────────────
let* ⟨ i15, i15_post ⟩ ← pow2k_m_spec by dis
have hv_i15 : i15.val < 2^108 := by
rw [i15_post]; have := Nat.mul_lt_mul'' hv_i4 hv_i4; omega
let* ⟨ i16, i16_post ⟩ ← pow2k_m_spec by dis
have hv_i16 : i16.val < 2^108 := by
rw [i16_post]; have := Nat.mul_lt_mul'' hv_i2 hv_i6; omega
let* ⟨ i17, i17_post ⟩ ← pow2k_m_spec by dis
have hv_i17 : i17.val < 2^54 * (19 * 2^54) := by
rw [i17_post]; have := Nat.mul_lt_mul'' hv_i1 hv_a3_19; omega
let* ⟨ i18, i18_post ⟩ ← U128.add_spec by scalar_tac
have hv_i18 : i18.val < 20 * 2^108 := by rw [i18_post]; omega
let* ⟨ i19, i19_post ⟩ ← U128.mul_spec by scalar_tac
have hv_i19 : i19.val < 40 * 2^108 := by rw [i19_post]; omega
let* ⟨ c2, c2_post ⟩ ← U128.add_spec by scalar_tac
have hv_c2 : c2.val < 41 * 2^108 := by rw [c2_post]; omega
-- ── column c3 = a4·(19·a4) + 2·(a0·a3 + a1·a2) (field.rs:491) ────────────
let* ⟨ i20, i20_post ⟩ ← pow2k_m_spec by dis
have hv_i20 : i20.val < 2^54 * (19 * 2^54) := by
rw [i20_post]; have := Nat.mul_lt_mul'' hv_i1 hv_a4_19; omega
let* ⟨ i21, i21_post ⟩ ← pow2k_m_spec by dis
have hv_i21 : i21.val < 2^108 := by
rw [i21_post]; have := Nat.mul_lt_mul'' hv_i2 hv_i; omega
let* ⟨ i22, i22_post ⟩ ← pow2k_m_spec by dis
have hv_i22 : i22.val < 2^108 := by
rw [i22_post]; have := Nat.mul_lt_mul'' hv_i4 hv_i6; omega
let* ⟨ i23, i23_post ⟩ ← U128.add_spec by scalar_tac
have hv_i23 : i23.val < 2 * 2^108 := by rw [i23_post]; omega
let* ⟨ i24, i24_post ⟩ ← U128.mul_spec by scalar_tac
have hv_i24 : i24.val < 4 * 2^108 := by rw [i24_post]; omega
let* ⟨ c3, c3_post ⟩ ← U128.add_spec by scalar_tac
have hv_c3 : c3.val < 23 * 2^108 := by rw [c3_post]; omega
-- ── column c4 = a2·a2 + 2·(a0·a4 + a1·a3) (field.rs:492) ─────────────────
-- no 19-folding here: this is the 2^204 column, it never wraps past 2^255
let* ⟨ i25, i25_post ⟩ ← pow2k_m_spec by dis
have hv_i25 : i25.val < 2^108 := by
rw [i25_post]; have := Nat.mul_lt_mul'' hv_i6 hv_i6; omega
let* ⟨ i26, i26_post ⟩ ← pow2k_m_spec by dis
have hv_i26 : i26.val < 2^108 := by
rw [i26_post]; have := Nat.mul_lt_mul'' hv_i2 hv_i1; omega
let* ⟨ i27, i27_post ⟩ ← pow2k_m_spec by dis
have hv_i27 : i27.val < 2^108 := by
rw [i27_post]; have := Nat.mul_lt_mul'' hv_i4 hv_i; omega
let* ⟨ i28, i28_post ⟩ ← U128.add_spec by scalar_tac
have hv_i28 : i28.val < 2 * 2^108 := by rw [i28_post]; omega
let* ⟨ i29, i29_post ⟩ ← U128.mul_spec by scalar_tac
have hv_i29 : i29.val < 4 * 2^108 := by rw [i29_post]; omega
let* ⟨ c4, c4_post ⟩ ← U128.add_spec by scalar_tac
have hv_c4 : c4.val < 5 * 2^108 := by rw [c4_post]; omega
-- ── the five debug_assert!(a[i] < 2^54) (field.rs:505-509) ───────────────
-- kept by Charon as `massert`; massert_spec makes us PROVE each bound
-- (from hba via scalar_tac) — verified, not assumed. i30 = 1 << 54 = 2^54.
let* ⟨ i30, i30_post1, i30_post2 ⟩ ← U64.ShiftLeft_IScalar_spec by dis
have hv_i30 : i30.val = 2^54 := by
rw [i30_post1]; simp [Nat.shiftLeft_eq, U64.size, U64.numBits]
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
-- ── carry pass (field.rs:515-528). Per limb: c_{k+1} += (c_k >> 51) as u64
-- as u128; a[k] = (c_k as u64) & mask. The u128→u64→u128 cast round-trip
-- is lossless because c_k/2^51 < 77·2^57 < 2^64 (the hv_* div facts). ──
-- carry c0 -> c11; limb 0
let* ⟨ i31, i31_post1, i31_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ i32, i32_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i33, i33_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_i33 : i33.val = c0.val / 2^51 := by
simp [i33_post, i32_post, i31_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ c11, c11_post ⟩ ← U128.add_spec by scalar_tac
have hv_c11 : c11.val < 60 * 2^108 := by
rw [c11_post]; omega
let* ⟨ i34, i34_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i35, i35_post ⟩ ← pow2k_mask_spec by dis
let* ⟨ i36, i36_post1, i36_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i36 : i36.val = c0.val % 2^51 := by
simp [i36_post1, i34_post, i35_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ a1, a1_post ⟩ ← Array.update_spec by scalar_tac
-- carry c11 -> c21; limb 1
let* ⟨ i37, i37_post1, i37_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ i38, i38_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i39, i39_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_i39 : i39.val = c11.val / 2^51 := by
simp [i39_post, i38_post, i37_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ c21, c21_post ⟩ ← U128.add_spec by scalar_tac
have hv_c21 : c21.val < 42 * 2^108 := by
rw [c21_post]; omega
let* ⟨ i40, i40_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i41, i41_post1, i41_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i41 : i41.val = c11.val % 2^51 := by
simp [i41_post1, i40_post, i35_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ a2, a2_post ⟩ ← Array.update_spec by scalar_tac
-- carry c21 -> c31; limb 2
let* ⟨ i42, i42_post1, i42_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ i43, i43_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i44, i44_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_i44 : i44.val = c21.val / 2^51 := by
simp [i44_post, i43_post, i42_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ c31, c31_post ⟩ ← U128.add_spec by scalar_tac
have hv_c31 : c31.val < 24 * 2^108 := by
rw [c31_post]; omega
let* ⟨ i45, i45_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i46, i46_post1, i46_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i46 : i46.val = c21.val % 2^51 := by
simp [i46_post1, i45_post, i35_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ a3, a3_post ⟩ ← Array.update_spec by scalar_tac
-- carry c31 -> c41; limb 3
let* ⟨ i47, i47_post1, i47_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ i48, i48_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i49, i49_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_i49 : i49.val = c31.val / 2^51 := by
simp [i49_post, i48_post, i47_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ c41, c41_post ⟩ ← U128.add_spec by scalar_tac
have hv_c41 : c41.val < 6 * 2^108 := by
rw [c41_post]; omega
let* ⟨ i50, i50_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i51, i51_post1, i51_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i51 : i51.val = c31.val % 2^51 := by
simp [i51_post1, i50_post, i35_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ a4, a4_post ⟩ ← Array.update_spec by scalar_tac
-- last limb: carry out of c41 (field.rs:527-528); carry counts 2^255-units
let* ⟨ i52, i52_post1, i52_post2 ⟩ ← U128.ShiftRight_IScalar_spec by dis
let* ⟨ carry, carry_post ⟩ ← UScalar.cast.step_spec by scalar_tac
have hv_carry : carry.val = c41.val / 2^51 := by
simp [carry_post, i52_post1, UScalar.cast_val_eq, U64.size, U128.size]; omega
let* ⟨ i53, i53_post ⟩ ← UScalar.cast.step_spec by scalar_tac
let* ⟨ i54, i54_post1, i54_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i54 : i54.val = c41.val % 2^51 := by
simp [i54_post1, i53_post, i35_post, UScalar.cast_val_eq, U64.size, U128.size]
let* ⟨ a5, a5_post ⟩ ← Array.update_spec by scalar_tac
-- ── fold the final carry * 19 into limb 0 (field.rs:544, since 2^255 ≡ 19),
-- then the mini-carry into limb 1 (field.rs:547-548). No overflow:
-- carry < 6·2^57, 19·carry < 2^62, a[0] + 19·carry < 2^51 + 2^62 < 2^64 ──
let* ⟨ i55, i55_post ⟩ ← U64.mul_spec by scalar_tac
let* ⟨ i56, i56_post ⟩ ← Array.index_usize_spec by scalar_tac
have hv_i56 : i56.val = c0.val % 2^51 := by
simp [i56_post, a5_post, a4_post, a3_post, a2_post, a1_post,
Array.set_val_eq, hv_i36]
let* ⟨ i57, i57_post ⟩ ← U64.add_spec by scalar_tac
let* ⟨ a6, a6_post ⟩ ← Array.update_spec by scalar_tac
let* ⟨ i58, i58_post ⟩ ← Array.index_usize_spec by scalar_tac
have hv_i58 : i58.val = i57.val := by
simp [i58_post, a6_post, a5_post, a4_post, a3_post, a2_post, a1_post,
Array.set_val_eq]
let* ⟨ i59, i59_post1, i59_post2 ⟩ ← U64.ShiftRight_IScalar_spec by scalar_tac
let* ⟨ i60, i60_post ⟩ ← Array.index_usize_spec by scalar_tac
have hv_i60 : i60.val = c11.val % 2^51 := by
simp [i60_post, a6_post, a5_post, a4_post, a3_post, a2_post, a1_post,
Array.set_val_eq, hv_i41]
let* ⟨ i61, i61_post ⟩ ← U64.add_spec by scalar_tac
let* ⟨ a7, a7_post ⟩ ← Array.update_spec by scalar_tac
let* ⟨ i62, i62_post ⟩ ← Array.index_usize_spec by scalar_tac
have hv_i62 : i62.val = i57.val := by
simp [i62_post, a7_post, a6_post, a5_post, a4_post, a3_post, a2_post,
a1_post, Array.set_val_eq]
let* ⟨ i63, i63_post1, i63_post2 ⟩ ← UScalar.and_spec by scalar_tac
have hv_i63 : i63.val = i57.val % 2^51 := by
simp [i63_post1, hv_i62, i35_post, UScalar.cast_val_eq, U64.size, U128.size]
-- Rust `a[0] &= mask` is modeled as a mutable borrow: `index_mut` returns
-- the current slot value q AND a write-back function with back v = a7.set 0 v
let* ⟨ q, back, q_post, back_post ⟩ ← Array.index_mut_usize_spec by scalar_tac
-- ── symbolic execution done; assemble facts shared by both loop exits ────
-- the result limb list (a8 = a7.set 0 i63 in both branches):
-- r = [i57 mod 2^51, (c11 mod 2^51) + i57/2^51,
-- c21 mod 2^51, c31 mod 2^51, c41 mod 2^51]
have hv_i59 : i59.val = i57.val / 2^51 := by
simp [i59_post1, hv_i58]
have hout : (↑(a7.set 0#usize i63) : List U64) = [i63, i61, i46, i51, i54] := by
simp [a7_post, a6_post, a5_post, a4_post, a3_post, a2_post, a1_post,
Array.set_val_eq, ha]
-- output bound: four limbs are `mod 2^51` < 2^51; limb 1 adds the
-- mini-carry i57/2^51 < 2^13, so it stays < 2^51 + 2^13
have hbnd8 : Bnd (a7.set 0#usize i63) (2^51 + 2^13) :=
(Bnd_eq _ _ _ _ _ _ _ hout).mpr
⟨by omega, by omega, by omega, by omega, by omega⟩
-- ── layer (1): exact accounting of the carry pass ──────────────────────
-- feVal r + p·carry = Σ_k c_k·2^(51k): shifting out carry·2^255 and adding
-- back 19·carry changes the value by exactly (2^255-19)·carry = p·carry
have hkey : feVal (a7.set 0#usize i63) + P * carry.val
= c0.val + 2^51*c1.val + 2^102*c2.val + 2^153*c3.val + 2^204*c4.val := by
rw [feVal_eq _ _ _ _ _ _ hout]; simp only [limbsVal, P]; omega
-- ── layer (2): product expansions of the five columns ──────────────────
-- substitute all step posts, then `ring` rearranges to a polynomial in
-- x0..x4 (38 = 2·19 comes from doubling a 19-folded cross product)
have hnc0 : c0.val = x0.val*x0.val + 38*(x1.val*x4.val) + 38*(x2.val*x3.val) := by
simp only [c0_post, i9_post, i8_post, i3_post, i5_post, i7_post,
a3_19_post, a4_19_post, he_i, he_i1, he_i2, he_i4, he_i6]
ring
have hnc1 : c1.val = 2*(x0.val*x1.val) + 19*(x3.val*x3.val) + 38*(x2.val*x4.val) := by
simp only [c1_post, i14_post, i13_post, i10_post, i11_post, i12_post,
a3_19_post, a4_19_post, he_i, he_i1, he_i2, he_i4, he_i6]
ring
have hnc2 : c2.val = x1.val*x1.val + 2*(x0.val*x2.val) + 38*(x3.val*x4.val) := by
simp only [c2_post, i19_post, i18_post, i15_post, i16_post, i17_post,
a3_19_post, a4_19_post, he_i, he_i1, he_i2, he_i4, he_i6]
ring
have hnc3 : c3.val = 19*(x4.val*x4.val) + 2*(x0.val*x3.val) + 2*(x1.val*x2.val) := by
simp only [c3_post, i24_post, i23_post, i20_post, i21_post, i22_post,
a3_19_post, a4_19_post, he_i, he_i1, he_i2, he_i4, he_i6]
ring
have hnc4 : c4.val = x2.val*x2.val + 2*(x0.val*x4.val) + 2*(x1.val*x3.val) := by
simp only [c4_post, i29_post, i28_post, i25_post, i26_post, i27_post,
a3_19_post, a4_19_post, he_i, he_i1, he_i2, he_i4, he_i6]
ring
-- ── layer (3): 𝔽_p bridge: A·A = Σ cᵢ·2⁵¹ⁱ using 2²⁵⁵ = 19 ───────────────
have h255 : (2:Fp)^255 = 19 := by
have h := two_pow_255_eq; push_cast at h; simpa using h
-- over : A·A Σ c_k·2^(51k) = (2^255 19)·D with the squaring
-- wrap-around polynomial D = Σ_{i+j≥5} x_i·x_j·2^(51(i+j5)), i.e.
-- D = 2·x1·x4 + 2·x2·x3 + 2^51·(2·x2·x4 + x3²) + 2^102·(2·x3·x4) + 2^153·x4²
-- (spelled out literally below); `linear_combination D * h255` certifies it
have hAA : ((feVal a : ) : Fp) * ((feVal a : ) : Fp)
= ((c0.val : ) : Fp) + 2^51*(c1.val : ) + 2^102*(c2.val : )
+ 2^153*(c3.val : ) + 2^204*(c4.val : ) := by
rw [feVal_eq a x0 x1 x2 x3 x4 ha]
simp only [limbsVal, hnc0, hnc1, hnc2, hnc3, hnc4]
push_cast
linear_combination (2*(x1.val:Fp)*(x4.val:Fp) + 2*(x2.val:Fp)*(x3.val:Fp)
+ 2^51*(2*(x2.val:Fp)*(x4.val:Fp) + (x3.val:Fp)*(x3.val:Fp))
+ 2^102*(2*(x3.val:Fp)*(x4.val:Fp))
+ 2^153*((x4.val:Fp)*(x4.val:Fp))) * h255
-- ── conclude the denotation fact: cast (1) into F_p where (p : F_p) = 0
-- kills p·carry, then chain with (3): ⟪r⟫ = ⟪a⟫·⟪a⟫ ───────────────────
have hc := congrArg (Nat.cast : → Fp) hkey
push_cast at hc
have hp0 : ((P : ) : Fp) = 0 := ZMod.natCast_self P
rw [hp0] at hc
simp only [zero_mul, add_zero] at hc
have hfin : ⟪a7.set 0#usize i63⟫ = ⟪a⟫ * ⟪a⟫ := by
simp only [denote]
linear_combination hc - hAA
-- ── k decrement + branch on k1 = 0 (Rust: k -= 1; if k == 0 { break })
-- U32.sub_spec needs 1 ≤ k (no u32 underflow) — exactly the hk
-- hypothesis; with k = 0 release Rust would wrap here (see README) ─────
let* ⟨ k1, k1_post1, k1_post2 ⟩ ← U32.sub_spec by scalar_tac
split
next hcond =>
-- k1 = 0: the loop breaks; we are in the `done` disjunct with k = 1
have hkv : k.val = 1 := by
have h0 : k1.val = 0 := by rw [hcond]; scalar_tac
scalar_tac
simp only [spec_ok]
exact Or.inl ⟨hkv, a7.set 0#usize i63, by rw [back_post], hbnd8, hfin⟩
next hcond =>
-- k1 ≠ 0: continue with (k-1, a²); the `cont` disjunct with k ≥ 2
have hkv : 2 ≤ k.val := by
have h0 : k1.val ≠ 0 := fun hh => hcond (UScalar.eq_of_val_eq (by scalar_tac))
scalar_tac
simp only [spec_ok]
exact Or.inr ⟨hkv, k1, a7.set 0#usize i63, by rw [back_post],
by scalar_tac, hbnd8, hfin⟩
/-- Fuel-indexed loop spec: `pow2k_loop k a` computes `⟪a⟫ ^ (2^k)`.
Rust: the whole `loop { … }` of `pow2k`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:466-556,
modeled by `…FieldElement51.pow2k_loop` = `Aeneas.Std.loop body`.
MATH (ASCII): for any fuel n with 1 ≤ k ≤ n and Bnd(a, 2^54):
pow2k_loop k a = ok r with Bnd(r, 2^51 + 2^13)
and [[r]] = [[a]] ^ (2^k),
because k successive squarings give ((a^2)^2…)^2 = a^(2^k).
PROOF: `Aeneas.Std.loop` carries no termination measure, so we induct on
the EXPLICIT fuel bound n (generalizing k, a and the limbs). Each step
peels one iteration with `loop_step` and runs `pow2k_body_spec`:
the `.done` branch has k = 1 and r = a², i.e. [[a]]^(2^1); the `.cont`
branch recurses on (k-1, a²) — the body's output bound 2^51 + 2^13 ≤ 2^54
re-establishes the input invariant (Bnd.mono) — and
(a²)^(2^(k-1)) = a^(2^k) closes it. k decreases by 1 per iteration, so
fuel n ≥ k always suffices; n = 0 contradicts 1 ≤ k.
WHY NEEDED: induction needs the spec stated for ALL n; `pow2k_loop_spec`
below instantiates n := k.val. -/
theorem pow2k_loop_spec_aux (n : ) (k : U32) (a : Fe) (x0 x1 x2 x3 x4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hba : Bnd a (2^54)) (hk : 1 ≤ k.val) (hkn : k.val ≤ n) :
backend.serial.u64.field.FieldElement51.pow2k_loop k a
⦃ r => Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ ^ (2^k.val) ⦄ := by
-- fuel induction; everything that changes between iterations is generalized
induction n generalizing k a x0 x1 x2 x3 x4 with
| zero => exact absurd hkn (by omega)
| succ n ih =>
unfold backend.serial.u64.field.FieldElement51.pow2k_loop
-- peel exactly one loop iteration (loop_step, Proofs/AddSpec.lean) and
-- feed it the body spec; then case on the ControlFlow disjunction
apply loop_step
apply spec_mono (pow2k_body_spec k a x0 x1 x2 x3 x4 ha hba hk)
rintro cf (⟨hk1, r, rfl, hbr, hr⟩ | ⟨hk2, k1, r, rfl, hk1v, hbr, hr⟩)
· -- done: k = 1, one squaring; ⟪r⟫ = ⟪a⟫·⟪a⟫ = ⟪a⟫^(2^1)
refine ⟨hbr, ?_⟩
rw [hr, hk1]
ring
· -- cont: recurse on (k-1, a²)
-- name the limbs of the squared element for the IH …
obtain ⟨r0, r1, r2, r3, r4, hrl⟩ := Fe.exists_limbs r
-- … re-establish the 2^54 input invariant (2^51 + 2^13 ≤ 2^54) and
-- apply the induction hypothesis at fuel n, counter k1 = k-1 ≥ 1
have hih := ih k1 r r0 r1 r2 r3 r4 hrl (hbr.mono (by norm_num))
(by omega) (by omega)
unfold backend.serial.u64.field.FieldElement51.pow2k_loop at hih
apply spec_mono hih
rintro r' ⟨hbr', hr'⟩
refine ⟨hbr', ?_⟩
-- exponent bookkeeping: (⟪a⟫²)^(2^(k-1)) = ⟪a⟫^(2·2^(k-1)) = ⟪a⟫^(2^k)
have hexp : 2 * 2 ^ (k.val - 1) = 2 ^ k.val := by
rw [← pow_succ']
congr 1
omega
rw [hr', hr, hk1v, ← hexp, pow_mul]
ring
/-- Loop spec: `pow2k_loop k a` computes `⟪a⟫ ^ (2^k)` (limbs < 2⁵¹ + 2¹³).
Same statement as `pow2k_loop_spec_aux` with the fuel hidden: instantiate
n := k.val (each iteration decrements k, so k.val iterations suffice).
WHY NEEDED: the fuel is a proof artifact; `pow2k_spec` wants the clean
statement. -/
theorem pow2k_loop_spec (k : U32) (a : Fe) (x0 x1 x2 x3 x4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hba : Bnd a (2^54)) (hk : 1 ≤ k.val) :
backend.serial.u64.field.FieldElement51.pow2k_loop k a
⦃ r => Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ ^ (2^k.val) ⦄ :=
pow2k_loop_spec_aux k.val k a x0 x1 x2 x3 x4 ha hba hk (Nat.le_refl _)
/-- Main spec for `pow2k`: under the 2⁵⁴ invariant and `k ≥ 1`, no panic,
output limbs < 2⁵¹ + 2¹³, and the denotation is `⟪a⟫ ^ (2^k)`.
Rust: `FieldElement51::pow2k`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:454-559.
MATH (ASCII): Bnd(a,2^54) and k ≥ 1 ==>
fe_pow2k a k = ok r with Bnd(r, 2^51 + 2^13) and [[r]] = [[a]]^(2^k).
The generated body is `massert (k > 0); pow2k_loop k self` — the massert
is the surviving `debug_assert!(k > 0)` (field.rs:456), provable from hk.
The k ≥ 1 hypothesis encodes the documented caveat: pow2k(_, 0) would
wrap k-1 in release Rust; all in-crate callers pass constants ≥ 1.
WHY NEEDED: `square` below and every pow2k step of the pow22501 chain in
Proofs/InvertSpec.lean (hence impl_mul_inv_cancel in FieldMain). -/
@[step]
theorem pow2k_spec (a : Fe) (k : U32) (x0 x1 x2 x3 x4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hba : Bnd a (2^54)) (hk : 1 ≤ k.val) :
fe_pow2k a k ⦃ r => Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ ^ (2^k.val) ⦄ := by
unfold fe_pow2k backend.serial.u64.field.FieldElement51.pow2k
-- discharge the debug_assert!(k > 0) — provable, not assumed
let* ⟨ _ ⟩ ← massert_spec by scalar_tac
-- depending on how the trailing `ok a` bind got normalized, the goal is
-- either exactly the loop spec or one bind away from it; handle both
first
| exact pow2k_loop_spec k a x0 x1 x2 x3 x4 ha hba hk
| (apply spec_bind (pow2k_loop_spec k a x0 x1 x2 x3 x4 ha hba hk);
intro r hr;
simp only [spec_ok];
exact hr)
/-- Main spec for `square`: under the 2⁵⁴ invariant, no panic, output limbs
< 2⁵¹ + 2¹³, and the denotation squares in 𝔽_p.
Rust: `FieldElement51::square` = `self.pow2k(1)`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:562-564.
MATH (ASCII): Bnd(a, 2^54) ==> fe_square a = ok r with
Bnd(r, 2^51 + 2^13) and [[r]] = [[a]] * [[a]]
— instance of pow2k_spec at k = 1, since [[a]]^(2^1) = [[a]]·[[a]].
WHY NEEDED: the squaring steps of InvertSpec's pow22501 chain run
through this lemma; with mul_spec it underpins impl_mul_inv_cancel. -/
@[step]
theorem square_spec (a : Fe) (x0 x1 x2 x3 x4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hba : Bnd a (2^54)) :
fe_square a ⦃ r => Bnd r (2^51 + 2^13) ∧ ⟪r⟫ = ⟪a⟫ * ⟪a⟫ ⦄ := by
unfold fe_square backend.serial.u64.field.FieldElement51.square
-- run pow2k at the literal 1#u32, then rewrite x^(2^1) to x*x
apply spec_mono (pow2k_spec a 1#u32 x0 x1 x2 x3 x4 ha hba (by scalar_tac))
rintro r ⟨hbr, hr⟩
refine ⟨hbr, ?_⟩
have h1 : (1#u32).val = 1 := by scalar_tac
rw [hr, h1]
ring
end CurveFieldProofs

View file

@ -0,0 +1,321 @@
/- ──────────────────────────────────────────────────────────────────────────────
Proofs/SubNegSpec.lean — subtraction and negation via the "add 16p" trick
WHAT THIS FILE CONTAINS
Specs for the transpiled `sub` (a + 16p b, then reduce) and `negate`
(16p a, then reduce): no panic under the 2⁵⁴ invariant, output < 2⁵²,
and the denotation is subtraction/negation in 𝔽_p. Plus three helpers:
a composition-friendly restatement of `reduce_spec` (`reduce_make_spec`),
the constant identity `sixteen_p`, and the 𝔽_p bridge `cast_key`.
RUST ANALOG
- `impl Sub<&FieldElement51> for &FieldElement51::sub`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:84-101
- `FieldElement51::negate`, field.rs:276-286
Transpiled bodies in gen/CurveField/Funs.lean:
`Shared0FieldElement51.Insts.CoreOpsArithSubSharedAFieldElement51FieldElement51.sub`
and `backend.serial.u64.field.FieldElement51.negate`.
THE 16p TRICK (why the strange constants)
u64 subtraction PANICS on underflow (in the Aeneas model: `fail`), and a_i b_i
would underflow whenever b_i > a_i. The Rust therefore computes a b as
a + 16p b, with 16p pre-encoded limb-wise:
C0 = 16·(2⁵¹ 19) = 36028797018963664 (limb 0)
Ci = 16·(2⁵¹ 1) = 36028797018963952 (limbs 1..4)
so that C0 + C1·2⁵¹ + C2·2¹⁰² + C3·2¹⁵³ + C4·2²⁰⁴ = 16p EXACTLY
(lemma `sixteen_p`; this is the radix-2⁵¹ "borrowed" spelling of
16p = 16·(2²⁵⁵ 19): limb 0 carries the 19, every higher limb is one short of
2⁵⁵ because it lent a borrow downward). Each Ci ≈ 2⁵⁵ exceeds any b_i < 2⁵⁴, so
(a_i + Ci) b_i never underflows, and a_i + Ci < 2⁵⁴ + 2⁵⁵ < 2⁶⁴ never overflows.
Adding 16p ≡ 0 (mod p) leaves the denotation unchanged; the trailing `reduce`
(Proofs/ReduceSpec.lean) restores limbs < 2⁵². `negate` is the b := a, a := 0
special case: 16p a.
PROOF ARCHITECTURE (shared by sub_spec / neg_spec)
1. Walk the monadic body with `let* ... ← op_spec by(...)` — one machine op per
line, each producing a named postcondition hypothesis (i_post, i1_post, ...);
the `by(...)` block discharges that op's overflow/underflow/bounds side
condition (this IS the panic-freedom proof, op by op).
2. End with `reduce_make_spec`, yielding the exact carry equation of reduce.
3. Assemble everything into one purely ADDITIVE equation `key`
(e.g. feVal r + p·carry + feVal b = feVal a + 16p) closed by `omega` —
additive so truncated subtraction never appears.
4. Cast once to 𝔽_p with `cast_key` (16p and p·carry vanish mod p).
ROLE IN THE MAIN THEOREM
`sub_spec`/`neg_spec` are the totality + correctness facts for the field's
subtraction and additive inverse; Proofs/Field.lean and Proofs/FieldMain.lean
consume them for `impl_add_neg` and friends. MulSpec.lean imports this file
(its carry accounting reuses the same lemma style and `reduce_make_spec`).
FILE RELATIONS
Imports Proofs/ReduceSpec.lean (reduce_spec + the %/÷ simp lemmas).
Imported by Proofs/MulSpec.lean and Proofs/Field.lean.
────────────────────────────────────────────────────────────────────────────── -/
import Proofs.ReduceSpec
open Aeneas Aeneas.Std Result
open curve25519_dalek
set_option maxHeartbeats 4000000
namespace CurveFieldProofs
/-- `reduce` applied to a literal `Array.make` — composition-friendly form.
Rust: same as `reduce_spec` — `FieldElement51::reduce`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:290-323 — but matching the
call shape `FieldElement51::reduce([e0, e1, e2, e3, e4])` that `sub` (field.rs:94-100)
and `negate` (field.rs:278-284) produce: the transpiler emits
`reduce (Array.make 5#usize [i3, i7, ...])`.
MATH: reduce [x0..x4] = ok r with Bnd(r, 2^51 + 19*2^13)
and feVal r + p*(x4 div 2^51) = x0 + x1*2^51 + x2*2^102 + x3*2^153 + x4*2^204.
WHY NEEDED: pure plumbing. `reduce_spec` takes an abstract `Fe` plus a hypothesis
naming its limbs; here the limbs are syntactically visible in the `Array.make`
literal, so this version needs no side hypothesis and can be applied directly by
the `let*`/`step` machinery (hence the `@[step]` registration) at the end of the
sub/negate proofs below. -/
@[step]
theorem reduce_make_spec (x0 x1 x2 x3 x4 : U64) :
fe_reduce (Array.make 5#usize [x0, x1, x2, x3, x4]) ⦃ r =>
Bnd r (2^51 + 19 * 2^13) ∧
feVal r + P * (x4.val / 2^51) = limbsVal x0 x1 x2 x3 x4 ⦄ := by
have h : (↑(Array.make 5#usize [x0, x1, x2, x3, x4]) : List U64)
= [x0, x1, x2, x3, x4] := rfl
have hs := reduce_spec (Array.make 5#usize [x0, x1, x2, x3, x4])
x0 x1 x2 x3 x4 h
simpa [feVal_eq _ _ _ _ _ _ h] using hs
/-- Σ Cᵢ·2⁵¹ⁱ for the sub/neg constants (C₀ = 16(2⁵¹19), Cᵢ = 16(2⁵¹1))
is exactly 16p.
Rust: the magic literals 36028797018963664 / 36028797018963952 in `sub`
(field.rs:95-99) and `negate` (field.rs:279-283); the Rust comment at
field.rs:85-86 explains "first add a multiple of p. Choose 16*p = p << 4
to be larger than 54-bit _rhs".
MATH:
ASCII: C0 + C1*2^51 + C2*2^102 + C3*2^153 + C4*2^204 = 16 * (2^255 - 19)
LaTeX: $\sum_{i=0}^{4} C_i\,2^{51 i} = 16p$ with $C_0 = 16(2^{51}-19)$,
$C_i = 16(2^{51}-1)$ for $i \ge 1$.
This is the radix-2⁵¹ borrowed expansion of 16p: each upper limb is 16 short of
16·2⁵¹ because it lends 16·2⁵¹ to the limb below, and limb 0 additionally absorbs
16·(19). Verified by `norm_num` literal arithmetic in the kernel.
WHY NEEDED: the `key` equations of `sub_spec`/`neg_spec` below state
"result + p·carry + b = a + 16p"; `omega` needs 16p both as the closed-form
constant and as the limb-constant sum that actually appears in the executed code,
and this lemma is that equality. -/
theorem sixteen_p :
36028797018963664 + 2^51 * 36028797018963952 + 2^102 * 36028797018963952
+ 2^153 * 36028797018963952 + 2^204 * 36028797018963952 = 16 * P := by
norm_num [P]
/-- The casting bridge: from the exact -level equation to 𝔽_p.
No Rust analog — this is pure proof infrastructure.
MATH:
ASCII: if x + p*k + y = m + 16*p over the naturals,
then (x : F_p) = (m : F_p) - (y : F_p).
LaTeX: $x + pk + y = m + 16p \;\Rightarrow\; \bar x = \bar m - \bar y$ in
$\mathbb F_p$ (both $pk$ and $16p$ vanish, since $p \equiv 0$).
Instantiated with x = feVal r, y = feVal b (or feVal a for negate), k = the
reduce carry, m = feVal a (or 0).
WHY NEEDED: this is the single point where the -level bookkeeping of the proofs
becomes a field equation. The hypothesis is deliberately ADDITIVE (no subtraction)
so it can be produced by `omega` over ; subtraction only ever appears here, on the
𝔽_p side, where it is total. `congrArg Nat.cast` maps the equation through the
ring homomorphism → ZMod p, `push_cast` distributes it, and `P ≡ 0` kills the
multiples of p. -/
theorem cast_key {x y k m : } (h : x + P * k + y = m + 16 * P) :
(x : Fp) = (m : Fp) - (y : Fp) := by
-- map the equation through the cast ring hom; (P : Fp) = 0 makes P·k and 16·P vanish
have hc := congrArg (Nat.cast : → Fp) h
push_cast at hc
-- turn the goal x = m y into x + y = m and close with the cast equation
rw [eq_sub_iff_add_eq]
simpa using hc
/-- `sub` spec: total under the 2⁵⁴ limb invariant, output < 2⁵², and the
denotation subtracts in 𝔽_p.
Rust: `impl Sub<&FieldElement51> for &FieldElement51::sub`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:84-101 —
`FieldElement51::reduce([(a[i] + C_i) - b[i]; 5])` with the 16p constants C_i.
MATH:
ASCII: forall a b : Fe, Bnd(a, 2^54) and Bnd(b, 2^54) ==>
fe_sub a b = ok r with Bnd(r, 2^52) and [[r]] = [[a]] - [[b]] in F_p.
LaTeX: $\forall a\,b,\ \mathrm{Bnd}(a,2^{54}) \wedge \mathrm{Bnd}(b,2^{54})
\Rightarrow \exists r,\ \mathrm{sub}(a,b) = \mathrm{ok}\ r \wedge
\mathrm{Bnd}(r,2^{52}) \wedge
\llbracket r\rrbracket = \llbracket a\rrbracket - \llbracket b\rrbracket$.
Per-limb machine arithmetic (i = 0..4, all in u64):
t_i = (a_i + C_i) b_i ; no overflow since a_i + C_i < 2⁵⁴ + 2⁵⁵·1.0007 < 2⁶⁴,
no underflow since C_i ≥ 16(2⁵¹19) > 2⁵⁴ > b_i; then r = reduce [t0..t4].
Value: Σ t_i 2^(51i) = feVal a + 16p feVal b exactly over , and reduce removes
p·carry, so feVal r + p·carry + feVal b = feVal a + 16p (`key` below); casting to
𝔽_p (cast_key) gives ⟪r⟫ = ⟪a⟫ ⟪b⟫.
WHY NEEDED: this is the field-subtraction leg of the main theorem — its totality is
part of `fieldImplementation`'s no-panic claim, and Field.lean/FieldMain.lean derive
`impl_add_neg` (a + (a) = 0) and the subtraction-compatibility laws from it. -/
theorem sub_spec (a b : Fe) (x0 x1 x2 x3 x4 y0 y1 y2 y3 y4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hb : (↑b : List U64) = [y0, y1, y2, y3, y4])
(hba : Bnd a (2^54)) (hbb : Bnd b (2^54)) :
fe_sub a b ⦃ r => Bnd r (2^52) ∧ ⟪r⟫ = ⟪a⟫ - ⟪b⟫ ⦄ := by
-- restate the Bnd invariants as plain per-limb inequalities (x_i < 2⁵⁴ etc.)
rw [Bnd_eq a x0 x1 x2 x3 x4 _ ha] at hba
rw [Bnd_eq b y0 y1 y2 y3 y4 _ hb] at hbb
-- expose the transpiled monadic body (gen/CurveField/Funs.lean)
unfold fe_sub
Shared0FieldElement51.Insts.CoreOpsArithSubSharedAFieldElement51FieldElement51.sub
-- Symbolic execution, one machine op per `let*`: read a limb / add the 16p constant /
-- read the other limb / subtract. Each `by(...)` block discharges that op's side
-- condition (index in bounds, no u64 overflow on +, no underflow on ) from the 2⁵⁴
-- bounds — these 20 discharges constitute the panic-freedom proof of `sub`.
-- limb 0: i3 = (x0 + C0) y0
let* ⟨ i, i_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i1, i1_post ⟩ ← U64.add_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i2, i2_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i3, i3_post1, i3_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- limb 1: i7 = (x1 + C1) y1
let* ⟨ i4, i4_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i5, i5_post ⟩ ← U64.add_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i6, i6_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i7, i7_post1, i7_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- limb 2: i11 = (x2 + C2) y2
let* ⟨ i8, i8_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i9, i9_post ⟩ ← U64.add_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i10, i10_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i11, i11_post1, i11_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- limb 3: i15 = (x3 + C3) y3
let* ⟨ i12, i12_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i13, i13_post ⟩ ← U64.add_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i14, i14_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i15, i15_post1, i15_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- limb 4: i19 = (x4 + C4) y4 (i19's carry i19/2⁵¹ is the p-multiple reduce removes)
let* ⟨ i16, i16_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i17, i17_post ⟩ ← U64.add_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i18, i18_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i19, i19_post1, i19_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- final reduce on the raw limbs [i3, i7, i11, i15, i19] (ReduceSpec, packaged form)
let* ⟨ r, r_post1, r_post2 ⟩ ← reduce_make_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- Bnd conjunct: weaken reduce's bound 2⁵¹ + 19·2¹³ to the stated 2⁵² (Bnd.mono)
refine ⟨r_post1.mono (by norm_num), ?_⟩
-- Exact -level accounting: result + P·carry + b = a + 16p.
have key : feVal r + P * (i19.val / 2^51) + feVal b
= feVal a + 16 * P := by
-- rewrite all three feVal's to limb sums; r_post2 is reduce's carry equation
rw [r_post2, feVal_eq a x0 x1 x2 x3 x4 ha, feVal_eq b y0 y1 y2 y3 y4 hb]
simp only [limbsVal] at *
-- All limb equations (incl. -subtractions with their ≤ side facts) are
-- in context; 16p is the constant sum (sixteen_p). Linear: omega.
have h16 := sixteen_p
simp [i_post, i2_post, i4_post, i6_post, i8_post, i10_post, i12_post,
i14_post, i16_post, i18_post, ha, hb] at *
omega
-- one cast to 𝔽_p: 16p and p·carry vanish, leaving ⟪r⟫ = ⟪a⟫ ⟪b⟫
simpa [denote] using cast_key key
/-- `negate` spec: total under the 2⁵⁴ limb invariant, output < 2⁵², and the
denotation is the additive inverse in 𝔽_p.
Rust: `FieldElement51::negate`,
curve25519/solana-ed25519/src/backend/serial/u64/field.rs:276-286 —
`FieldElement51::reduce([C_i - self[i]; 5])` ("see commentary in the Sub impl").
MATH:
ASCII: forall a : Fe, Bnd(a, 2^54) ==>
fe_neg a = ok r with Bnd(r, 2^52) and [[r]] = -[[a]] in F_p.
LaTeX: $\forall a,\ \mathrm{Bnd}(a,2^{54}) \Rightarrow \exists r,\
\mathrm{negate}(a) = \mathrm{ok}\ r \wedge \mathrm{Bnd}(r,2^{52}) \wedge
\llbracket r\rrbracket = -\llbracket a\rrbracket$.
This is `sub` specialised to 0 a: per limb t_i = C_i a_i (no underflow because
C_i ≥ 16(2⁵¹19) > 2⁵⁴ > a_i), so Σ t_i 2^(51i) = 16p feVal a exactly, then
reduce. The `key` equation is the m = 0 instance of sub's:
feVal r + p·carry + feVal a = 0 + 16p.
WHY NEEDED: provides the additive inverse for the field structure — FieldMain's
`impl_add_neg` (run negate, run add, get 0) rests on this spec's totality and
value clause. -/
theorem neg_spec (a : Fe) (x0 x1 x2 x3 x4 : U64)
(ha : (↑a : List U64) = [x0, x1, x2, x3, x4])
(hba : Bnd a (2^54)) :
fe_neg a ⦃ r => Bnd r (2^52) ∧ ⟪r⟫ = -⟪a⟫ ⦄ := by
-- restate Bnd as per-limb bounds, expose the transpiled body
rw [Bnd_eq a x0 x1 x2 x3 x4 _ ha] at hba
unfold fe_neg backend.serial.u64.field.FieldElement51.negate
-- Symbolic execution, two ops per limb (read a_i, compute C_i a_i); each `by(...)`
-- discharges the underflow side condition from a_i < 2⁵⁴ < C_i.
-- limb 0: i1 = C0 x0
let* ⟨ i, i_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i1, i1_post1, i1_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- limb 1: i3 = C1 x1
let* ⟨ i2, i2_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i3, i3_post1, i3_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- limb 2: i5 = C2 x2
let* ⟨ i4, i4_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i5, i5_post1, i5_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- limb 3: i7 = C3 x3
let* ⟨ i6, i6_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i7, i7_post1, i7_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- limb 4: i9 = C4 x4
let* ⟨ i8, i8_post ⟩ ← Array.index_usize_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
let* ⟨ i9, i9_post1, i9_post2 ⟩ ← U64.sub_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- final reduce on [i1, i3, i5, i7, i9]
let* ⟨ neg, neg_post1, neg_post2 ⟩ ← reduce_make_spec
by(subst_vars; try simp [Array.set_val_eq, *]; try scalar_tac)
-- Bnd conjunct: weaken 2⁵¹ + 19·2¹³ to 2⁵²
refine ⟨neg_post1.mono (by norm_num), ?_⟩
-- Exact accounting (sub's `key` with a := 0): result + P·carry + a = 0 + 16p.
have key : feVal neg + P * (i9.val / 2^51) + feVal a
= 0 + 16 * P := by
rw [neg_post2, feVal_eq a x0 x1 x2 x3 x4 ha]
simp only [limbsVal] at *
-- limb equations + sixteen_p in context; linear over : omega
have h16 := sixteen_p
simp [i_post, i2_post, i4_post, i6_post, i8_post, ha] at *
omega
-- cast once to 𝔽_p: ⟪neg⟫ = 0 ⟪a⟫ = ⟪a⟫
have := cast_key key
simpa [denote] using this
end CurveFieldProofs

134
verification/check.sh Executable file
View file

@ -0,0 +1,134 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# check.sh — THE button. Compiles EVERY shipped .lean file and axiom-audits
# EVERY layer certificate. If a file is in this repo, this script checks it;
# if this script doesn't check it, it must not be in the repo.
#
# Phases:
# 0. resource + source-integrity guards
# 1. stub audit: no `by trivial` specs, no True-target theorems, and — the
# anti-axiom-smuggling gate — ZERO `axiom` declarations under Proofs/
# (external models in gen/ are the only sanctioned axiom site)
# 2. compile gen/ + Proofs/ in dependency order (explicit -o, capped cores,
# per-file timeout). Any "declaration uses 'sorry'" warning is a FAILURE
# (this catches sorry robustly — text greps can't, comments mention it).
# 3. axiom audit: #print axioms for every certificate in CERTS; each must
# report exactly [propext, Classical.choice, Quot.sound]
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
source ~/aeneas-toolchain/env.sh
HERE="$(cd "$(dirname "$0")" && pwd)"
AENEAS_LEAN="$AENEAS_HOME/backends/lean"
TIMEOUT="${LEAN_TIMEOUT:-300}"
CORES="${LEAN_MAX_CORES:-0-3}"
# Layer manifests (extended as the pyramid grows; ORDER = import order).
GEN_MODULES=(
CurveField/TypesExternal
CurveField/Types
CurveField/FunsExternal
CurveField/Funs
)
PROOFS=(
Basic
Denote
P25519
ReduceSpec
SubNegSpec
ConstSpecs
AddSpec
MulSpec
SquareSpec
Square2Spec
Field
InvertSpec
FieldMain
FeQ
)
# Fully-qualified certificate names; each must be axiom-clean.
CERTS=(
CurveFieldProofs.fieldImplementation
)
# Imports needed so every certificate in CERTS is in scope for the audit.
AUDIT_IMPORTS=(
Proofs.FieldMain
)
# ── Phase 0: resource + integrity guards ────────────────────────────────────
free -m | awk '/Mem:/{if($7<2048){print "FATAL: <2GB RAM available — refusing to compile"; exit 1}}'
echo "=== Phase 0: source integrity ==="
for f in "$HERE"/gen/CurveField/*.lean "$HERE"/Proofs/*.lean; do
[ -f "$f" ] || continue
if ! grep -qE '^(/-|import |namespace |theorem |def |open |set_option |--)' "$f"; then
echo "CORRUPTED: $f is not Lean source (olean clobber?). Restore: git checkout HEAD -- $f"
exit 1
fi
done
echo " all sources valid"
# ── Phase 1: stub + axiom-smuggling audit ───────────────────────────────────
echo "=== Phase 1: stub audit ==="
if grep -rn 'by trivial' "$HERE"/Proofs/*Spec*.lean 2>/dev/null; then
echo "STUB DETECTED: 'by trivial' in spec files"; exit 1; fi
if grep -rn ' : True :=' "$HERE"/Proofs/*.lean 2>/dev/null; then
echo "STUB DETECTED: True-target theorem"; exit 1; fi
if grep -rnE '^(private |protected |noncomputable )*axiom ' "$HERE"/Proofs/*.lean 2>/dev/null; then
echo "AXIOM SMUGGLING DETECTED: axiom declaration under Proofs/ — forbidden."
echo "External models belong in gen/*/FunsExternal.lean and must stay outside"
echo "every certificate's dependency cone (Phase 3 verifies that)."
exit 1
fi
echo " clean: no trivial stubs, no True targets, no axioms outside gen/"
# ── Phase 2: compile everything shipped ─────────────────────────────────────
echo "=== Phase 2: compile ==="
LOG=$(mktemp /tmp/check-compile-XXXX.log)
cd "$AENEAS_LEAN"
lake env bash -c "
set -euo pipefail
cd '$HERE/gen' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD:$HERE\"
compile() {
echo \" · \$1\"
taskset -c $CORES timeout --kill-after=15 $TIMEOUT lean -o \"\${1}.olean\" \"\${1}.lean\" 2>&1 | tee -a '$LOG' || { echo \"FAIL: \$1\"; exit 1; }
}
for m in ${GEN_MODULES[*]}; do compile \"\$m\"; done
cd '$HERE'
for m in ${PROOFS[*]}; do
[ -f \"Proofs/\$m.lean\" ] || { echo \"MISSING: Proofs/\$m.lean listed in manifest\"; exit 1; }
compile \"Proofs/\$m\"
done
# every shipped proof file must be in the manifest (no dead files)
for f in Proofs/*.lean; do
b=\$(basename \"\$f\" .lean)
[ \"\$b\" = AxiomCheck ] && continue
case \" ${PROOFS[*]} \" in (*\" \$b \"*) ;; (*) echo \"DEAD FILE: \$f not in check manifest\"; exit 1;; esac
done
"
if grep -q "uses 'sorry'" "$LOG"; then
echo "STUB DETECTED: a compiled declaration uses 'sorry'"; exit 1; fi
rm -f "$LOG"
# ── Phase 3: axiom audit of every certificate ───────────────────────────────
echo "=== Phase 3: axiom audit ==="
EXPECTED="[propext, Classical.choice, Quot.sound]"
cd "$AENEAS_LEAN"
lake env bash -c "
set -euo pipefail
cd '$HERE/gen' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD:$HERE\"
cd '$HERE'
AUD=\$(mktemp /tmp/audit-XXXX.lean)
{
for i in ${AUDIT_IMPORTS[*]}; do echo \"import \$i\"; done
for c in ${CERTS[*]}; do echo \"#print axioms \$c\"; done
} > \"\$AUD\"
OUT=\$(taskset -c $CORES timeout --kill-after=15 $TIMEOUT lean \"\$AUD\" 2>&1)
echo \"\$OUT\"
rm -f \"\$AUD\"
N_CLEAN=\$(echo \"\$OUT\" | grep -cF \"depends on axioms: $EXPECTED\" || true)
if [ \"\$N_CLEAN\" -ne ${#CERTS[@]} ]; then
echo \"AXIOM AUDIT FAILED: \$N_CLEAN/${#CERTS[@]} certificates clean\"
exit 1
fi
"
echo ""
echo "ALL PROOFS PASS. ALL CERTIFICATES AXIOM-CLEAN. NO DEAD FILES."

45
verification/extract.sh Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Regenerate the Lean model in gen/ from the Rust sources.
#
# SCOPE: field arithmetic + Edwards point arithmetic
# roots: crate::field, crate::backend::serial::u64::field,
# crate::backend::serial::curve_models, crate::edwards
# (same widening the reference solution used for its Tier-1 addition-law
# theorem; scalar-mul backends and decompress internals stay opaque —
# upstream Aeneas cannot translate them; they are modeled/axiomatized in
# gen/CurveField/FunsExternal.lean OUTSIDE every certificate's cone).
#
# Rust --charon--> CurveField.llbc --aeneas--> gen/CurveField/*.lean
#
# The hand-written gen/CurveField/{TypesExternal,FunsExternal}.lean are NOT
# touched by regeneration (Aeneas only rewrites the *_Template variants).
# After regenerating, diff the templates against the hand-written files:
# diff gen/CurveField/FunsExternal_Template.lean gen/CurveField/FunsExternal.lean
#
# Usage: ./extract.sh
set -euo pipefail
source ~/aeneas-toolchain/env.sh
HERE="$(cd "$(dirname "$0")" && pwd)"
CRATE=~/GitClone/FormalVerification/sources/curve25519-dalek-source/curve25519-dalek
echo "[1/2] charon: Rust -> LLBC (field + curve_models + edwards)"
cd "$CRATE"
charon cargo --preset=aeneas \
--start-from crate::field \
--start-from crate::backend::serial::u64::field \
--start-from crate::backend::serial::curve_models \
--start-from crate::edwards \
--opaque 'crate::field::_::internal_invert_batch' \
--opaque 'crate::backend::serial::scalar_mul' \
--opaque 'crate::backend::vector' \
--opaque 'crate::edwards::decompress' \
--opaque 'crate::edwards::_::from_slice' \
--dest-file "$HERE/CurveField.llbc" \
-- --no-default-features
echo "[2/2] aeneas: LLBC -> Lean (split files, CurveField.* modules)"
cd "$HERE"
aeneas -backend lean -split-files -subdir CurveField -dest gen CurveField.llbc
echo "Done. Now run ./check.sh to type-check the regenerated model."

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,247 @@
-- Hand-written models for external functions (derived from FunsExternal_Template.lean).
-- [curve25519]: external functions.
--
-- Modeling policy (see ../../README.md):
-- * `subtle` items whose Rust bodies are real bit math are modeled FAITHFULLY
-- (bitwise or, mask-based select collapses to if-then-else only on the
-- documented {0,1} Choice invariant — noted per item).
-- * `subtle` items whose Rust bodies are optimization barriers
-- (`black_box`/volatile reads) are semantically the identity and modeled so.
-- * core RangeFull slice indexing (`s[..]`) is the identity on the slice.
-- * Remaining axioms (Debug fmt, raw-pointer get_unchecked*, the deliberately
-- opaque `internal_invert_batch`) carry no semantics field proofs rely on.
import Aeneas
import CurveField.Types
open Aeneas Aeneas.Std Result ControlFlow Error
set_option linter.dupNamespace false
set_option linter.hashCommand false
set_option linter.unusedVariables false
/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/
set_option maxHeartbeats 1000000
/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/
set_option maxRecDepth 2048
open curve25519_dalek
/-- [core::fmt::{impl core::fmt::Debug for [T]}::fmt]:
Source: '/rustc/library/core/src/fmt/mod.rs', lines 3122:4-3122:50
Name pattern: [core::fmt::{core::fmt::Debug<[@T]>}::fmt]
Visibility: public
AXIOM: only reachable from the `Debug` impl; no field proof depends on it. -/
@[rust_fun "core::fmt::{core::fmt::Debug<[@T]>}::fmt"]
axiom Slice.Insts.CoreFmtDebug.fmt
{T : Type} (DebugInst : core.fmt.Debug T) :
Slice T → core.fmt.Formatter → Result ((core.result.Result Unit
core.fmt.Error) × core.fmt.Formatter)
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::index_mut]:
Source: '/rustc/library/core/src/slice/index.rs', lines 660:4-660:51
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::index_mut]
MODEL: `&mut s[..]` is the whole slice; the backward function is the
identity update. -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::index_mut"]
def
core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.index_mut
{T : Type} (_ : core.ops.range.RangeFull) (s : Slice T) :
Result ((Slice T) × (Slice T → Slice T)) :=
ok (s, fun s' => s')
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::index]:
Source: '/rustc/library/core/src/slice/index.rs', lines 655:4-655:39
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::index]
MODEL: `&s[..]` is the whole slice. -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::index"]
def core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.index
{T : Type} (_ : core.ops.range.RangeFull) (s : Slice T) :
Result (Slice T) :=
ok s
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::get_unchecked_mut]:
Source: '/rustc/library/core/src/slice/index.rs', lines 650:4-650:66
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_unchecked_mut]
AXIOM: raw-pointer API, never called by the extracted field code. -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_unchecked_mut"]
axiom
core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_unchecked_mut
{T : Type} :
core.ops.range.RangeFull → MutRawPtr (Slice T) → Result (MutRawPtr (Slice
T))
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::get_unchecked]:
Source: '/rustc/library/core/src/slice/index.rs', lines 645:4-645:66
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_unchecked]
AXIOM: raw-pointer API, never called by the extracted field code. -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_unchecked"]
axiom
core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_unchecked
{T : Type} :
core.ops.range.RangeFull → ConstRawPtr (Slice T) → Result (ConstRawPtr
(Slice T))
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::get_mut]:
Source: '/rustc/library/core/src/slice/index.rs', lines 640:4-640:57
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_mut]
MODEL: always `some` (RangeFull never fails); backward function folds an
updated `some` back into the slice and keeps the original on `none`. -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_mut"]
def core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_mut
{T : Type} (_ : core.ops.range.RangeFull) (s : Slice T) :
Result ((Option (Slice T)) × (Option (Slice T) → Slice T)) :=
ok (some s, fun o => o.getD s)
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::get]:
Source: '/rustc/library/core/src/slice/index.rs', lines 635:4-635:45
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get]
MODEL: always `some` (RangeFull never fails). -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get"]
def core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get
{T : Type} (_ : core.ops.range.RangeFull) (s : Slice T) :
Result (Option (Slice T)) :=
ok (some s)
/-- [subtle::{impl core::convert::From<subtle::Choice> for bool}::from]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 153:4-153:35
Name pattern: [subtle::{core::convert::From<bool, subtle::Choice>}::from]
MODEL (faithful): Rust body is `source.0 != 0`. -/
@[rust_fun "subtle::{core::convert::From<bool, subtle::Choice>}::from"]
def Bool.Insts.CoreConvertFromChoice.from (c : subtle.Choice) : Result Bool :=
ok (c.val != 0)
/-- [subtle::{impl core::ops::bit::BitOr<subtle::Choice, subtle::Choice> for subtle::Choice}::bitor]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 177:4-177:41
Name pattern: [subtle::{core::ops::bit::BitOr<subtle::Choice, subtle::Choice, subtle::Choice>}::bitor]
MODEL (faithful): Rust body is `(self.0 | rhs.0).into()`, and the `.into()`
(`Choice::from`) is an optimization barrier = identity. Bitwise or on u8. -/
@[rust_fun
"subtle::{core::ops::bit::BitOr<subtle::Choice, subtle::Choice, subtle::Choice>}::bitor"]
def subtle.Choice.Insts.CoreOpsBitBitOrChoiceChoice.bitor
(a b : subtle.Choice) : Result subtle.Choice :=
ok (a ||| b)
/-- [subtle::{impl core::convert::From<u8> for subtle::Choice}::from]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 238:4-238:32
Name pattern: [subtle::{core::convert::From<subtle::Choice, u8>}::from]
MODEL (faithful): Rust body is `Choice(black_box(input))`; the volatile
read in `black_box` is semantically the identity. -/
@[rust_fun "subtle::{core::convert::From<subtle::Choice, u8>}::from"]
def subtle.Choice.Insts.CoreConvertFromU8.from
(b : Std.U8) : Result subtle.Choice :=
ok b
/-- [subtle::{impl subtle::ConstantTimeEq for [T]}::ct_eq]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 313:4-313:41
Name pattern: [subtle::{subtle::ConstantTimeEq<[@T]>}::ct_eq]
MODEL: 1 iff the slices are equal (length + elementwise), else 0.
CAVEAT: this equates `ConstantTimeEqInst.ct_eq` with logical equality on
`T`. That is exact for the only instantiation reachable from the field
code (`T = u8`, whose `ct_eq` is genuine equality); a hypothetical exotic
`ConstantTimeEq` instance would not be modeled faithfully. -/
@[rust_fun "subtle::{subtle::ConstantTimeEq<[@T]>}::ct_eq"]
noncomputable def Slice.Insts.SubtleConstantTimeEq.ct_eq
{T : Type} (ConstantTimeEqInst : subtle.ConstantTimeEq T)
(a b : Slice T) : Result subtle.Choice :=
open Classical in
ok (if a.val = b.val then 1#u8 else 0#u8)
/-- [subtle::{impl subtle::ConstantTimeEq for u8}::ct_eq]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 348:12-348:51
Name pattern: [subtle::{subtle::ConstantTimeEq<u8>}::ct_eq]
MODEL: 1 iff equal, else 0 — the specification the Rust xor/shift bit
trick implements for all inputs. -/
@[rust_fun "subtle::{subtle::ConstantTimeEq<u8>}::ct_eq"]
def U8.Insts.SubtleConstantTimeEq.ct_eq
(a b : Std.U8) : Result subtle.Choice :=
ok (if a = b then 1#u8 else 0#u8)
/-- [subtle::ConditionallySelectable::conditional_assign]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 442:4-442:66
Name pattern: [subtle::ConditionallySelectable::conditional_assign]
MODEL (faithful): the trait's default body is
`*self = Self::conditional_select(self, other, choice)`. -/
@[rust_fun "subtle::ConditionallySelectable::conditional_assign"]
def subtle.ConditionallySelectable.conditional_assign.default
{Self : Type} (ConditionallySelectableInst : subtle.ConditionallySelectable
Self) (self other : Self) (choice : subtle.Choice) : Result Self :=
ConditionallySelectableInst.conditional_select self other choice
/-- [subtle::ConditionallySelectable::conditional_swap]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 469:4-469:67
Name pattern: [subtle::ConditionallySelectable::conditional_swap]
MODEL (faithful): the trait's default body conditionally assigns each side
the other's original value. -/
@[rust_fun "subtle::ConditionallySelectable::conditional_swap"]
def subtle.ConditionallySelectable.conditional_swap.default
{Self : Type} (ConditionallySelectableInst : subtle.ConditionallySelectable
Self) (a b : Self) (choice : subtle.Choice) : Result (Self × Self) := do
let a1 ← ConditionallySelectableInst.conditional_assign a b choice
let b1 ← ConditionallySelectableInst.conditional_assign b a choice
ok (a1, b1)
/-- [subtle::{impl subtle::ConditionallySelectable for u64}::conditional_select]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 513:12-513:77
Name pattern: [subtle::{subtle::ConditionallySelectable<u64>}::conditional_select]
MODEL: `a` if choice = 0, else `b`. The Rust mask trick
`a ^ (-(choice as i64) as u64 & (a ^ b))` agrees with this on the Choice
invariant {0,1} (mask = 0 or all-ones). -/
@[rust_fun
"subtle::{subtle::ConditionallySelectable<u64>}::conditional_select"]
def U64.Insts.SubtleConditionallySelectable.conditional_select
(a b : Std.U64) (choice : subtle.Choice) : Result Std.U64 :=
ok (if choice.val = 0 then a else b)
/-- [subtle::{impl subtle::ConditionallySelectable for u64}::conditional_assign]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 521:12-521:74
Name pattern: [subtle::{subtle::ConditionallySelectable<u64>}::conditional_assign]
MODEL: keep `self` if choice = 0, else take `other` (same mask trick). -/
@[rust_fun
"subtle::{subtle::ConditionallySelectable<u64>}::conditional_assign"]
def U64.Insts.SubtleConditionallySelectable.conditional_assign
(self other : Std.U64) (choice : subtle.Choice) : Result Std.U64 :=
ok (if choice.val = 0 then self else other)
/-- [subtle::{impl subtle::ConditionallySelectable for u64}::conditional_swap]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 529:12-529:75
Name pattern: [subtle::{subtle::ConditionallySelectable<u64>}::conditional_swap]
MODEL: swap iff choice ≠ 0 (same mask trick, applied to both sides). -/
@[rust_fun "subtle::{subtle::ConditionallySelectable<u64>}::conditional_swap"]
def U64.Insts.SubtleConditionallySelectable.conditional_swap
(a b : Std.U64) (choice : subtle.Choice) : Result (Std.U64 × Std.U64) :=
ok (if choice.val = 0 then (a, b) else (b, a))
/-- [curve25519::field::{curve25519::backend::serial::u64::field::FieldElement51}::internal_invert_batch]:
Source: 'curve25519/solana-ed25519/src/field.rs', lines 195:4-229:5
AXIOM (deliberate): extracted opaque via charon `--opaque`. Dead code under
the extraction feature set (its only caller `invert_batch_alloc` is
alloc-gated); its iterator `rev/zip` loops have no Aeneas model. Give it a
model here if batch inversion ever becomes a verification target. -/
axiom field.FieldElement51.internal_invert_batch
:
Slice backend.serial.u64.field.FieldElement51 → Slice
backend.serial.u64.field.FieldElement51 → Result ((Slice
backend.serial.u64.field.FieldElement51) × (Slice
backend.serial.u64.field.FieldElement51))

View file

@ -0,0 +1,189 @@
-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS
-- [curve25519_dalek]: external functions.
-- This is a template file: rename it to "FunsExternal.lean" and fill the holes.
import Aeneas
import CurveField.Types
open Aeneas Aeneas.Std Result ControlFlow Error
set_option linter.dupNamespace false
set_option linter.hashCommand false
set_option linter.unusedVariables false
/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/
set_option maxHeartbeats 1000000
/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/
set_option maxRecDepth 2048
open curve25519_dalek
/-- [core::fmt::{impl core::fmt::Debug for [T]}::fmt]:
Source: '/rustc/library/core/src/fmt/mod.rs', lines 3122:4-3122:50
Name pattern: [core::fmt::{core::fmt::Debug<[@T]>}::fmt]
Visibility: public -/
@[rust_fun "core::fmt::{core::fmt::Debug<[@T]>}::fmt"]
axiom Slice.Insts.CoreFmtDebug.fmt
{T : Type} (DebugInst : core.fmt.Debug T) :
Slice T → core.fmt.Formatter → Result ((core.result.Result Unit
core.fmt.Error) × core.fmt.Formatter)
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::index_mut]:
Source: '/rustc/library/core/src/slice/index.rs', lines 660:4-660:51
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::index_mut]
Visibility: public -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::index_mut"]
axiom
core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.index_mut
{T : Type} :
core.ops.range.RangeFull → Slice T → Result ((Slice T) × (Slice T →
Slice T))
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::index]:
Source: '/rustc/library/core/src/slice/index.rs', lines 655:4-655:39
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::index]
Visibility: public -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::index"]
axiom core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.index
{T : Type} : core.ops.range.RangeFull → Slice T → Result (Slice T)
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::get_unchecked_mut]:
Source: '/rustc/library/core/src/slice/index.rs', lines 650:4-650:66
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_unchecked_mut]
Visibility: public -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_unchecked_mut"]
axiom
core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_unchecked_mut
{T : Type} :
core.ops.range.RangeFull → MutRawPtr (Slice T) → Result (MutRawPtr (Slice
T))
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::get_unchecked]:
Source: '/rustc/library/core/src/slice/index.rs', lines 645:4-645:66
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_unchecked]
Visibility: public -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_unchecked"]
axiom
core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_unchecked
{T : Type} :
core.ops.range.RangeFull → ConstRawPtr (Slice T) → Result (ConstRawPtr
(Slice T))
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::get_mut]:
Source: '/rustc/library/core/src/slice/index.rs', lines 640:4-640:57
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_mut]
Visibility: public -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get_mut"]
axiom core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_mut
{T : Type} :
core.ops.range.RangeFull → Slice T → Result ((Option (Slice T)) ×
(Option (Slice T) → Slice T))
/-- [core::slice::index::{impl core::slice::index::SliceIndex<[T], [T]> for core::ops::range::RangeFull}::get]:
Source: '/rustc/library/core/src/slice/index.rs', lines 635:4-635:45
Name pattern: [core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get]
Visibility: public -/
@[rust_fun
"core::slice::index::{core::slice::index::SliceIndex<core::ops::range::RangeFull, [@T], [@T]>}::get"]
axiom core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get
{T : Type} :
core.ops.range.RangeFull → Slice T → Result (Option (Slice T))
/-- [subtle::{impl core::convert::From<subtle::Choice> for bool}::from]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 153:4-153:35
Name pattern: [subtle::{core::convert::From<bool, subtle::Choice>}::from]
Visibility: public -/
@[rust_fun "subtle::{core::convert::From<bool, subtle::Choice>}::from"]
axiom Bool.Insts.CoreConvertFromChoice.from : subtle.Choice → Result Bool
/-- [subtle::{impl core::ops::bit::BitOr<subtle::Choice, subtle::Choice> for subtle::Choice}::bitor]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 177:4-177:41
Name pattern: [subtle::{core::ops::bit::BitOr<subtle::Choice, subtle::Choice, subtle::Choice>}::bitor]
Visibility: public -/
@[rust_fun
"subtle::{core::ops::bit::BitOr<subtle::Choice, subtle::Choice, subtle::Choice>}::bitor"]
axiom subtle.Choice.Insts.CoreOpsBitBitOrChoiceChoice.bitor
: subtle.Choice → subtle.Choice → Result subtle.Choice
/-- [subtle::{impl core::convert::From<u8> for subtle::Choice}::from]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 238:4-238:32
Name pattern: [subtle::{core::convert::From<subtle::Choice, u8>}::from]
Visibility: public -/
@[rust_fun "subtle::{core::convert::From<subtle::Choice, u8>}::from"]
axiom subtle.Choice.Insts.CoreConvertFromU8.from
: Std.U8 → Result subtle.Choice
/-- [subtle::{impl subtle::ConstantTimeEq for [T]}::ct_eq]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 313:4-313:41
Name pattern: [subtle::{subtle::ConstantTimeEq<[@T]>}::ct_eq]
Visibility: public -/
@[rust_fun "subtle::{subtle::ConstantTimeEq<[@T]>}::ct_eq"]
axiom Slice.Insts.SubtleConstantTimeEq.ct_eq
{T : Type} (ConstantTimeEqInst : subtle.ConstantTimeEq T) :
Slice T → Slice T → Result subtle.Choice
/-- [subtle::{impl subtle::ConstantTimeEq for u8}::ct_eq]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 348:12-348:51
Name pattern: [subtle::{subtle::ConstantTimeEq<u8>}::ct_eq]
Visibility: public -/
@[rust_fun "subtle::{subtle::ConstantTimeEq<u8>}::ct_eq"]
axiom U8.Insts.SubtleConstantTimeEq.ct_eq
: Std.U8 → Std.U8 → Result subtle.Choice
/-- [subtle::ConditionallySelectable::conditional_assign]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 442:4-442:66
Name pattern: [subtle::ConditionallySelectable::conditional_assign]
Visibility: public -/
@[rust_fun "subtle::ConditionallySelectable::conditional_assign"]
axiom subtle.ConditionallySelectable.conditional_assign.default
{Self : Type} (ConditionallySelectableInst : subtle.ConditionallySelectable
Self) :
Self → Self → subtle.Choice → Result Self
/-- [subtle::ConditionallySelectable::conditional_swap]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 469:4-469:67
Name pattern: [subtle::ConditionallySelectable::conditional_swap]
Visibility: public -/
@[rust_fun "subtle::ConditionallySelectable::conditional_swap"]
axiom subtle.ConditionallySelectable.conditional_swap.default
{Self : Type} (ConditionallySelectableInst : subtle.ConditionallySelectable
Self) :
Self → Self → subtle.Choice → Result (Self × Self)
/-- [subtle::{impl subtle::ConditionallySelectable for u64}::conditional_select]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 513:12-513:77
Name pattern: [subtle::{subtle::ConditionallySelectable<u64>}::conditional_select]
Visibility: public -/
@[rust_fun
"subtle::{subtle::ConditionallySelectable<u64>}::conditional_select"]
axiom U64.Insts.SubtleConditionallySelectable.conditional_select
: Std.U64 → Std.U64 → subtle.Choice → Result Std.U64
/-- [subtle::{impl subtle::ConditionallySelectable for u64}::conditional_assign]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 521:12-521:74
Name pattern: [subtle::{subtle::ConditionallySelectable<u64>}::conditional_assign]
Visibility: public -/
@[rust_fun
"subtle::{subtle::ConditionallySelectable<u64>}::conditional_assign"]
axiom U64.Insts.SubtleConditionallySelectable.conditional_assign
: Std.U64 → Std.U64 → subtle.Choice → Result Std.U64
/-- [subtle::{impl subtle::ConditionallySelectable for u64}::conditional_swap]:
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 529:12-529:75
Name pattern: [subtle::{subtle::ConditionallySelectable<u64>}::conditional_swap]
Visibility: public -/
@[rust_fun "subtle::{subtle::ConditionallySelectable<u64>}::conditional_swap"]
axiom U64.Insts.SubtleConditionallySelectable.conditional_swap
: Std.U64 → Std.U64 → subtle.Choice → Result (Std.U64 × Std.U64)
/-- [curve25519_dalek::field::{curve25519_dalek::backend::serial::u64::field::FieldElement51}::internal_invert_batch]:
Source: 'curve25519-dalek/src/field.rs', lines 238:4-272:5 -/
axiom field.FieldElement51.internal_invert_batch
:
Slice backend.serial.u64.field.FieldElement51 → Slice
backend.serial.u64.field.FieldElement51 → Result ((Slice
backend.serial.u64.field.FieldElement51) × (Slice
backend.serial.u64.field.FieldElement51))

View file

@ -0,0 +1,110 @@
-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS
-- [curve25519_dalek]: type definitions
import Aeneas
import CurveField.TypesExternal
open Aeneas Aeneas.Std Result ControlFlow Error
set_option linter.dupNamespace false
set_option linter.hashCommand false
set_option linter.unusedVariables false
/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/
set_option maxHeartbeats 1000000
/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/
set_option maxRecDepth 2048
namespace curve25519_dalek
/-- Trait declaration: [core::ops::arith::Add]
Source: '/rustc/library/core/src/ops/arith.rs', lines 76:0-76:31
Name pattern: [core::ops::arith::Add]
Visibility: public -/
@[rust_trait "core::ops::arith::Add"]
structure core.ops.arith.Add (Self : Type) (Rhs : Type) (Self_Output : Type)
where
add : Self → Rhs → Result Self_Output
/-- Trait declaration: [core::ops::arith::Sub]
Source: '/rustc/library/core/src/ops/arith.rs', lines 188:0-188:31
Name pattern: [core::ops::arith::Sub]
Visibility: public -/
@[rust_trait "core::ops::arith::Sub"]
structure core.ops.arith.Sub (Self : Type) (Rhs : Type) (Self_Output : Type)
where
sub : Self → Rhs → Result Self_Output
/-- Trait declaration: [core::ops::arith::Mul]
Source: '/rustc/library/core/src/ops/arith.rs', lines 322:0-322:31
Name pattern: [core::ops::arith::Mul]
Visibility: public -/
@[rust_trait "core::ops::arith::Mul"]
structure core.ops.arith.Mul (Self : Type) (Rhs : Type) (Self_Output : Type)
where
mul : Self → Rhs → Result Self_Output
/-- Trait declaration: [core::ops::arith::Neg]
Source: '/rustc/library/core/src/ops/arith.rs', lines 690:0-690:19
Name pattern: [core::ops::arith::Neg]
Visibility: public -/
@[rust_trait "core::ops::arith::Neg"]
structure core.ops.arith.Neg (Self : Type) (Self_Output : Type) where
neg : Self → Result Self_Output
/-- Trait declaration: [core::ops::arith::AddAssign]
Source: '/rustc/library/core/src/ops/arith.rs', lines 768:0-768:37
Name pattern: [core::ops::arith::AddAssign]
Visibility: public -/
@[rust_trait "core::ops::arith::AddAssign"]
structure core.ops.arith.AddAssign (Self : Type) (Rhs : Type) where
add_assign : Self → Rhs → Result Self
/-- Trait declaration: [core::ops::arith::SubAssign]
Source: '/rustc/library/core/src/ops/arith.rs', lines 839:0-839:37
Name pattern: [core::ops::arith::SubAssign]
Visibility: public -/
@[rust_trait "core::ops::arith::SubAssign"]
structure core.ops.arith.SubAssign (Self : Type) (Rhs : Type) where
sub_assign : Self → Rhs → Result Self
/-- Trait declaration: [core::ops::arith::MulAssign]
Source: '/rustc/library/core/src/ops/arith.rs', lines 901:0-901:37
Name pattern: [core::ops::arith::MulAssign]
Visibility: public -/
@[rust_trait "core::ops::arith::MulAssign"]
structure core.ops.arith.MulAssign (Self : Type) (Rhs : Type) where
mul_assign : Self → Rhs → Result Self
/-- [core::ops::range::RangeFull]
Source: '/rustc/library/core/src/ops/range.rs', lines 44:0-44:20
Name pattern: [core::ops::range::RangeFull]
Visibility: public -/
@[reducible, rust_type "core::ops::range::RangeFull"]
def core.ops.range.RangeFull := Unit
/-- Trait declaration: [subtle::ConstantTimeEq]
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 262:0-262:24
Name pattern: [subtle::ConstantTimeEq]
Visibility: public -/
@[rust_trait "subtle::ConstantTimeEq"]
structure subtle.ConstantTimeEq (Self : Type) where
ct_eq : Self → Self → Result subtle.Choice
/-- Trait declaration: [subtle::ConditionallySelectable]
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 393:0-393:39
Name pattern: [subtle::ConditionallySelectable]
Visibility: public -/
@[rust_trait "subtle::ConditionallySelectable"
(parentClauses := ["coremarkerCopyInst"])]
structure subtle.ConditionallySelectable (Self : Type) where
coremarkerCopyInst : core.marker.Copy Self
conditional_select : Self → Self → subtle.Choice → Result Self
conditional_assign : Self → Self → subtle.Choice → Result Self
conditional_swap : Self → Self → subtle.Choice → Result (Self × Self)
/-- [curve25519_dalek::backend::serial::u64::field::FieldElement51]
Source: 'curve25519-dalek/src/backend/serial/u64/field.rs', lines 43:0-43:47
Visibility: public -/
@[reducible]
def backend.serial.u64.field.FieldElement51 := Array Std.U64 5#usize
end curve25519_dalek

View file

@ -0,0 +1,24 @@
-- Hand-written models for external types (derived from TypesExternal_Template.lean).
-- [curve25519]: external types.
import Aeneas
open Aeneas Aeneas.Std Result ControlFlow Error
set_option linter.dupNamespace false
set_option linter.hashCommand false
set_option linter.unusedVariables false
/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/
set_option maxHeartbeats 1000000
/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/
set_option maxRecDepth 2048
/-- [subtle::Choice]
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 120:0-120:17
Name pattern: [subtle::Choice]
MODEL: `Choice` is a transparent `u8` newtype carrying the (informal)
invariant that its value is 0 or 1. The wrapper exists in Rust purely as an
optimization barrier (`black_box` volatile read), which is semantically the
identity, so we model the type as `U8` directly. -/
@[reducible, rust_type "subtle::Choice"]
def subtle.Choice : Type := Std.U8

View file

@ -0,0 +1,22 @@
-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS
-- [curve25519_dalek]: external types.
-- This is a template file: rename it to "TypesExternal.lean" and fill the holes.
import Aeneas
open Aeneas Aeneas.Std Result ControlFlow Error
set_option linter.dupNamespace false
set_option linter.hashCommand false
set_option linter.unusedVariables false
/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/
set_option maxHeartbeats 1000000
/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/
set_option maxRecDepth 2048
/-- [subtle::Choice]
Source: '/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs', lines 120:0-120:17
Name pattern: [subtle::Choice]
Visibility: public -/
@[rust_type "subtle::Choice"]
axiom subtle.Choice : Type