crypto: Pseudo-Mersenne field multiplication for secp256k1 - #1649
Draft
chfast wants to merge 4 commits into
Draft
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1649 +/- ##
=======================================
Coverage 97.72% 97.72%
=======================================
Files 171 171
Lines 15631 15673 +42
Branches 3617 3622 +5
=======================================
+ Hits 15275 15317 +42
Misses 269 269
Partials 87 87
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
chfast
force-pushed
the
crypto/field-arith-specialization
branch
3 times, most recently
from
August 12, 2026 19:52
1aca914 to
a944895
Compare
The inversion folds the R² factor in by initializing the Bézout coefficient u to R² instead of 1, which is specific to inputs in the Montgomery form. Expose the underlying loop as inv_scaled() so a representation that needs a different initial value can reuse it, and keep inv() as the Montgomery-form wrapper.
For a modulus 2ⁿ-c with a single-word c the reduction of a double-width value is a fold of the high half instead of a division: h⋅2ⁿ + l ≡ l + h⋅c. It takes 21 single-word multiplications for a 256-bit modulus, where the Montgomery multiplication takes 36. Two of the carry foldings cannot be reached by any product of two canonical operands, at probabilities of about 2⁻¹⁹⁰ and 2⁻²²³, so the test constructs inputs for them directly rather than searching.
chfast
force-pushed
the
crypto/field-arith-specialization
branch
2 times, most recently
from
August 13, 2026 10:23
1370827 to
fdf923e
Compare
The modular arithmetic of a prime field can be implemented in more than one way depending on the structure of the modulus, and the choice also decides the internal representation of the value. Define the FieldArith concept for such a backend, wrap the Montgomery multiplication as the one implementing it, and let FieldElement delegate to the backend selected for its modulus. FieldElement no longer knows the representation, so the naming of the conversions loses the Montgomery reference. No functional change: every field still selects the Montgomery backend, and the instruction counts of ecrecover and ecmul are unchanged.
The secp256k1 field prime is 2²⁵⁶-2³²-977, so its multiplication can fold the high half of the product instead of running the Montgomery reduction. Add the backend doing that and select it from the structure of the modulus. Of the fields in use only this one qualifies, the rest keep the Montgomery form. Deriving the choice from the modulus value rather than declaring it per field keeps the two from ever disagreeing. Values are kept plain, which makes the conversions to and from the internal form free, but they cannot reduce an out-of-range input the way the conversion to the Montgomery form does, hence the added assertion. The reduction is roughly 2x faster where there is instruction-level parallelism to exploit, but it has the same latency as the Montgomery multiplication. So ecrecover, which is dominated by dependent multiplications, drops about a third of its instructions at unchanged cycles.
chfast
force-pushed
the
crypto/field-arith-specialization
branch
from
August 13, 2026 11:08
fdf923e to
5102be4
Compare
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.
Proof of concept: specialize the modular multiplication for the secp256k1 field prime,
which is the pseudo-Mersenne prime 2²⁵⁶-2³²-977, instead of running the generic Montgomery
CIOS loop on it.
The verdict up front: this is a wash on ecrecover cycles today. It is roughly 2x faster
where there is instruction-level parallelism to exploit, and exactly break-even where there
is not — and ecrecover turns out to be latency-bound. Opening it as a draft because the
mechanism is worth reviewing on its own terms and it is the prerequisite for the follow-ups
below.
What it does
For a modulus 2ⁿ-c with a single-word c, the product is reduced by folding the high half
instead of by Montgomery reduction, with values kept in the plain representation:
t, carry = l + h⋅c— one addmul pass over the high halfr, overflow = t + carry⋅c—carry ≤ c, so this product spans two words at mostif (overflow) r += c— cannot overflow againif (r >= Mod) r -= Mod— the excess is less than cBecause the representation is plain rather than Montgomery, the conversions to and from the
internal form become the identity, so constructing a field element and reading its value are
free.
The backend, and with it the representation, is selected from the value of the modulus,
never from a declaration:
0x1000003d1(33 bits)A hand-written tag would be redundant with the modulus and could be set inconsistently,
silently producing wrong arithmetic; deriving it cannot desync, and adding a curve needs no
knowledge of which backend to pick.
Numbers
Per-mul cost on the secp256k1 field, min of 5 reps, perf two-point subtraction:
ecrecover: -33.4% instructions, cycles unchanged (median 906k → 909k, IPC 1.88 → 1.18).
bn254 moves by -0.01% instructions, confirming the Montgomery path is untouched.
Multiply counts from the generated code: 21 for the pseudo-Mersenne reduction against 36 for
CIOS in general — but only 28 for this modulus, because three of its four words are all-ones
and the compiler strength-reduces
m⋅(2⁶⁴-1)to a shift and a subtract. So the baseline wasalready better than generic CIOS here.
Why ecrecover does not move
Per-mul latency is identical (~118 cycles, measured directly), and ecrecover's critical path is
a chain of dependent field multiplications — ~5000 of them, of which
field_sqrtalone is 253sequential squarings.
I first suspected generic
mulqwas serializing the parallel partial products through the fixedrdx:raxpair. Rebuilding both sides with-march=native, somulx/adcx/adoxareavailable, leaves ecrecover at -33.4% instructions / -0.3% cycles. Refuted — neither the
multiply count nor its encoding is what bounds ecrecover.
The most promising lever for ecrecover is therefore not arithmetic at all:
ecc::decompose()and the 4-way MSM already exist and are used by bn254, but
secp256k1::Curvedefines noLAMBDA/BETA/X1…, so ecrecover runs a full 256-doubling ladder. GLV would roughly halvethe critical path. After that, redundant limbs (5×52, how libsecp256k1 wins on latency too) and
a dedicated squaring for
FieldElement, which it lacks unlikeExtFieldElem.Correctness
udivremreference: zero mismatches,every result canonical. Corners, random canonical operands, operands near p, random raw
512-bit inputs, and crafted inputs for the rare branches.
~2⁻²²³), so neither ctest nor EEST will ever exercise them. The added unit test constructs
inputs for both, and it is mutation-tested: deleting either branch makes it fail on exactly
the intended input.
Montgomery form does, so
assert(v < Mod)now enforces the precondition. Note thatsecp256k1.cppdeliberately relies on the Montgomery conversion to reduce the message hashmod n — that still holds only because n is not pseudo-Mersenne.
Structure
Four commits, each independently reviewable and landable in order; the last is the optimization:
crypto: Let the binary GCD inversion start from a given coefficient— exposes the inversionloop as
inv_scaled()so a non-Montgomery representation can seed it with 1 instead of R².crypto: Add the pseudo-Mersenne modular reduction— the primitive and its tests, unused.crypto: Introduce the field arithmetic backend— theFieldArithconcept,MontArithimplementing it, and the per-modulus selection. No functional change: every field still
selects Montgomery, and the instruction counts of ecrecover and ecmul move by ≤0.02%.
crypto: Use the pseudo-Mersenne reduction for the secp256k1 field— addsPMArithand letsthe selection pick it.
FieldElementdelegates to the backend and contains no algorithm dispatch of its own, so it nolonger knows the internal representation. Two abstractions: the backend family and the element.
ModArithkeeps its name for now and becomes the runtime-modulus Montgomery backend later.Scope stops deliberately at two backends. An interface is validated by implementations that
differ along its axes, and these two differ on the representation axis — the one that produced
every constraint here (
inv_scaled, theto_internalprecondition). secp256r1 comes along freeas another
MontArithuser: a third user, not a third shape. Left for follow-ups, in order: theModArithrename, mandatorysqr(dead code without a real implementation), the sparse-modulusrefinement (which needs the CIOS loop extracted from
ModArith, touching every curve'smultiply), asm kernels with cpuid dispatch, and
SolinasArith.FpSpecis left exactly as it was; folding it away is a separate change.