This PR carries the scalar reduction modernization originally proposed in #1951 and extends it with a 128-bit accumulator and branchless modular reduction in reduce_512. #1951 was closed in favor of this PR following formal verification.
Currently, secp256k1_scalar_reduce_512 tracks a 192-bit accumulator (acc, c2) across all reduction steps, even though limb multiplications involve only SECP256K1_N_C_0 (0x402da1732fc9bebf) and SECP256K1_N_C_1 (0x4551231950b75fc4). Because the sum of these constants is strictly bounded by ~0.522 * 2^64, the accumulator across both reduction phases peaks at ~0.52 * 2^128 (0x857ec48c80811e84ffffffffffffffff) under worst-case 512-bit inputs. The carry into c2 is unconditionally zero.
Additionally, Phase 3 previously wrote intermediate limbs to r->d, ran lexical comparisons in secp256k1_scalar_check_overflow, and executed a conditional addition pass in secp256k1_scalar_reduce.
This PR makes the following changes:
- Force inlining of 4x64
mul_512andreduce_512intosecp256k1_scalar_mul. - Disable x86_64 assembly by default, keeping it available via
--with-asm=x86_64and-DSECP256K1_ASM=x86_64. - Eliminate
c2fromreduce_512by switching all reduction accumulator calls to_fastmacros (muladd_fast,sumadd_fast,extract_fast) and removing the unusedsumaddmacro. - Replace Phase 3 and
secp256k1_scalar_reducewith the branchless 258-to-256 double-correction tail from #453 usingsecp256k1_uint128primitives, shared across both portable C and x86_64 assembly paths.
Correctness and formal proofs
The branchless reduction pre-increments p4 by 1 before reduction, folding an extra SECP256K1_N_C. Because X < 2N holds for all 258-bit intermediates in reduce_512, the carry out of limb 3 is strictly bounded to {0, 1} and acts as a canonicality selector. Decrementing p4 converts carry 0 into UINT64_MAX (adding N in the recovery pass) and carry 1 into 0 (no-op).
The head of this PR has been formally verified in VST (Verifiable C) by @remix7531 (https://github.com/remix7531/secp256k1/tree/fv-pr1951-1954), reducing the proof tree by 764 lines net compared to helper-based contracts.
Benchmarks
On AMD Zen 2 (Clang 22.1.8, -O2, 20M iterations), reduce_512 latency drops from 88.56 to 77.27 cycles/call (-13%).
On Apple M5 (AppleClang 21, -O2), medians of interleaved runs (bench_internal scalar at 1M iterations) measured by @fametrano:
| benchmark | master (asm) | #1951 | this PR | vs master | vs #1951 |
|---|---|---|---|---|---|
| scalar_mul | 0.0219 µs | 0.0175 µs | 0.0158 µs | -28% | -10% |
| scalar_split | 0.0909 µs | 0.0754 µs | 0.0706 µs | -22% | -6% |
On AMD Ryzen 9 7950X3D (Clang 23) measured by @fanquake:
| build | scalar_mul |
|---|---|
| master with asm | 0.0229 µs |
| master without asm | 0.0220 µs |
| this PR with asm | 0.0228 µs |
| this PR without asm | 0.0192 µs |