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

170 lines
9.5 KiB
Text
Raw Normal View History

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