mirror of
https://github.com/saymrwulf/dalek-ed25519-verified.git
synced 2026-09-05 20:30:58 +00:00
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:
parent
549bef148b
commit
a80e360a3e
24 changed files with 7366 additions and 1 deletions
|
|
@ -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 | — |
|
||||
|
|
|
|||
1
verification/CurveField.llbc
Normal file
1
verification/CurveField.llbc
Normal file
File diff suppressed because one or more lines are too long
327
verification/Proofs/AddSpec.lean
Normal file
327
verification/Proofs/AddSpec.lean
Normal 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
|
||||
172
verification/Proofs/Basic.lean
Normal file
172
verification/Proofs/Basic.lean
Normal 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
|
||||
190
verification/Proofs/ConstSpecs.lean
Normal file
190
verification/Proofs/ConstSpecs.lean
Normal 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) = ((P−1) : 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
|
||||
312
verification/Proofs/Denote.lean
Normal file
312
verification/Proofs/Denote.lean
Normal 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^(p−2) via the pow22501 addition chain; equals x⁻¹ by Fermat's little
|
||||
theorem — Proofs/InvertSpec.lean). -/
|
||||
abbrev fe_invert := field.FieldElement51.invert
|
||||
/-- Rust: `FieldElement51::ZERO`, u64/field.rs:263 (limbs [0,0,0,0,0] —
|
||||
Proofs/ConstSpecs.lean). -/
|
||||
abbrev fe_zero := backend.serial.u64.field.FieldElement51.ZERO
|
||||
/-- Rust: `FieldElement51::ONE`, u64/field.rs:265 (limbs [1,0,0,0,0] —
|
||||
Proofs/ConstSpecs.lean). -/
|
||||
abbrev fe_one := backend.serial.u64.field.FieldElement51.ONE
|
||||
/-- Rust: `FieldElement51::MINUS_ONE`, u64/field.rs:267-273 (the limbs of the
|
||||
literal p − 1 — Proofs/ConstSpecs.lean). -/
|
||||
abbrev fe_minus_one := backend.serial.u64.field.FieldElement51.MINUS_ONE
|
||||
|
||||
-- ───────────────────── Destructuring a field element ───────────────────────
|
||||
|
||||
/-- Every `Fe` is a 5-element list of u64 limbs.
|
||||
|
||||
MATH: forall a : Fe, exists a0 a1 a2 a3 a4 : U64,
|
||||
a = [a0, a1, a2, a3, a4].
|
||||
LaTeX: $\forall a,\ \exists a_0\dots a_4,\ a = [a_0,a_1,a_2,a_3,a_4]$.
|
||||
|
||||
The destructuring device EVERY proof in Proofs/ starts with
|
||||
(`obtain ⟨a0, a1, a2, a3, a4, hl⟩ := Fe.exists_limbs a`): `Fe` is a
|
||||
length-5 subtype, and the limbs must be given names before any limb
|
||||
arithmetic can be stated.
|
||||
WHY NEEDED: turns the abstract array into the concrete 5-element shape on
|
||||
which `feVal_eq` / `Bnd_eq` and the symbolic execution of the generated
|
||||
code (which indexes limbs 0..4) can fire. -/
|
||||
theorem Fe.exists_limbs (a : Fe) :
|
||||
∃ a0 a1 a2 a3 a4 : U64, (↑a : List U64) = [a0, a1, a2, a3, a4] := by
|
||||
-- The subtype carries the proof that the underlying list has length 5.
|
||||
have h : (↑a : List U64).length = 5 := by
|
||||
have := a.property
|
||||
simp_all
|
||||
-- Case-split on the shape of the list; only length 5 is consistent with h.
|
||||
match hl : (↑a : List U64) with
|
||||
| [a0, a1, a2, a3, a4] => exact ⟨a0, a1, a2, a3, a4, rfl⟩
|
||||
-- Lists of length 0–4 contradict h …
|
||||
| [] | [_] | [_,_] | [_,_,_] | [_,_,_,_] => simp [hl] at h
|
||||
-- … as does any list of length ≥ 6.
|
||||
| _::_::_::_::_::_::_ => simp [hl] at h
|
||||
|
||||
-- ─────────────────── Exact ℕ value and the denotation ⟪·⟫ ──────────────────
|
||||
|
||||
/-- Value of a limb vector as a natural number (radix 2⁵¹).
|
||||
|
||||
MATH: limbsVal a0 a1 a2 a3 a4
|
||||
= a0 + a1·2⁵¹ + a2·2¹⁰² + a3·2¹⁵³ + a4·2²⁰⁴ (in ℕ).
|
||||
LaTeX: $\sum_{i=0}^{4} a_i \cdot 2^{51 i}$.
|
||||
|
||||
This is the EXACT integer value — no `mod p`, no wraparound. All
|
||||
overflow/carry accounting in ReduceSpec/AddSpec/SubNegSpec/MulSpec/
|
||||
SquareSpec is performed on this ℕ value first (e.g. ReduceSpec proves
|
||||
`feVal r + p·(l4 >> 51) = feVal l` exactly); only at the end is the value
|
||||
cast into 𝔽_p by `denote`.
|
||||
WHY NEEDED: separates the bit-level bookkeeping (ℕ, decided by `omega`/
|
||||
`ring`) from the modular reasoning (𝔽_p, one cast at the end). -/
|
||||
def limbsVal (a0 a1 a2 a3 a4 : U64) : ℕ :=
|
||||
a0.val + 2^51 * a1.val + 2^102 * a2.val + 2^153 * a3.val + 2^204 * a4.val
|
||||
|
||||
/-- Value of a field element as a natural number.
|
||||
|
||||
MATH: feVal a = limbsVal a0 a1 a2 a3 a4 where [a0,…,a4] are a's limbs.
|
||||
Defined by matching on the underlying list; the `_ => 0` default is dead
|
||||
code (by `Fe.exists_limbs` the list always has exactly 5 elements) and
|
||||
exists only to make the function total without dependent matching.
|
||||
WHY NEEDED: lifts `limbsVal` from named limbs to whole field elements, so
|
||||
specs can be stated about an abstract `a : Fe`. -/
|
||||
def feVal (a : Fe) : ℕ :=
|
||||
match (↑a : List U64) with
|
||||
| [a0, a1, a2, a3, a4] => limbsVal a0 a1 a2 a3 a4
|
||||
| _ => 0
|
||||
|
||||
/-- Rewriting lemma: once the limbs of `a` are named (via `Fe.exists_limbs`),
|
||||
`feVal a` unfolds to the explicit polynomial `limbsVal a0 a1 a2 a3 a4`.
|
||||
Marked `@[simp]` so it fires automatically during proofs.
|
||||
WHY NEEDED: the `match` inside `feVal` cannot reduce on an abstract `a`;
|
||||
this lemma is the bridge every value computation goes through. -/
|
||||
@[simp]
|
||||
theorem feVal_eq (a : Fe) (a0 a1 a2 a3 a4 : U64)
|
||||
(h : (↑a : List U64) = [a0, a1, a2, a3, a4]) :
|
||||
feVal a = limbsVal a0 a1 a2 a3 a4 := by
|
||||
simp [feVal, h]
|
||||
|
||||
/-- The denotation ⟪a⟫ : 𝔽_p of a field element.
|
||||
|
||||
MATH: ⟪a⟫ = (a0 + a1·2⁵¹ + a2·2¹⁰² + a3·2¹⁵³ + a4·2²⁰⁴) mod p.
|
||||
LaTeX: $\llbracket a\rrbracket=\bigl(\sum_i a_i\,2^{51 i}\bigr)\bmod p$
|
||||
(the ℕ → `ZMod P` coercion performs the reduction mod p).
|
||||
|
||||
THE bridge from machine limbs to mathematics. The main theorem
|
||||
(Proofs/FieldMain.lean, `fieldImplementation`) is phrased entirely through
|
||||
this map: e.g. "fe_mul a b succeeds with result r and ⟪r⟫ = ⟪a⟫ * ⟪b⟫".
|
||||
Note the map is total but NOT injective — many limb vectors denote the
|
||||
same field element (the representation is redundant), which is exactly
|
||||
why "the code is a field" must be stated via ⟪·⟫ (surjectivity + each op
|
||||
realizing its 𝔽_p counterpart) rather than as a `Field Fe` instance. -/
|
||||
def denote (a : Fe) : Fp := (feVal a : Fp)
|
||||
|
||||
/- Bracket notation for the denotation. (⟪·⟫ rather than ⟦·⟧, which collides
|
||||
with `Quotient.mk`.) -/
|
||||
notation "⟪" a "⟫" => denote a
|
||||
|
||||
-- ───────────────────── The limb-bound invariant (Bnd) ──────────────────────
|
||||
|
||||
/-- Limb-bound invariant: all limbs < `c`. Operations require/provide:
|
||||
`reduce`/`mul`/`square` outputs satisfy `Bnd · (2^52)`;
|
||||
`mul`/`square`/`sub`/`neg` inputs require `Bnd · (2^54)`.
|
||||
|
||||
MATH: Bnd a c iff a_i < c for all i in 0..4.
|
||||
|
||||
This is the dalek "limb discipline" stated in the crate's own docs
|
||||
(u64/field.rs:29-32: radix-2⁵¹ coefficients "are allowed to grow up to
|
||||
2⁵⁴ between reductions modulo p"). Each operation's spec has the shape
|
||||
|
||||
Bnd inputs (2⁵⁴) ==> op succeeds, output Bnd (2⁵²), ⟪·⟫ correct
|
||||
|
||||
so any output (< 2⁵²) can be fed back as an input (< 2⁵⁴) via `Bnd.mono`,
|
||||
closing the composition loop. Like `feVal`, the non-5-element branch
|
||||
(`False`) is dead code that keeps the definition total.
|
||||
WHY NEEDED: totality (panic-freedom: every u64/u128 add/mul/shift in the
|
||||
carry chains stays in range, every `debug_assert` holds) is only true
|
||||
under this invariant; it is the hypothesis of every clause of the main
|
||||
theorem. -/
|
||||
def Bnd (a : Fe) (c : ℕ) : Prop :=
|
||||
match (↑a : List U64) with
|
||||
| [a0, a1, a2, a3, a4] =>
|
||||
a0.val < c ∧ a1.val < c ∧ a2.val < c ∧ a3.val < c ∧ a4.val < c
|
||||
| _ => False
|
||||
|
||||
/-- Rewriting lemma: once the limbs are named, `Bnd a c` unfolds to the five
|
||||
explicit inequalities (companion of `feVal_eq`; `@[simp]`).
|
||||
WHY NEEDED: the `match` inside `Bnd` cannot reduce on an abstract `a`. -/
|
||||
@[simp]
|
||||
theorem Bnd_eq (a : Fe) (a0 a1 a2 a3 a4 : U64) (c : ℕ)
|
||||
(h : (↑a : List U64) = [a0, a1, a2, a3, a4]) :
|
||||
Bnd a c ↔
|
||||
(a0.val < c ∧ a1.val < c ∧ a2.val < c ∧ a3.val < c ∧ a4.val < c) := by
|
||||
simp [Bnd, h]
|
||||
|
||||
/-- `Bnd` is monotone in the bound.
|
||||
|
||||
MATH: Bnd a c and c ≤ c' ==> Bnd a c'.
|
||||
WHY NEEDED: glues operation specs together — outputs carry the tight
|
||||
bound 2⁵² while the next operation's hypothesis asks for 2⁵⁴ (and the
|
||||
main theorem's invariant clauses weaken bounds the same way). -/
|
||||
theorem Bnd.mono {a : Fe} {c c' : ℕ} (h : Bnd a c) (hcc : c ≤ c') : Bnd a c' := by
|
||||
-- Name the limbs, rewrite both `Bnd`s to the five inequalities …
|
||||
obtain ⟨a0, a1, a2, a3, a4, hl⟩ := Fe.exists_limbs a
|
||||
rw [Bnd_eq a a0 a1 a2 a3 a4 c hl] at h
|
||||
rw [Bnd_eq a a0 a1 a2 a3 a4 c' hl]
|
||||
-- … then it is linear arithmetic.
|
||||
omega
|
||||
|
||||
/-! ## Basic facts about P -/
|
||||
|
||||
/-- MATH: 0 < p (p is a 255-bit number, so certainly positive).
|
||||
WHY NEEDED: positivity feeds the ℕ-subtraction and `mod p` lemmas used
|
||||
throughout the value-accounting proofs. -/
|
||||
theorem P_pos : 0 < P := by norm_num [P]
|
||||
|
||||
/-- THE modular identity of the whole development: 2²⁵⁵ = 19 in 𝔽_p.
|
||||
|
||||
MATH: 2^255 ≡ 19 (mod p), because 2^255 − 19 = p ≡ 0 (mod p).
|
||||
LaTeX: $2^{255} \equiv 19 \pmod{2^{255}-19}$.
|
||||
|
||||
Every appearance of the magic constant 19 in the Rust code is justified
|
||||
by exactly this identity: a contribution that overflows past bit 255 may
|
||||
be folded back into limb 0 multiplied by 19 (the ×19 limb products in
|
||||
`mul`/`square`, the re-entering top carry in `reduce`) and the 16p
|
||||
constants added by `sub`/`negate` vanish mod p for the same reason.
|
||||
WHY NEEDED: invoked, directly or through derived multiple-of-p facts, by
|
||||
every correctness proof that moves value across the 2²⁵⁵ boundary. -/
|
||||
theorem two_pow_255_eq :
|
||||
((2^255 : ℕ) : Fp) = (19 : Fp) := by
|
||||
-- p itself vanishes when cast into 𝔽_p …
|
||||
have h : ((P : ℕ) : Fp) = 0 := ZMod.natCast_self P
|
||||
-- … and over ℕ, 2²⁵⁵ is literally p + 19 (checked numerically).
|
||||
have : (2^255 : ℕ) = P + 19 := by norm_num [P]
|
||||
rw [this]
|
||||
-- Push the cast through the sum; the p-summand is 0, leaving 19.
|
||||
push_cast [h]
|
||||
ring
|
||||
|
||||
end CurveFieldProofs
|
||||
717
verification/Proofs/FeQ.lean
Normal file
717
verification/Proofs/FeQ.lean
Normal 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^(p−2) 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)
|
||||
|
||||
/-- x−y = 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^(p−2) = 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
|
||||
205
verification/Proofs/Field.lean
Normal file
205
verification/Proofs/Field.lean
Normal 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
|
||||
451
verification/Proofs/FieldMain.lean
Normal file
451
verification/Proofs/FieldMain.lean
Normal 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^(p−2) 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^(p−2); 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
|
||||
243
verification/Proofs/InvertSpec.lean
Normal file
243
verification/Proofs/InvertSpec.lean
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
/- ───────────────────────────────────────────────────────────────────────────
|
||||
Proofs/InvertSpec.lean — Spec for the transpiled `invert`: x ↦ x^(p−2) 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^(p−1) = 1 for a ≠ 0, hence a · a^(p−2) = 1, i.e.
|
||||
a^(p−2) = a⁻¹. The Rust code (curve25519/solana-ed25519/src/field.rs)
|
||||
computes x^(p−2) 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^250−1), 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^250−1) and ⟪t3⟫ = x^11 are exactly the two facts
|
||||
`invert_spec` combines into x^(p−2). -/
|
||||
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^250−1), t3 = x^11
|
||||
let t20 = t19.pow2k(5); // t20 = x^(2^255−2^5)
|
||||
let t21 = &t20 * &t3; // t21 = x^(2^255−32+11) = x^(2^255−21)
|
||||
|
||||
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^(p−1) = 1, hence
|
||||
a · a^(p−2) = a^(p−1) = 1, hence a^(p−2) = a⁻¹;
|
||||
* if ⟪a⟫ = 0: 0^(p−2) = 0 (p−2 > 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^250−1), ⟪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^250−1))^(2^5) · ⟪a⟫^11 = ⟪a⟫^(2^255−21) = ⟪a⟫^(P−2) = ⟪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^(P−2) = 0 = 0⁻¹ (mathlib convention, P−2 > 0)
|
||||
rw [h0]
|
||||
rw [zero_pow (by norm_num [P]), inv_zero]
|
||||
· -- Fermat: a^(P−1) = 1, hence a · a^(P−2) = 1, hence a^(P−2) = 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^(P−2) = 1 = a · a⁻¹
|
||||
exact mul_left_cancel₀ h0 (by rw [hmul, mul_inv_cancel₀ h0])
|
||||
|
||||
end CurveFieldProofs
|
||||
542
verification/Proofs/MulSpec.lean
Normal file
542
verification/Proofs/MulSpec.lean
Normal 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) hnc0–hnc4 — 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+j−5)) (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
|
||||
552
verification/Proofs/P25519.lean
Normal file
552
verification/Proofs/P25519.lean
Normal 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^(p−2) 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^(n−1) ≡ 1 (mod n) (Fermat condition)
|
||||
(2) g^((n−1)/q) ≢ 1 (mod n) for EVERY prime q dividing n−1,
|
||||
then n is prime.
|
||||
|
||||
Why this works: (1) says the multiplicative order of g modulo n divides n−1;
|
||||
if that order were a PROPER divisor of n−1 it would divide (n−1)/q for some
|
||||
prime q | n−1, contradicting (2). So g has order exactly n−1 in the unit
|
||||
group of Z/n. But that group has only φ(n) ≤ n−1 elements, so an element of
|
||||
order n−1 can exist only if φ(n) = n−1 — which happens precisely when n is
|
||||
prime. Mathlib packages this as `lucas_primality`.
|
||||
|
||||
The catch: condition (2) needs the COMPLETE prime factorization of n−1, 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) ^ (n−1) = 1` directly is hopeless: `^`
|
||||
on `ZMod n` unfolds to n−1 ≈ 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 `(n−1)/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 n−1, with
|
||||
k = (n−1)/q — this is what forces the witness to have full order n−1. -/
|
||||
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^(n−1) = 1 in ZMod n via `pow_eq_one_of_powMod`
|
||||
-- (its two `by decide`s check: n−1 < 2^256, and the powMod equation)
|
||||
-- h2 : ∀ q prime, q ∣ n−1 → g^((n−1)/q) ≠ 1 via `pow_ne_one_of_powMod`
|
||||
-- (its four `by decide`s check: (n−1)/q < 2^256, 1 < n,
|
||||
-- powMod g ((n−1)/q) n ≠ 1, and powMod … < n).
|
||||
--
|
||||
-- For h2 the published factorization of n−1 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^(n−1) ≡ 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 | n−1 must leave 2^((n−1)/q) ≢ 1 (mod n)
|
||||
· intro q hq hqd
|
||||
-- kernel-verified factorization of n−1, 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^((n−1)/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^((n−1)/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 n−1
|
||||
· 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 n−1 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
|
||||
169
verification/Proofs/ReduceSpec.lean
Normal file
169
verification/Proofs/ReduceSpec.lean
Normal 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 1–4 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
|
||||
288
verification/Proofs/Square2Spec.lean
Normal file
288
verification/Proofs/Square2Spec.lean
Normal 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
|
||||
593
verification/Proofs/SquareSpec.lean
Normal file
593
verification/Proofs/SquareSpec.lean
Normal 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) hnc0–hnc4 — 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+j−5)), 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
|
||||
321
verification/Proofs/SubNegSpec.lean
Normal file
321
verification/Proofs/SubNegSpec.lean
Normal 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
134
verification/check.sh
Executable 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
45
verification/extract.sh
Executable 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."
|
||||
1511
verification/gen/CurveField/Funs.lean
Normal file
1511
verification/gen/CurveField/Funs.lean
Normal file
File diff suppressed because it is too large
Load diff
247
verification/gen/CurveField/FunsExternal.lean
Normal file
247
verification/gen/CurveField/FunsExternal.lean
Normal 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))
|
||||
189
verification/gen/CurveField/FunsExternal_Template.lean
Normal file
189
verification/gen/CurveField/FunsExternal_Template.lean
Normal 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))
|
||||
|
||||
110
verification/gen/CurveField/Types.lean
Normal file
110
verification/gen/CurveField/Types.lean
Normal 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
|
||||
24
verification/gen/CurveField/TypesExternal.lean
Normal file
24
verification/gen/CurveField/TypesExternal.lean
Normal 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
|
||||
22
verification/gen/CurveField/TypesExternal_Template.lean
Normal file
22
verification/gen/CurveField/TypesExternal_Template.lean
Normal 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
|
||||
|
||||
Loading…
Reference in a new issue