revert: remove safegcd — Fermat chain is faster on JVM
The safegcd (Bernstein-Yang divsteps) algorithm is faster than Fermat in C due to native 128-bit integer support, but on JVM the 128-bit arithmetic overhead via multiplyHigh + carry tracking in the inner loop (12 rounds × matrix multiply on 5 limbs) is slower than the Fermat addition chain (255 sqr + 15 mul of optimized field ops). Benchmark showed 8.3x vs native (was 5.0x with Fermat), confirming that the per-operation constant factor matters more than algorithmic complexity for this problem size on JVM. https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
@@ -158,18 +158,6 @@ internal object FieldP {
|
||||
fun inv(
|
||||
out: LongArray,
|
||||
a: LongArray,
|
||||
) {
|
||||
require(!U256.isZero(a))
|
||||
ModInv.modinv(out, a)
|
||||
}
|
||||
|
||||
/**
|
||||
* Modular inverse using Fermat's little theorem: a^(p-2) mod p.
|
||||
* Kept as reference/fallback. Uses 255 squarings + 15 multiplications.
|
||||
*/
|
||||
fun invFermat(
|
||||
out: LongArray,
|
||||
a: LongArray,
|
||||
) {
|
||||
require(!U256.isZero(a))
|
||||
val w = wide.get()
|
||||
|
||||
@@ -1,409 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils.secp256k1
|
||||
|
||||
// =====================================================================================
|
||||
// SAFEGCD-BASED MODULAR INVERSE FOR secp256k1 FIELD ELEMENTS
|
||||
// =====================================================================================
|
||||
//
|
||||
// Implements the Bernstein-Yang (2019) "divsteps" algorithm for computing modular
|
||||
// inverses. This is significantly faster than the Fermat approach (a^(p-2) mod p)
|
||||
// which requires 255 squarings + 15 multiplications.
|
||||
//
|
||||
// The algorithm maintains (f, g, d, e) with invariants:
|
||||
// d * input ≡ f (mod p)
|
||||
// e * input ≡ g (mod p)
|
||||
// and iteratively reduces g toward 0 via divstep transitions. After ~741 steps,
|
||||
// g = 0 and f = ±1, so d = ±input^{-1}.
|
||||
//
|
||||
// Steps are batched in groups of 62, processing the bottom 62 bits of f, g to
|
||||
// produce a 2×2 transition matrix, then applying it to the full-precision values.
|
||||
// This requires 12 rounds × 62 steps = 744 ≥ 741 needed for 256-bit modulus.
|
||||
//
|
||||
// Based on bitcoin-core/secp256k1's modinv64 implementation.
|
||||
// =====================================================================================
|
||||
|
||||
internal object ModInv {
|
||||
private const val M62 = (1L shl 62) - 1 // 0x3FFFFFFFFFFFFFFF
|
||||
|
||||
// p^{-1} mod 2^62, computed via Hensel lifting at class init.
|
||||
// Used to compute the correction factor in updateDE.
|
||||
private val P_INV_62: Long =
|
||||
run {
|
||||
val pLo = FieldP.P[0] // low 64 bits of p
|
||||
var x = 1L // p is odd so p*1 ≡ 1 (mod 2)
|
||||
// Each iteration doubles the number of correct bits
|
||||
x *= 2 - pLo * x // mod 2^2
|
||||
x *= 2 - pLo * x // mod 2^4
|
||||
x *= 2 - pLo * x // mod 2^8
|
||||
x *= 2 - pLo * x // mod 2^16
|
||||
x *= 2 - pLo * x // mod 2^32
|
||||
x *= 2 - pLo * x // mod 2^64
|
||||
x and M62
|
||||
}
|
||||
|
||||
// p as 5×62-bit signed limbs (little-endian, each limb in [0, 2^62))
|
||||
private val P62: LongArray = toLimbs62(FieldP.P)
|
||||
|
||||
/**
|
||||
* Compute modular inverse: out = a^{-1} mod p using safegcd.
|
||||
*/
|
||||
fun modinv(
|
||||
out: LongArray,
|
||||
a: LongArray,
|
||||
) {
|
||||
// f = p, g = a (as 5×62-bit signed limbs)
|
||||
val f = toLimbs62(FieldP.P)
|
||||
val g = toLimbs62(a)
|
||||
// d = 0, e = 1 (as 5×62-bit signed limbs, values mod p)
|
||||
val d = LongArray(5)
|
||||
val e = longArrayOf(1, 0, 0, 0, 0)
|
||||
|
||||
var delta = 1L
|
||||
|
||||
// 12 rounds of 62 divsteps = 744 total (need ≥ 741 for 256-bit modulus)
|
||||
for (round in 0 until 12) {
|
||||
// Bottom 64 bits of f and g for the inner loop
|
||||
val fBot = f[0] or (f[1] shl 62)
|
||||
val gBot = g[0] or (g[1] shl 62)
|
||||
|
||||
// Run 62 divsteps on the truncated values, get transition matrix
|
||||
val t = divsteps62var(delta, fBot, gBot)
|
||||
delta = t.delta
|
||||
|
||||
// Apply matrix to full-precision (f, g) and (d, e)
|
||||
updateFG(f, g, t)
|
||||
updateDE(d, e, t)
|
||||
}
|
||||
|
||||
// At this point g ≈ 0, f = ±1.
|
||||
// d * a ≡ f (mod p), so if f = 1 then d = a^{-1}
|
||||
// if f = -1, negate d
|
||||
normalize5(d)
|
||||
if (isNegativeOne(f)) {
|
||||
negateMod(d)
|
||||
}
|
||||
fromLimbs62(out, d)
|
||||
FieldP.reduceSelf(out)
|
||||
}
|
||||
|
||||
// ==================== Transition Matrix ====================
|
||||
|
||||
private class Trans(
|
||||
val delta: Long,
|
||||
val u: Long,
|
||||
val v: Long,
|
||||
val q: Long,
|
||||
val r: Long,
|
||||
)
|
||||
|
||||
/**
|
||||
* Variable-time 62 divsteps on bottom 64 bits of f, g.
|
||||
* Returns the transition matrix [u v; q r] and updated delta.
|
||||
*
|
||||
* Invariant maintained:
|
||||
* u * f_orig + v * g_orig = f_current * 2^steps_done
|
||||
* q * f_orig + r * g_orig = g_current * 2^steps_done
|
||||
*/
|
||||
private fun divsteps62var(
|
||||
delta: Long,
|
||||
f0: Long,
|
||||
g0: Long,
|
||||
): Trans {
|
||||
var d = delta
|
||||
var f = f0
|
||||
var g = g0
|
||||
var u = 1L
|
||||
var v = 0L
|
||||
var q = 0L
|
||||
var r = 1L
|
||||
var steps = 62
|
||||
|
||||
while (steps > 0) {
|
||||
if (g == 0L) {
|
||||
u = u shl steps
|
||||
v = v shl steps
|
||||
d += steps
|
||||
break
|
||||
}
|
||||
|
||||
// Count and skip trailing zeros in g
|
||||
val zeros = g.countTrailingZeroBits().coerceAtMost(steps)
|
||||
if (zeros > 0) {
|
||||
g = g shr zeros // arithmetic shift
|
||||
u = u shl zeros
|
||||
v = v shl zeros
|
||||
d += zeros
|
||||
steps -= zeros
|
||||
if (steps == 0) break
|
||||
}
|
||||
|
||||
// g is odd. Apply divstep.
|
||||
if (d > 0) {
|
||||
// Swap: f_new = g, g_new = (g - f)/2
|
||||
val tU = u
|
||||
val tV = v
|
||||
val tF = f
|
||||
u = 2 * q
|
||||
v = 2 * r
|
||||
q = q - tU
|
||||
r = r - tV
|
||||
f = g
|
||||
g = (g - tF) shr 1
|
||||
d = 1 - d
|
||||
} else {
|
||||
// No swap: g_new = (g + f)/2
|
||||
q += u
|
||||
r += v
|
||||
u *= 2
|
||||
v *= 2
|
||||
g = (g + f) shr 1
|
||||
d += 1
|
||||
}
|
||||
steps--
|
||||
}
|
||||
|
||||
return Trans(d, u, v, q, r)
|
||||
}
|
||||
|
||||
// ==================== Matrix Application ====================
|
||||
|
||||
/**
|
||||
* Apply transition matrix to (f, g): [f,g] = [u,v; q,r] * [f,g] / 2^62
|
||||
* Uses 128-bit signed arithmetic via multiplyHigh.
|
||||
*/
|
||||
private fun updateFG(
|
||||
f: LongArray,
|
||||
g: LongArray,
|
||||
t: Trans,
|
||||
) {
|
||||
// Save originals (both newF and newG depend on original f, g)
|
||||
val of = f.copyOf()
|
||||
val og = g.copyOf()
|
||||
|
||||
// f_new = (u*of + v*og) / 2^62
|
||||
updateRow(f, of, og, t.u, t.v)
|
||||
|
||||
// g_new = (q*of + r*og) / 2^62
|
||||
updateRow(g, of, og, t.q, t.r)
|
||||
}
|
||||
|
||||
// Computes out = (s1*a + s2*b) / 2^62 using 128-bit accumulation
|
||||
private fun updateRow(
|
||||
out: LongArray,
|
||||
a: LongArray,
|
||||
b: LongArray,
|
||||
s1: Long,
|
||||
s2: Long,
|
||||
) {
|
||||
// Accumulate limb-by-limb with 128-bit carry
|
||||
var cLo = 0L
|
||||
var cHi = 0L
|
||||
|
||||
for (i in 0 until 5) {
|
||||
// acc = s1*a[i] + s2*b[i] + carry (128-bit signed)
|
||||
var sLo = s1 * a[i]
|
||||
var sHi = multiplyHigh(s1, a[i])
|
||||
val bLo = s2 * b[i]
|
||||
val bHi = multiplyHigh(s2, b[i])
|
||||
|
||||
// sLo:sHi += bLo:bHi
|
||||
val prevSLo = sLo
|
||||
sLo += bLo
|
||||
sHi += bHi
|
||||
if (sLo.toULong() < prevSLo.toULong()) sHi++
|
||||
|
||||
// += carry
|
||||
val prevSLo2 = sLo
|
||||
sLo += cLo
|
||||
if (sLo.toULong() < prevSLo2.toULong()) sHi++
|
||||
sHi += cHi
|
||||
|
||||
if (i == 0) {
|
||||
// Low 62 bits should be zero. Just shift right by 62.
|
||||
cLo = (sLo ushr 62) or (sHi shl 2)
|
||||
cHi = sHi shr 62 // arithmetic
|
||||
} else {
|
||||
out[i - 1] = sLo and M62
|
||||
cLo = (sLo ushr 62) or (sHi shl 2)
|
||||
cHi = sHi shr 62
|
||||
}
|
||||
}
|
||||
out[4] = cLo
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply transition matrix to (d, e) modulo p: [d,e] = [u,v; q,r] * [d,e] / 2^62 mod p.
|
||||
*
|
||||
* The division by 2^62 is exact after adding a suitable multiple of p to make
|
||||
* the low 62 bits zero. This uses the precomputed P_INV_62 = p^{-1} mod 2^62.
|
||||
*/
|
||||
private fun updateDE(
|
||||
d: LongArray,
|
||||
e: LongArray,
|
||||
t: Trans,
|
||||
) {
|
||||
val od = d.copyOf()
|
||||
val oe = e.copyOf()
|
||||
|
||||
updateRowDE(d, od, oe, t.u, t.v)
|
||||
updateRowDE(e, od, oe, t.q, t.r)
|
||||
}
|
||||
|
||||
private fun updateRowDE(
|
||||
out: LongArray,
|
||||
a: LongArray,
|
||||
b: LongArray,
|
||||
s1: Long,
|
||||
s2: Long,
|
||||
) {
|
||||
// Step 1: compute the low 62 bits of s1*a + s2*b
|
||||
val mdLo = (s1 * a[0] + s2 * b[0]) and M62
|
||||
|
||||
// Step 2: compute correction factor so that (s1*a + s2*b + cd*p) ≡ 0 (mod 2^62)
|
||||
val cd = (-mdLo * P_INV_62) and M62
|
||||
|
||||
// Step 3: accumulate s1*a[i] + s2*b[i] + cd*p[i], then shift right by 62
|
||||
var cLo = 0L
|
||||
var cHi = 0L
|
||||
|
||||
for (i in 0 until 5) {
|
||||
// acc = s1*a[i] + s2*b[i] + cd*P62[i] + carry
|
||||
var sLo = s1 * a[i]
|
||||
var sHi = multiplyHigh(s1, a[i])
|
||||
|
||||
// += s2*b[i]
|
||||
var tLo = s2 * b[i]
|
||||
var tHi = multiplyHigh(s2, b[i])
|
||||
var prev = sLo
|
||||
sLo += tLo
|
||||
sHi += tHi
|
||||
if (sLo.toULong() < prev.toULong()) sHi++
|
||||
|
||||
// += cd*P62[i]
|
||||
tLo = cd * P62[i]
|
||||
tHi = multiplyHigh(cd, P62[i])
|
||||
prev = sLo
|
||||
sLo += tLo
|
||||
sHi += tHi
|
||||
if (sLo.toULong() < prev.toULong()) sHi++
|
||||
|
||||
// += carry
|
||||
prev = sLo
|
||||
sLo += cLo
|
||||
if (sLo.toULong() < prev.toULong()) sHi++
|
||||
sHi += cHi
|
||||
|
||||
if (i == 0) {
|
||||
// Low 62 bits are now zero by construction. Shift right.
|
||||
cLo = (sLo ushr 62) or (sHi shl 2)
|
||||
cHi = sHi shr 62
|
||||
} else {
|
||||
out[i - 1] = sLo and M62
|
||||
cLo = (sLo ushr 62) or (sHi shl 2)
|
||||
cHi = sHi shr 62
|
||||
}
|
||||
}
|
||||
out[4] = cLo
|
||||
}
|
||||
|
||||
// ==================== Limb Conversion ====================
|
||||
|
||||
// Convert 4×64-bit unsigned to 5×62-bit unsigned limbs
|
||||
private fun toLimbs62(a: LongArray): LongArray {
|
||||
val r = LongArray(5)
|
||||
r[0] = a[0] and M62
|
||||
r[1] = ((a[0] ushr 62) or (a[1] shl 2)) and M62
|
||||
r[2] = ((a[1] ushr 60) or (a[2] shl 4)) and M62
|
||||
r[3] = ((a[2] ushr 58) or (a[3] shl 6)) and M62
|
||||
r[4] = a[3] ushr 56
|
||||
return r
|
||||
}
|
||||
|
||||
// Convert 5×62-bit signed limbs to 4×64-bit unsigned
|
||||
// Assumes value is in [0, p) after normalization.
|
||||
private fun fromLimbs62(
|
||||
out: LongArray,
|
||||
a: LongArray,
|
||||
) {
|
||||
out[0] = (a[0] and M62) or (a[1] shl 62)
|
||||
out[1] = (a[1] ushr 2) or (a[2] shl 60)
|
||||
out[2] = (a[2] ushr 4) or (a[3] shl 58)
|
||||
out[3] = (a[3] ushr 6) or (a[4] shl 56)
|
||||
}
|
||||
|
||||
// Normalize 5×62-bit signed limbs: propagate carries so all limbs are in [0, 2^62)
|
||||
private fun normalize5(a: LongArray) {
|
||||
for (i in 0 until 4) {
|
||||
val carry = a[i] shr 62 // arithmetic shift (preserves sign)
|
||||
a[i] = a[i] and M62
|
||||
a[i + 1] += carry
|
||||
}
|
||||
// If a[4] is negative, the entire number is negative → add p
|
||||
if (a[4] < 0) {
|
||||
addP62(a)
|
||||
// Normalize again
|
||||
for (i in 0 until 4) {
|
||||
val carry = a[i] shr 62
|
||||
a[i] = a[i] and M62
|
||||
a[i + 1] += carry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if 5×62-bit limb number equals -1 (f should be ±1 at the end)
|
||||
private fun isNegativeOne(f: LongArray): Boolean {
|
||||
// -1 in 5×62-bit signed: all limbs = M62 (i.e., 2^62-1) except possibly the top
|
||||
// More robust: normalize and check if limb[0..3] are M62 and limb[4] = M62
|
||||
// Or just check the sign of the number
|
||||
// After normalization, f should be exactly 1 or p-1 (which is ≡ -1 mod p)
|
||||
// A simpler check: if the value is p-1, it means f was -1
|
||||
// For now, just check if f is negative before normalization:
|
||||
// Sum the value: the sign is determined by the highest non-zero limb
|
||||
val norm = f.copyOf()
|
||||
for (i in 0 until 4) {
|
||||
val carry = norm[i] shr 62
|
||||
norm[i] = norm[i] and M62
|
||||
norm[i + 1] += carry
|
||||
}
|
||||
return norm[4] < 0 || (norm[4] == 0L && norm[3] == 0L && norm[2] == 0L && norm[1] == 0L && norm[0] < 0)
|
||||
}
|
||||
|
||||
// Negate d modulo p: d = p - d
|
||||
private fun negateMod(d: LongArray) {
|
||||
var borrow = 0L
|
||||
for (i in 0 until 5) {
|
||||
val diff = P62[i] - d[i] - borrow
|
||||
d[i] = diff and M62
|
||||
borrow = -(diff shr 62) // 0 or 1
|
||||
}
|
||||
}
|
||||
|
||||
// Add p (in 62-bit limbs) to a
|
||||
private fun addP62(a: LongArray) {
|
||||
var carry = 0L
|
||||
for (i in 0 until 5) {
|
||||
val sum = a[i] + P62[i] + carry
|
||||
a[i] = sum and M62
|
||||
carry = sum shr 62
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user