Skip to content

x86 X448/Ed448: field arithmetic by calls of verified functions - #1257

Merged
alex merged 2 commits into
mainfrom
claude/kind-heisenberg-okxdak-gf448-x86
Oct 8, 2026
Merged

alex merged 2 commits into
mainfrom
claude/kind-heisenberg-okxdak-gf448-x86

Conversation

@alex

@alex alex commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Implements #1252's spec of vg_gf448_r16_* on x86.

Why

On x86 (32-bit), X448 and Ed448 inlined every field operation: each multiplication was a fully unrolled 28×28 product. That made src/asm/x86/x448.rs about 46k instructions and ed448.rs about 179k. This PR implements #1252's curve448 field functions on x86 and makes the callers call them.

What

  • The field functions (Impl/X448/X86.lean, Artifacts/Gf448R16/X86.lean): vg_gf448_r16_mul, _add, _sub and _mul_a24.
    • The multiplication is the fully unrolled 28×28 product, as the inline code had it: each row reads its a limb and the b limbs at fixed offsets, with a carry pass. It exists once, so its size barely matters. (A first version ran the rows as a loop through a row pointer; CI measured it about 6% slower, from the dependent load at each row.)
    • Each function saves the callee-saved registers in its own part of the working space (bytes 4080–4095, inside the 3584–4095 the spec gives it). They use no stack.
    • Correctness and constant time are proven against the shared contracts (Proof/X448/X86/Fn*.lean).
  • The callers: X448's vg_x448, and Ed448's vg_ed448_scalar_base and vg_ed448_verify_equation. Each field operation pushes its arguments (ws, o, a, b) and calls a field function, which uses the 20 bytes of stack below the return address. Their contracts become …Contract X86.abi 20 with stack := 20.
  • Correctness of the callers:
    • Memory frames now include the call stack. Facts about memory outside the working space are recovered per piece with Exec.frameSp.
    • X448's Main.lean and Ed448's BaseMain.lean are split into stage lemmas.
  • Constant time of the callers (Proof/X448/X86/{CallCT,Runs,CT}.lean, Proof/Ed448/X86/{BaseCT,VerifyCT}.lean):
    • The calls are related run by run (RelCT.callWith), using the callee's own constant time, rather than by one taint analysis through the callees' bodies.
    • The blocks between calls are proven by the taint analysis, from what each run's correctness says of its own state.
    • The loops (ladder, squarings, Ed448's loops) use RelCT.loop with the correctness invariants.
    • Ed448's RestCT.lean, the old single taint check of everything after the entry block, is deleted.
  • Ed448's complete operations (public key, signing, verification): the only change is that their call sites now show the callee's 20 bytes of stack are apart from its buffers (Kit.callee_stk, Kit.callee_in). They already reserved those bytes (CalleeOk.stack ≤ 20).
  • No Spec/ or TCB/ changes.

The registration file for the multiplication, addition and subtraction states ofSig explicitly. The default rfl proof does not see through binContract's extra definition in #1252's spec. This is harmless, but it may be worth inlining binContract there.

Size

Instruction lines in the generated x86 code:

file before after
x448.rs 46,474 5,239
ed448.rs 179,479 24,712
gf448_r16.rs (new) — 8,934
total 225,953 38,885

Only src/asm/x86/{x448,ed448,mod}.rs and the new gf448_r16.rs change under src/.

Performance

CI's Benchmarks check ("Compare with base (x86)", i686, on the unrolled multiplication) shows every X448 and Ed448 benchmark faster than base:

Benchmark Base Head Change
ed448_keygen/57 6.74 ms 6.50 ms 3.5% faster
ed448_sign/64 6.77 ms 6.54 ms 3.4% faster
ed448_sign/1024 6.78 ms 6.55 ms 3.4% faster
ed448_sign/16384 7.15 ms 6.92 ms 3.1% faster
ed448_verify/64 11.12 ms 10.72 ms 3.5% faster
ed448_verify/1024 11.13 ms 10.75 ms 3.4% faster
ed448_verify/16384 11.32 ms 10.91 ms 3.6% faster
x448/56 3.39 ms 3.28 ms 3.3% faster
x448_generate/56 3.39 ms 3.28 ms 3.3% faster
x448_public_key/56 3.39 ms 3.28 ms 3.4% faster
x448_raw/56 3.39 ms 3.28 ms 3.2% faster

Local i686 measurements (a small harness over the public API, since the bench crate needs a 32-bit OpenSSL) were too noisy on this machine to resolve a few percent either way.

Checks

  • lake build (all jobs) and lake env lean --run Emit.lean --check pass. The emitter's audits pass: standard axioms only, every VG.Spec definition in Spec/.
  • ci/check_lean_imports.py, check_lean_speed.py, check_vectors.py, check_arch_gates.py, check_variants.py, check_mcdt.py, cpu_tests.py lint and algorithms_table.py all pass.
  • cargo fmt --check passes, and cargo clippy --all-targets -- -D warnings passes on x86-64 and i686-unknown-linux-gnu.
  • cargo test with Wycheproof passes natively and on i686-unknown-linux-gnu, in debug and release. That includes the RFC 7748/8032 and Wycheproof X448/Ed448 tests.
  • The costliest new declaration is verifyEquation_ct, at about 4.6 s, mostly kernel time for the entry block's taint check.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AiLjb1MZn17fRtREr4t9q9


Generated by Claude Code

@reaperhulk
reaperhulk added this pull request to stack #1266 October 8, 2026 02:36
@reaperhulk
reaperhulk added this pull request to the merge queue Oct 8, 2026
@reaperhulk
reaperhulk removed this pull request from the merge queue due to a manual request Oct 8, 2026
Base automatically changed from claude/kind-heisenberg-okxdak-gf448-spec to main October 8, 2026 02:53
@reaperhulk
reaperhulk force-pushed the claude/kind-heisenberg-okxdak-gf448-x86 branch from 13ca193 to 79a326d Compare October 8, 2026 02:53
claude added 2 commits October 8, 2026 03:03
Implement and prove curve448's field functions on x86 (32-bit),
vg_gf448_r16_{mul,add,sub,mul_a24} (Spec/X448/Field16.lean), and make
X448 and Ed448's base-point multiplication and verification's equation
call them instead of inlining every field operation.

- Impl/X448/X86.lean: the four functions (one rolled multiplication row
  loop; the callee-saved registers saved in the function's own part of the
  working space), and the callers' field operations as cdecl calls that
  push their arguments (20 bytes of stack below the return address).
- Proof/X448/X86: the functions' correctness and constant time against the
  shared contracts (Fn*.lean); the calls (FieldCall.lean); X448's
  correctness in stages with frames that include the call stack
  (Main.lean); constant time of code calling the field functions run by
  run (CallCT.lean, Runs.lean, CT.lean): the taint analysis cannot follow
  a callee that stores through a moving pointer, so calls are related by
  the callee's own constant time (RelCT.callWith) and the blocks between
  them by the taint analysis.
- Proof/Ed448/X86: the same for base-point multiplication (BaseMain.lean
  stages, BaseCT.lean) and verification's equation (VerifyCT.lean); the
  complete operations (public key, signing, verification) show their
  callee's 20 bytes of stack apart from its buffers. RestCT.lean, the
  whole-function taint check, is gone.
- The three callers' contracts take stack 20.

Generated x86 code: x448.rs 46,474 -> 5,239 instructions, ed448.rs
179,479 -> 24,712, plus 2,811 in the new gf448_r16.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AiLjb1MZn17fRtREr4t9q9
The row loop loaded the offset of `a` from the stack and added it to the
row pointer in every row, a dependent chain before each row's products,
which made X448 and Ed448 about 6% slower on x86 than the inlined code
(CI's comparison with the base). The rows are now straight-line code:
`ebp` points at `a` and the product's words are at constant offsets of
`edi`, so a row loads `a_i` with one instruction and needs no pointer or
end test (`mulRowU_ok`, as `rowWith_ok` without the walking pointer).
The function exists once, so its size (8,934 instructions for the four
functions) costs little.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AiLjb1MZn17fRtREr4t9q9
@alex
alex force-pushed the claude/kind-heisenberg-okxdak-gf448-x86 branch from 79a326d to d6047fd Compare October 8, 2026 03:03
@alex
alex added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit a262771 Oct 8, 2026
30 checks passed
@alex
alex deleted the claude/kind-heisenberg-okxdak-gf448-x86 branch October 8, 2026 03:23

alex commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main (9feca45, after #1252 and #1254 merged; no conflicts, no files shared with #1254). CI is green on d6047fd, including the x86 Benchmarks comparison, and a clean local lake build plus Emit.lean --check pass. Ready to be queued.


Generated by Claude Code

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