Skip to content

x86-64 P-256 ECDH by 5-bit windows in Jacobian coordinates (−18%) - #1256

Merged
alex merged 19 commits into
mainfrom
claude/pensive-tesla-vwbldb-p256-ecdh-jac
Oct 8, 2026
Merged

alex merged 19 commits into
mainfrom
claude/pensive-tesla-vwbldb-p256-ecdh-jac

Conversation

@alex

@alex alex commented Oct 7, 2026

Copy link
Copy Markdown
Member

P-256 ECDH on x86-64 (vg_ecdh_p256, vg_ecdh_p256_adx) now computes [d]P by a new window method: 5-bit signed windows in Jacobian coordinates, with no complete additions. P-256's group has prime order, which this PR proves, so the window's additions are never exceptional.

The window method (Impl/Weierstrass/X86_64/WinJac.lean)

  • Digits. d is recoded as d + 16 Σ_{j<52} 32^j, giving 52 digits in [-16, 15]. They are read by the comb's digit code with w = 5.
  • Table. It holds [1 … 16]P, each entry as (X, Y, Z, Z², Z³) in Jacobian coordinates:
    • [2]P is a doubling, and [m+1]P = [m]P + P by the mixed addition (jacMixedHead/jacMixedTail).
    • Entries are stored at an address computed from the public counter.
  • Each window, from R = O:
    1. Five in-place doublings by the halving doubler (doubleHalfPublic, which the joint verification already uses), in a loop.
    2. The digit's entry selected in constant time: every entry is loaded and kept under a mask, 32 bytes at a time with AVX2 on _adx (selPassY) and 16 at a time otherwise (selPassAt). It is then negated for a negative digit.
    3. A Jacobian addition that uses the entry's cached Z² and Z³ (CachedJac.head + jacTail), 11M + 3S.
    4. Two masks: D = T where R = O (nzMask), and R is kept for a zero digit (eqMask 0).
  • Output. R is converted to projective coordinates for the existing tail (TCombCfg.outFix/outOps).

The previous method used 4-bit windows (65 digits), complete additions (Renes–Costello–Batina), and conversions between projective and Jacobian coordinates around every window's doublings.

The doubling is a parameter of the window method. Cfg.exchangeJ is the exchange with this multiplication, and only P-256 uses it. The other curves keep Cfg.exchange, and their generated code is unchanged.

Why no addition is exceptional (Proof/Weierstrass/WinJacMath.lean)

  • Prime order. Proof/Weierstrass/GroupOrder.lean and Proof/P256/PrimeOrder.lean prove that every point but O has order n:
    • #E ≤ 2p + 1 (an injection into Option (ZMod p × Bool));
    • n ∣ #E, since G has order n;
    • 2p + 1 < 3n;
    • no 2-torsion, by the existing certificate that the cubic has no root;
    • so #E is odd, #E = n, and n is prime.
  • Window additions. Before digit j, R = [32 e]P with 0 ≤ e < n, and e < n - 32 for j ≥ 1. Adding [d]P, for e ≥ 1 and d ≠ 0, is exceptional only if 32 e ≡ ±d (mod n).
    • The bounds rule this out except at j = 0 with k = n - 2|d|.
    • That case needs digit d₀ = -|d|. But d₀ = ((k + 16) mod 32) - 16, and n ≡ 17 (mod 32) for P-256, so it never happens (loop_noexc).
  • Table additions. [m]P + P for 2 ≤ m ≤ 15 is never exceptional (tbl_noexc).
  • Scalars ≥ n. The proof (jstep_pt) only needs R to stay on the curve; the result is used only for 1 ≤ d < n.

winJac_ok (Proof/Weierstrass/X86_64/WinJac.lean) is the window method's theorem. Its pieces are in WinJac{Lay,State,Ops,Store,Build,Dbls,Select,Entry,Step,Iter}.lean, and the P-256 instance with the halving doubler is in Proof/P256/X86_64/WinJac.lean.

The ECDH proof is now generic over the multiplication

  • Proof/Ecdh/X86_64/MulOk.lean defines the interface:
    • MulOk c mq W: the old mulPow_ok's hypotheses, concluding R = [k]P for 1 ≤ k < n, with W the bytes mq may write.
    • MulW c W: W is apart from what the rest of the exchange reads.
  • exchangeWith_ok (Main.lean) proves the exchange for any mq that satisfies them. d = 0 is handled there directly.
  • Instances:
    • The old window/ladder: mulQ_ok. P-224, P-384 and P-521 go through it, unchanged.
    • The new method: mulQJ_ok (Proof/Ecdh/X86_64/WinJac.lean).
  • Constant time is checked over the whole new code by taint_decide, as before (ecdh_ct, ecdh_ct_adx).

Prime order in the law variant

  • HasLawInvOrd (Proof/Weierstrass/X86_64/InvInterface.lean) extends HasLawInv with prime : PrimeOrder C.
  • P-256's x86-64 law variant (Variants/P256/X86_64/Law.lean) now has this type.
  • Its generic files (Generic/P256/X86_64/*, Generic/MdHash/P256/X86_64/*) take it.
  • The Mathlib group theory (GroupOrder.lean) is imported only through the variant.

Features

vg_ecdh_p256_adx now uses AVX2 for the selection, so it declares ["bmi2", "adx", "avx", "avx2"], as vg_ec_p256_public_key_adx does. The Rust side already chooses the _adx functions together, by ec::p256::Mul::select, which requires all four.

This touches no TCB or spec files.

Measurements

Criterion on Cascade Lake, main and this branch alternated:

ecdh_p256 main this PR aws-lc-rs OpenSSL
_adx 50.2–51.2 µs 41.0–41.9 µs (−18%) 39.1–40.0 µs 116–123 µs
baseline (VG_CPU_FEATURES=none) 62.4 µs 50.5 µs (−19%)

Checks

  • lake build and lake env lean --run Emit.lean --check: pass. Only src/asm/x86_64/ecdh_p256.rs changes.
  • ci/check_lean_imports.py, check_lean_speed.py, check_vectors.py, check_arch_gates.py, check_variants.py, check_mcdt.py: pass.
  • ci/algorithms_table.py: no change.
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings: pass.
  • cargo test --release with Wycheproof: pass as is and with VG_CPU_FEATURES=none.
  • Extra local checks (not committed), against the previous implementation:
    • 2002 small scalars;
    • d = n - j for small j.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe


Generated by Claude Code

claude added 19 commits October 7, 2026 21:48
`PrimeOrder C` (Proof/Weierstrass/PrimeOrder.lean, no Mathlib): every point
of the curve but the point at infinity has order `C.n`.

`primeOrder_of` (Proof/Weierstrass/GroupOrder.lean) proves it for a curve
with no point of order 2 whose base point has prime order n with
2p + 1 < 3n: Mathlib's group of points has at most 2p + 1 elements (an
injection into Option (ZMod p × Bool)), n divides its order, so the order
is n or 2n, and 2n is ruled out by Cauchy's theorem, since there is no
element of order 2. `Proof.P256.primeOrder` (Proof/P256/PrimeOrder.lean)
instantiates it; only the P-256 law variant imports it.

The x86-64 interface `HasLawInvOrd C` (Proof/Weierstrass/X86_64/
InvInterface.lean) extends `HasLawInv C` with `prime : PrimeOrder C`;
P-256's x86-64 variant now supplies it, and its generic registration files
take it. No TCB, Spec or generated code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
`Cfg.exchangeWith c mq` is the exchange with any program `mq` computing
`[d]P`; `exchange` and `exchangeJ` are it with `mulQ` and `mulQJ`
(definitionally: the generated code is unchanged).

`MulOk c mq W` (Proof/Ecdh/X86_64/MulOk.lean) states what the exchange
needs of `mq` followed by the power: from the state before it (the
hypotheses of `mulPow_ok`), it leaves `MulPostW`, `R` representing `[k]P`
for `k` in `[1, n-1]` and `Z^(p-2)` in `ACC`, writing only `W`; `MulW c W`
is what the rest needs of `W` (the constants, `D` and the flag kept).
`mulQ_ok`/`mulQ_w` give them for the window method or the ladder.
`exchangeWith_ok` and `ecdh_x86_of` take them; `exchange_ok` is the
corollary for `mulQ`, so P-224, P-384 and P-521 are unchanged.

P-256's `ecdh_x86`, `ecdh_verified` and their ADX versions are commented
out (TODO(WinJac)) until the Jacobian window method's `MulOk` is proven;
its constant-time proofs still check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
The ECDH proof's MulOk runs the multiplication for any scalar below
2^256 and uses the result only for k < n, so the window method's frame
now holds for any k, with R a triple of some point of the curve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
mulQJ_ok: the Jacobian 5-bit window method computes [d]P for d < n
(MulOk) for four words and a curve of prime order n = 17 (mod 32), with
any doubling that doubles a Jacobian triple in place; mulQJ_w: what it
writes keeps the constants, d and the flag (MulW). P-256's instances
(mulQJP256_ok, mulQJP256x_ok) re-enable ecdh_x86, ecdh_verified and
their _adx versions, which now take P-256's prime order, and the
registration file passes the variant's h.prime.

Rename the Jacobian window method's TblOk, storeEntry_ok, build_ok and
buildStep_ok (JTblOk, jstoreEntry_ok, jbuild_ok, jbuildStep_ok), which
clashed with the 4-bit window method's in modules that import both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
with_reducible assumption, instruction lists ascribed next to ++, and
the doubling's slots by a sublist rather than tauto.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
The _adx variant selects the table's entries 32 bytes at a time with
AVX2, so it requires avx and avx2 too (the emitter refused it); the
notes describe the 5-bit Jacobian window method and the doubling by
halving rather than the 4-bit windows with complete additions.
src/asm/x86_64/ecdh_p256.rs regenerated (EmitOne P256.X86_64; its
consts.rs, which holds every algorithm's tables, left as it was: the
group's table is unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
@alex
alex added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 33b414e Oct 8, 2026
35 checks passed
@alex
alex deleted the claude/pensive-tesla-vwbldb-p256-ecdh-jac branch October 8, 2026 02:27
alex pushed a commit that referenced this pull request Oct 8, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFYaXu423bswZqpujcACe
alex pushed a commit that referenced this pull request Oct 8, 2026
#1256 made the x86-64 ECDH proof generic over the scalar multiplication
(exchangeWith, MulOk) and added PrimeOrder. P-521 now uses its own
mulQJ4 (the 4-bit windows with a Jacobian accumulator) through
exchangeWith, instead of a Cfg.jacWin switch in mulQ, and takes its prime
order from #1256's PrimeOrder and primeOrder_of in place of OrdN and its
own group-order proof.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KRjToTNRB2yxFCdNMux4P
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants