Repository navigation
x86 X448/Ed448: field arithmetic by calls of verified functions - #1257
Merged
Merged
Conversation
reaperhulk
added this pull request to stack #1266
October 8, 2026 02:36
Base automatically changed from
claude/kind-heisenberg-okxdak-gf448-spec
to
main
October 8, 2026 02:53
reaperhulk
force-pushed
the
claude/kind-heisenberg-okxdak-gf448-x86
branch
from
October 8, 2026 02:53
13ca193 to
79a326d
Compare
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
force-pushed
the
claude/kind-heisenberg-okxdak-gf448-x86
branch
from
October 8, 2026 03:03
79a326d to
d6047fd
Compare
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.rsabout 46k instructions anded448.rsabout 179k. This PR implements #1252's curve448 field functions on x86 and makes the callers call them.What
Impl/X448/X86.lean,Artifacts/Gf448R16/X86.lean):vg_gf448_r16_mul,_add,_suband_mul_a24.alimb and theblimbs 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.)Proof/X448/X86/Fn*.lean).vg_x448, and Ed448'svg_ed448_scalar_baseandvg_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 20withstack := 20.Exec.frameSp.Main.leanand Ed448'sBaseMain.leanare split into stage lemmas.Proof/X448/X86/{CallCT,Runs,CT}.lean,Proof/Ed448/X86/{BaseCT,VerifyCT}.lean):RelCT.callWith), using the callee's own constant time, rather than by one taint analysis through the callees' bodies.RelCT.loopwith the correctness invariants.RestCT.lean, the old single taint check of everything after the entry block, is deleted.Kit.callee_stk,Kit.callee_in). They already reserved those bytes (CalleeOk.stack ≤ 20).Spec/orTCB/changes.The registration file for the multiplication, addition and subtraction states
ofSigexplicitly. The defaultrflproof does not see throughbinContract's extra definition in #1252's spec. This is harmless, but it may be worth inliningbinContractthere.Size
Instruction lines in the generated x86 code:
x448.rsed448.rsgf448_r16.rs(new)Only
src/asm/x86/{x448,ed448,mod}.rsand the newgf448_r16.rschange undersrc/.Performance
CI's Benchmarks check ("Compare with base (x86)", i686, on the unrolled multiplication) shows every X448 and Ed448 benchmark faster than base:
ed448_keygen/57ed448_sign/64ed448_sign/1024ed448_sign/16384ed448_verify/64ed448_verify/1024ed448_verify/16384x448/56x448_generate/56x448_public_key/56x448_raw/56Local 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) andlake env lean --run Emit.lean --checkpass. The emitter's audits pass: standard axioms only, everyVG.Specdefinition inSpec/.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 lintandalgorithms_table.pyall pass.cargo fmt --checkpasses, andcargo clippy --all-targets -- -D warningspasses on x86-64 andi686-unknown-linux-gnu.cargo testwith Wycheproof passes natively and oni686-unknown-linux-gnu, in debug and release. That includes the RFC 7748/8032 and Wycheproof X448/Ed448 tests.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