perf: add dedicated squaring, mixed Jac+Affine addition, optimized doubling

Phase 1 optimizations from C code analysis:

1. Dedicated sqrWide: Exploits a[i]*a[j] symmetry — 36 inner products
   instead of 64. Reduces field squaring cost by ~40%.

2. Mixed Jacobian+Affine addition (addMixed): 8M+3S instead of 12M+4S.
   Saves 4 multiplications per addition when one operand is affine.
   Used for precomputed G table lookups during Shamir's trick.

3. Optimized point doubling (3M+4S via fe_half): Uses the (3/2)*X²
   formula from libsecp256k1, replacing a field multiplication with a
   cheap halving operation (carry-propagating right shift).

4. fe_half: Branchless divide-by-2 mod p, used by the new doubling formula.

5. AffinePoint type: Stores precomputed table entries as (x,y) without z,
   enabling mixed addition. G table now stored as affine.

6. U256.mulShift: 256x256→shift multiplication for future GLV scalar
   decomposition.

7. GLV infrastructure (straussGlvGP, scalarSplitLambda, wNAF, endomorphism
   constants): Implemented but not yet wired into the verify hot path due
   to sign-handling bugs being debugged. The 4-stream Strauss with GLV
   will halve doublings from 256→128 once the sign logic is fixed.

Benchmark: verifySchnorr 1,940 → 2,116 ops/s (~9% improvement)
The modest gain reflects that only G-side additions use mixed add;
P-side still uses full Jacobian. GLV will provide the next big jump.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
Claude
2026-04-05 15:42:14 +00:00
parent fbd145557e
commit 858cf2ea1a
2 changed files with 651 additions and 108 deletions
@@ -99,6 +99,92 @@ internal object U256 {
}
}
/**
* Dedicated squaring: out = a², written to out (size 16).
* Exploits symmetry: a[i]*a[j] == a[j]*a[i], computing 28 cross-products
* once and doubling, plus 8 diagonal products. Total: 36 multiplications
* vs 64 for generic mul.
*/
fun sqrWide(
out: IntArray,
a: IntArray,
) {
for (i in 0 until 16) out[i] = 0
// Pass 1: accumulate cross-products a[i]*a[j] for i < j (single, not doubled yet)
for (i in 0 until 8) {
var carry = 0L
val ai = a[i].toLong() and 0xFFFFFFFFL
for (j in i + 1 until 8) {
val prod = ai * (a[j].toLong() and 0xFFFFFFFFL) + (out[i + j].toLong() and 0xFFFFFFFFL) + carry
out[i + j] = prod.toInt()
carry = prod ushr 32
}
out[i + 8] = carry.toInt()
}
// Pass 2: double all cross-products (left shift by 1 bit)
var carry = 0
for (i in 1 until 16) {
val v = out[i]
out[i] = (v shl 1) or carry
carry = v ushr 31
}
// Pass 3: add diagonal products a[i]*a[i] at positions 2*i and 2*i+1
var dCarry = 0L
for (i in 0 until 8) {
val ai = a[i].toLong() and 0xFFFFFFFFL
val diag = ai * ai
val pos = 2 * i
dCarry += (out[pos].toLong() and 0xFFFFFFFFL) + (diag and 0xFFFFFFFFL)
out[pos] = dCarry.toInt()
dCarry = dCarry ushr 32
dCarry += (out[pos + 1].toLong() and 0xFFFFFFFFL) + (diag ushr 32)
out[pos + 1] = dCarry.toInt()
dCarry = dCarry ushr 32
}
}
/** 256x128 -> 384 bit multiply, return top portion shifted right by `shift` bits. */
fun mulShift(
k: IntArray,
g: IntArray,
shift: Int,
): IntArray {
val wide = IntArray(16)
mulWide(wide, k, g)
val wordShift = shift / 32
val bitShift = shift % 32
val result = IntArray(8)
for (i in 0 until 8) {
val srcIdx = i + wordShift
if (srcIdx < 16) {
result[i] = wide[srcIdx]
if (bitShift > 0 && srcIdx + 1 < 16) {
result[i] = (result[i] ushr bitShift) or (wide[srcIdx + 1] shl (32 - bitShift))
} else if (bitShift > 0) {
result[i] = result[i] ushr bitShift
}
}
}
// Rounding: check the bit just below the shift
if (shift > 0) {
val roundBitIdx = shift - 1
val roundWord = roundBitIdx / 32
val roundBit = (wide[roundWord] ushr (roundBitIdx % 32)) and 1
if (roundBit == 1) {
var c = 1L
for (i in 0 until 8) {
c += (result[i].toLong() and 0xFFFFFFFFL)
result[i] = c.toInt()
c = c ushr 32
}
}
}
return result
}
/** Convert big-endian 32-byte array to IntArray(8) little-endian limbs */
fun fromBytes(bytes: ByteArray): IntArray {
require(bytes.size == 32)
@@ -302,12 +388,37 @@ internal object FieldP {
reduceWide(out, w)
}
/** out = a² mod p. Uses thread-local scratch space. */
/** out = a² mod p. Uses dedicated squaring for ~40% fewer inner products. */
fun sqr(
out: IntArray,
a: IntArray,
) {
mul(out, a, a)
val w = wide.get()
U256.sqrWide(w, a)
reduceWide(out, w)
}
/** out = a / 2 mod p. If a is odd, add p first (since p is odd, a+p is even). */
fun half(
out: IntArray,
a: IntArray,
) {
val isOdd = a[0] and 1
// If odd, compute (a + p) / 2. If even, compute a / 2.
// Add p conditionally (branchless via mask)
val mask = (-isOdd).toLong() // all 1s if odd, all 0s if even
var carry = 0L
for (i in 0 until 8) {
carry += (a[i].toLong() and 0xFFFFFFFFL) + ((P[i].toLong() and 0xFFFFFFFFL) and mask)
out[i] = carry.toInt()
carry = carry ushr 32
}
// carry is 0 or 1 from the addition
// Now right-shift by 1 (the carry bit becomes the top bit)
for (i in 0 until 7) {
out[i] = (out[i] ushr 1) or (out[i + 1] shl 31)
}
out[7] = (out[7] ushr 1) or (carry.toInt() shl 31)
}
/** out = -a mod p */
@@ -56,10 +56,15 @@ internal class MutablePoint(
for (i in 1 until 8) z[i] = 0
}
/** Create a snapshot (immutable copy for table storage) */
fun snapshot(): MutablePoint = MutablePoint(x.copyOf(), y.copyOf(), z.copyOf())
}
/** Affine point stored as two IntArray(8). Used for precomputed tables. */
internal class AffinePoint(
val x: IntArray = IntArray(8),
val y: IntArray = IntArray(8),
)
internal object ECPoint {
val GX =
intArrayOf(
@@ -84,35 +89,132 @@ internal object ECPoint {
0x483ADA77.toInt(),
)
private val B = intArrayOf(7, 0, 0, 0, 0, 0, 0, 0)
private val ONE = intArrayOf(1, 0, 0, 0, 0, 0, 0, 0)
// Precomputed table for G: gTable[i] = (i+1)*G for i in 0..15
// Lazily initialized on first use.
private val gTable: Array<MutablePoint> by lazy { buildGTable() }
// GLV endomorphism: beta (cube root of unity in field, beta^3 = 1 mod p)
// lambda*P = (beta*P.x, P.y) for any point P on the curve
private val BETA =
intArrayOf(
0x719501EE.toInt(),
0xC1396C28.toInt(),
0x12F58995.toInt(),
0x9CF04975.toInt(),
0xAC3434E9.toInt(),
0x6E64479E.toInt(),
0x657C0710.toInt(),
0x7AE96A2B.toInt(),
)
private fun buildGTable(): Array<MutablePoint> {
val table = Array(16) { MutablePoint() }
// table[0] = G
table[0].setAffine(GX, GY)
// table[i] = table[i-1] + G
val tmp = MutablePoint()
// GLV scalar decomposition constants (from libsecp256k1)
// lambda: the scalar such that lambda*P = endomorphism(P)
private val LAMBDA =
intArrayOf(
0x1B23BD72.toInt(),
0xDF02967C.toInt(),
0x20816678.toInt(),
0x122E22EA.toInt(),
0x8812645A.toInt(),
0xA5261C02.toInt(),
0xC05C30E0.toInt(),
0x5363AD4C.toInt(),
)
// -lambda mod n
private val MINUS_LAMBDA =
intArrayOf(
0xB512F0CF.toInt(),
0xE0CF97D5.toInt(),
0x8F279763.toInt(),
0xA89CBA5C.toInt(),
0x77EDE0E7.toInt(),
0x09E86C02.toInt(),
0xEF481860.toInt(),
0x574B0E83.toInt(),
)
// g1 = round(2^384 * |b2| / n) for Babai rounding
private val G1 =
intArrayOf(
0xEB153DAB.toInt(),
0x90E49284.toInt(),
0x6BCDE86C.toInt(),
0xD221A7D4.toInt(),
0x00003086,
0,
0,
0,
)
// g2 = round(2^384 * |b1| / n)
private val G2 =
intArrayOf(
0xE4C42212.toInt(),
0x7FA90ABF.toInt(),
0x88286F54.toInt(),
0x7ED6010E.toInt(),
0x0000E443.toInt(),
0,
0,
0,
)
// -b1 mod n (b1 is negative, so this is |b1|)
private val MINUS_B1 =
intArrayOf(
0x0ABFE4C3.toInt(),
0x6F547FA9.toInt(),
0x010E8828.toInt(),
0xE4437ED6.toInt(),
0,
0,
0,
0,
)
// -b2 mod n
private val MINUS_B2 =
intArrayOf(
0x3DB1562C.toInt(),
0xD765CDA8.toInt(),
0x0774346D.toInt(),
0x8A280AC5.toInt(),
0xFFFFFFFE.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
)
// Precomputed G table: gTable[i] = (i+1)*G as affine points
private val gTable: Array<AffinePoint> by lazy { buildGTable() }
private fun buildGTable(): Array<AffinePoint> {
val jac = Array(16) { MutablePoint() }
jac[0].setAffine(GX, GY)
for (i in 1 until 16) {
addPoints(table[i], table[i - 1], table[0])
addPoints(jac[i], jac[i - 1], jac[0])
}
// Convert all to affine via batch-style (one inversion per point)
return Array(16) { i ->
val x = IntArray(8)
val y = IntArray(8)
toAffine(jac[i], x, y)
AffinePoint(x, y)
}
return Array(16) { table[it].snapshot() }
}
// ============ Scratch buffers for point operations (thread-local) ============
// Thread-local scratch
private class PointScratch {
val t = Array(12) { IntArray(8) } // temporary field elements
val dblCopy = MutablePoint() // copy buffer for in-place doubling
val t = Array(12) { IntArray(8) }
val dblCopy = MutablePoint()
}
private val scratch = ThreadLocal.withInitial { PointScratch() }
// ============ Optimized Point Doubling (3M + 4S via fe_half) ============
/**
* Point doubling: out = 2*p.
* Formula: dbl-2009-l from https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html
* Point doubling using the 3M+4S formula with fe_half.
* L = (3/2)*X², S = Y², T = -X*S
* X3 = L² + 2T, Y3 = -(L*(X3+T) + S²), Z3 = Y*Z
*/
fun doublePoint(
out: MutablePoint,
@@ -123,7 +225,6 @@ internal object ECPoint {
return
}
val s = scratch.get()
// If out aliases inp, copy inp to scratch first
val p =
if (out === inp) {
s.dblCopy.copyFrom(inp)
@@ -132,42 +233,98 @@ internal object ECPoint {
inp
}
val t = s.t
// t0=A=X², t1=B=Y², t2=C=B², t3=(X+B)²
FieldP.sqr(t[0], p.x)
FieldP.sqr(t[1], p.y)
FieldP.sqr(t[2], t[1])
FieldP.add(t[3], p.x, t[1])
FieldP.sqr(t[3], t[3])
// t3 = D = 2*((X+B)²-A-C)
FieldP.sub(t[3], t[3], t[0])
FieldP.sub(t[3], t[3], t[2])
FieldP.add(t[3], t[3], t[3]) // D
// t4 = E = 3*A
FieldP.add(t[4], t[0], t[0])
FieldP.add(t[4], t[4], t[0])
// t5 = F = E²
FieldP.sqr(t[5], t[4])
// X3 = F - 2*D
FieldP.add(t[6], t[3], t[3]) // 2D
FieldP.sub(out.x, t[5], t[6])
// Y3 = E*(D-X3) - 8*C
FieldP.sub(t[7], t[3], out.x)
FieldP.mul(t[7], t[4], t[7])
FieldP.add(t[2], t[2], t[2]) // 2C
FieldP.add(t[2], t[2], t[2]) // 4C
FieldP.add(t[2], t[2], t[2]) // 8C
FieldP.sub(out.y, t[7], t[2])
// Z3 = 2*Y*Z
FieldP.add(t[8], p.y, p.z)
FieldP.sqr(t[8], t[8])
FieldP.sub(t[8], t[8], t[1]) // -B
FieldP.sqr(t[9], p.z)
FieldP.sub(out.z, t[8], t[9])
// S = Y²
FieldP.sqr(t[0], p.y)
// L = (3/2)*X² = half(3*X²)
FieldP.sqr(t[1], p.x) // X²
FieldP.add(t[2], t[1], t[1]) // 2*X²
FieldP.add(t[2], t[2], t[1]) // 3*X²
FieldP.half(t[2], t[2]) // L = (3/2)*X²
// T = -X*S
FieldP.mul(t[3], p.x, t[0]) // X*S
FieldP.neg(t[3], t[3]) // T = -X*S
// X3 = L² + 2T
FieldP.sqr(out.x, t[2]) // L²
FieldP.add(out.x, out.x, t[3]) // + T
FieldP.add(out.x, out.x, t[3]) // + T
// Y3 = -(L*(X3+T) + S²)
FieldP.add(t[4], out.x, t[3]) // X3+T
FieldP.mul(t[4], t[2], t[4]) // L*(X3+T)
FieldP.sqr(t[5], t[0]) // S²
FieldP.add(t[4], t[4], t[5]) // L*(X3+T) + S²
FieldP.neg(out.y, t[4]) // negate
// Z3 = Y*Z
FieldP.mul(out.z, p.y, p.z)
}
// ============ Mixed Addition: Jacobian + Affine (8M + 3S) ============
/**
* Point addition: out = p + q. Handles p==q (doubling) and inverses.
* Mixed addition: out = p + (qx, qy) where q is affine (z=1).
* Saves 4M+1S vs full Jacobian addition.
*/
fun addMixed(
out: MutablePoint,
p: MutablePoint,
qx: IntArray,
qy: IntArray,
) {
if (p.isInfinity()) {
out.setAffine(qx, qy)
return
}
val s = scratch.get()
val t = s.t
// Z1² and Z1³
FieldP.sqr(t[0], p.z) // Z1²
FieldP.mul(t[1], t[0], p.z) // Z1³
// U2 = qx * Z1², S2 = qy * Z1³ (U1=X1, S1=Y1 since q.z=1)
FieldP.mul(t[2], qx, t[0]) // U2
FieldP.mul(t[3], qy, t[1]) // S2
// H = U2 - X1
FieldP.sub(t[4], t[2], p.x) // H
if (U256.isZero(t[4])) {
// U1 == U2, check S1 vs S2
val tmp = IntArray(8)
FieldP.sub(tmp, t[3], p.y)
if (U256.isZero(tmp)) {
doublePoint(out, p)
} else {
out.setInfinity()
}
return
}
// I = (2H)², J = H*I
FieldP.add(t[5], t[4], t[4]) // 2H
FieldP.sqr(t[5], t[5]) // I = (2H)²
FieldP.mul(t[6], t[4], t[5]) // J = H*I
// r = 2*(S2 - Y1)
FieldP.sub(t[7], t[3], p.y)
FieldP.add(t[7], t[7], t[7]) // r
// V = X1 * I
FieldP.mul(t[8], p.x, t[5]) // V
// X3 = r² - J - 2V
FieldP.sqr(out.x, t[7])
FieldP.sub(out.x, out.x, t[6])
FieldP.sub(out.x, out.x, t[8])
FieldP.sub(out.x, out.x, t[8])
// Y3 = r*(V - X3) - 2*Y1*J
FieldP.sub(t[9], t[8], out.x)
FieldP.mul(out.y, t[7], t[9])
FieldP.mul(t[9], p.y, t[6]) // Y1*J
FieldP.add(t[9], t[9], t[9]) // 2*Y1*J
FieldP.sub(out.y, out.y, t[9])
// Z3 = 2*Z1*H
FieldP.mul(out.z, p.z, t[4])
FieldP.add(out.z, out.z, out.z) // *2
}
// ============ Full Jacobian Addition (kept for table building) ============
fun addPoints(
out: MutablePoint,
p: MutablePoint,
@@ -184,14 +341,14 @@ internal object ECPoint {
val s = scratch.get()
val t = s.t
FieldP.sqr(t[0], p.z) // Z1²
FieldP.sqr(t[1], q.z) // Z2²
FieldP.mul(t[2], p.x, t[1]) // U1 = X1*Z2²
FieldP.mul(t[3], q.x, t[0]) // U2 = X2*Z1²
FieldP.mul(t[4], q.z, t[1]) // Z2³
FieldP.mul(t[4], p.y, t[4]) // S1 = Y1*Z2³
FieldP.mul(t[5], p.z, t[0]) // Z1³
FieldP.mul(t[5], q.y, t[5]) // S2 = Y2*Z1³
FieldP.sqr(t[0], p.z)
FieldP.sqr(t[1], q.z)
FieldP.mul(t[2], p.x, t[1])
FieldP.mul(t[3], q.x, t[0])
FieldP.mul(t[4], q.z, t[1])
FieldP.mul(t[4], p.y, t[4])
FieldP.mul(t[5], p.z, t[0])
FieldP.mul(t[5], q.y, t[5])
if (U256.cmp(t[2], t[3]) == 0) {
if (U256.cmp(t[4], t[5]) == 0) {
@@ -202,35 +359,326 @@ internal object ECPoint {
return
}
FieldP.sub(t[6], t[3], t[2]) // H = U2-U1
FieldP.add(t[7], t[6], t[6]) // 2H
FieldP.sqr(t[7], t[7]) // I = (2H)²
FieldP.mul(t[8], t[6], t[7]) // J = H*I
FieldP.sub(t[6], t[3], t[2])
FieldP.add(t[7], t[6], t[6])
FieldP.sqr(t[7], t[7])
FieldP.mul(t[8], t[6], t[7])
FieldP.sub(t[9], t[5], t[4])
FieldP.add(t[9], t[9], t[9]) // r = 2*(S2-S1)
FieldP.mul(t[10], t[2], t[7]) // V = U1*I
// X3 = r² - J - 2V
FieldP.add(t[9], t[9], t[9])
FieldP.mul(t[10], t[2], t[7])
FieldP.sqr(out.x, t[9])
FieldP.sub(out.x, out.x, t[8])
FieldP.sub(out.x, out.x, t[10])
FieldP.sub(out.x, out.x, t[10])
// Y3 = r*(V-X3) - 2*S1*J
FieldP.sub(t[11], t[10], out.x)
FieldP.mul(out.y, t[9], t[11])
FieldP.mul(t[11], t[4], t[8]) // S1*J
FieldP.add(t[11], t[11], t[11]) // 2*S1*J
FieldP.mul(t[11], t[4], t[8])
FieldP.add(t[11], t[11], t[11])
FieldP.sub(out.y, out.y, t[11])
// Z3 = ((Z1+Z2)²-Z1²-Z2²)*H
FieldP.add(out.z, p.z, q.z)
FieldP.sqr(out.z, out.z)
FieldP.sub(out.z, out.z, t[0])
FieldP.sub(out.z, out.z, t[1])
FieldP.mul(out.z, out.z, t[6])
}
// ============ wNAF Encoding ============
/** Convert scalar to width-w wNAF. Returns array of signed digits and the number of used bits. */
fun wnaf(
scalar: IntArray,
w: Int,
maxBits: Int,
): IntArray {
val result = IntArray(maxBits + 1)
// Work on a mutable copy
val s = scalar.copyOf()
var bit = 0
while (bit < maxBits) {
if (s[bit / 32] ushr (bit % 32) and 1 == 0) {
bit++
continue
}
// Extract w bits
var word = getBitsVar(s, bit, w.coerceAtMost(maxBits - bit))
if (word >= (1 shl (w - 1))) {
word -= (1 shl w)
// Propagate the borrow
addBitTo(s, bit + w)
}
result[bit] = word
bit += w
}
return result
}
private fun getBitsVar(
s: IntArray,
bitPos: Int,
count: Int,
): Int {
if (count == 0) return 0
val limb = bitPos / 32
val shift = bitPos % 32
var r = (s[limb] ushr shift)
if (shift + count > 32 && limb + 1 < s.size) {
r = r or (s[limb + 1] shl (32 - shift))
}
return r and ((1 shl count) - 1)
}
private fun addBitTo(
s: IntArray,
bitPos: Int,
) {
val limb = bitPos / 32
if (limb >= s.size) return
val bit = bitPos % 32
var carry = (1L shl bit)
for (i in limb until s.size) {
carry += (s[i].toLong() and 0xFFFFFFFFL)
s[i] = carry.toInt()
carry = carry ushr 32
if (carry == 0L) break
}
}
// ============ GLV Endomorphism ============
/** Apply endomorphism: (x, y) -> (beta*x, y) */
fun mulLambdaAffine(src: AffinePoint): AffinePoint {
val nx = IntArray(8)
FieldP.mul(nx, src.x, BETA)
return AffinePoint(nx, src.y.copyOf())
}
/**
* Scalar multiplication: out = scalar * p, using 4-bit windowed method.
* Split scalar k into k1, k2 such that k = k1 + k2*lambda (mod n),
* where |k1|, |k2| are ~128 bits. Returns (k1, k2, negK1, negK2)
* where negK1/negK2 indicate if the point should be negated.
*/
fun scalarSplitLambda(k: IntArray): SplitResult {
// c1 = round(k * g1 >> 384), c2 = round(k * g2 >> 384)
val c1 = U256.mulShift(k, G1, 384)
val c2 = U256.mulShift(k, G2, 384)
// r2 = c1*(-b1) + c2*(-b2) mod n
val c1b1 = ScalarN.mul(c1, MINUS_B1)
val c2b2 = ScalarN.mul(c2, MINUS_B2)
val r2 = ScalarN.add(c1b1, c2b2)
// r1 = k + r2*(-lambda) mod n = k - r2*lambda mod n
val r2lam = ScalarN.mul(r2, MINUS_LAMBDA)
val r1 = ScalarN.add(r2lam, k)
// If r1 or r2 > n/2, negate them (and we'll negate the corresponding point)
val negK1 = isHigh(r1)
val negK2 = isHigh(r2)
val k1 = if (negK1) ScalarN.neg(r1) else r1
val k2 = if (negK2) ScalarN.neg(r2) else r2
return SplitResult(k1, k2, negK1, negK2)
}
class SplitResult(
val k1: IntArray,
val k2: IntArray,
val negK1: Boolean,
val negK2: Boolean,
)
/** Check if scalar > n/2 */
private fun isHigh(s: IntArray): Boolean {
// n/2 = 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
// Simple check: if top bit of byte 31 (bit 255) is set, it's > n/2
// More precise: compare against n/2
val nHalf =
intArrayOf(
0x681B20A0.toInt(),
0xDFE92F46.toInt(),
0x57A4501D.toInt(),
0x5D576E73.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0x7FFFFFFF.toInt(),
)
return U256.cmp(s, nHalf) > 0
}
// ============ Strauss with GLV: s*G + e*P in one pass ============
/**
* Compute s*G + e*P using GLV endomorphism and interleaved wNAF (Strauss method).
* Splits each scalar into two 128-bit halves, processes 4 wNAF streams simultaneously.
* Uses mixed Jacobian+Affine addition with precomputed affine tables.
*/
fun straussGlvGP(
out: MutablePoint,
s: IntArray,
px: IntArray,
py: IntArray,
e: IntArray,
) {
val windowA = 5 // for arbitrary point P
val tableSize = 1 shl (windowA - 2) // 8 entries
// Split scalars via GLV
val sSplit = scalarSplitLambda(s)
val eSplit = scalarSplitLambda(e)
// Build wNAF for each half-scalar (128 bits)
val wnafS1 = wnaf(sSplit.k1, windowA, 129)
val wnafS2 = wnaf(sSplit.k2, windowA, 129)
val wnafE1 = wnaf(eSplit.k1, windowA, 129)
val wnafE2 = wnaf(eSplit.k2, windowA, 129)
// Build base table for P (no negation), then derive negated versions
val pTableBase = buildOddMultiplesTable(px, py, tableSize, false)
val pTable =
if (eSplit.negK1) {
Array(tableSize) { i ->
val ny = IntArray(8)
FieldP.neg(ny, pTableBase[i].y)
AffinePoint(pTableBase[i].x, ny)
}
} else {
pTableBase
}
// Build lambda(P) table from base (not pre-negated) table, then apply negK2
val pLamBase = Array(tableSize) { mulLambdaAffine(pTableBase[it]) }
val pLamTable =
if (eSplit.negK2) {
Array(tableSize) { i ->
val ny = IntArray(8)
FieldP.neg(ny, pLamBase[i].y)
AffinePoint(pLamBase[i].x, ny)
}
} else {
pLamBase
}
// G table: odd multiples [1G, 3G, 5G, ..., 15G] (base, un-negated)
val gOdd = Array(tableSize) { gTable[it * 2] }
val gOddSigned =
if (sSplit.negK1) {
Array(tableSize) { i ->
val ny = IntArray(8)
FieldP.neg(ny, gOdd[i].y)
AffinePoint(gOdd[i].x, ny)
}
} else {
gOdd
}
// Lambda(G) table: from un-negated base
val gLamOdd = Array(tableSize) { mulLambdaAffine(gOdd[it]) }
val gLamOddSigned =
if (sSplit.negK2) {
Array(tableSize) { i ->
val ny = IntArray(8)
FieldP.neg(ny, gLamOdd[i].y)
AffinePoint(gLamOdd[i].x, ny)
}
} else {
gLamOdd
}
// Handle negation from GLV split for s
// If negK1 for s: negate the G table entries y-coords (flip sign)
// If negK2 for s: negate the Glam table entries y-coords
// Simpler: we encode the negation into the wNAF lookup
// Find highest bit across all 4 wNAFs
var bits = 129
while (bits > 0 && wnafS1[bits - 1] == 0 && wnafS2[bits - 1] == 0 &&
wnafE1[bits - 1] == 0 && wnafE2[bits - 1] == 0
) {
bits--
}
out.setInfinity()
val tmp = MutablePoint()
for (i in bits - 1 downTo 0) {
doublePoint(out, out)
// Stream 1: s1 * G (sign baked into gOddSigned)
val n1 = wnafS1[i]
if (n1 != 0) {
val idx = (if (n1 > 0) n1 else -n1) / 2
addMixedWithSign(tmp, out, gOddSigned[idx], n1 < 0)
out.copyFrom(tmp)
}
// Stream 2: s2 * lambda(G) (sign baked into gLamOddSigned)
val n2 = wnafS2[i]
if (n2 != 0) {
val idx = (if (n2 > 0) n2 else -n2) / 2
addMixedWithSign(tmp, out, gLamOddSigned[idx], n2 < 0)
out.copyFrom(tmp)
}
// Stream 3: e1 * P (sign baked into pTable)
val n3 = wnafE1[i]
if (n3 != 0) {
val idx = (if (n3 > 0) n3 else -n3) / 2
addMixedWithSign(tmp, out, pTable[idx], n3 < 0)
out.copyFrom(tmp)
}
// Stream 4: e2 * lambda(P) (sign baked into pLamTable)
val n4 = wnafE2[i]
if (n4 != 0) {
val idx = (if (n4 > 0) n4 else -n4) / 2
addMixedWithSign(tmp, out, pLamTable[idx], n4 < 0)
out.copyFrom(tmp)
}
}
}
/** Add affine point with optional y-negation */
private fun addMixedWithSign(
out: MutablePoint,
p: MutablePoint,
q: AffinePoint,
negateQ: Boolean,
) {
if (negateQ) {
val negY = IntArray(8)
FieldP.neg(negY, q.y)
addMixed(out, p, q.x, negY)
} else {
addMixed(out, p, q.x, q.y)
}
}
/** Build table of odd multiples [1,3,5,...,(2*n-1)]*P as affine points */
private fun buildOddMultiplesTable(
px: IntArray,
py: IntArray,
n: Int,
negate: Boolean,
): Array<AffinePoint> {
val p = MutablePoint()
p.setAffine(px, py)
// 2*P for stepping
val p2 = MutablePoint()
doublePoint(p2, p)
val jPoints = Array(n) { MutablePoint() }
jPoints[0].copyFrom(p) // 1*P
for (i in 1 until n) {
addPoints(jPoints[i], jPoints[i - 1], p2) // (2i+1)*P
}
// Convert to affine
return Array(n) { i ->
val x = IntArray(8)
val y = IntArray(8)
toAffine(jPoints[i], x, y)
if (negate) FieldP.neg(y, y)
AffinePoint(x, y)
}
}
// ============ Generic scalar multiplication (for non-verify paths) ============
fun mul(
out: MutablePoint,
p: MutablePoint,
@@ -240,19 +688,14 @@ internal object ECPoint {
out.setInfinity()
return
}
// Build 4-bit window table: table[i] = (i+1)*p for i in 0..15
// Build 4-bit window table (16 entries, Jacobian)
val table = Array(16) { MutablePoint() }
table[0].copyFrom(p)
val tmp = MutablePoint()
for (i in 1 until 16) {
addPoints(table[i], table[i - 1], p)
}
for (i in 1 until 16) addPoints(table[i], table[i - 1], p)
out.setInfinity()
// Process 4 bits at a time, MSB first (64 nibbles for 256 bits)
val tmp = MutablePoint()
for (nibbleIdx in 63 downTo 0) {
// 4 doublings
doublePoint(out, out)
doublePoint(out, out)
doublePoint(out, out)
@@ -265,10 +708,6 @@ internal object ECPoint {
}
}
/**
* G multiplication using precomputed table: out = scalar * G.
* Uses 4-bit windowed method with static precomputed table.
*/
fun mulG(
out: MutablePoint,
scalar: IntArray,
@@ -277,7 +716,7 @@ internal object ECPoint {
out.setInfinity()
return
}
val table = gTable // force lazy init
val table = gTable
out.setInfinity()
val tmp = MutablePoint()
for (nibbleIdx in 63 downTo 0) {
@@ -287,51 +726,48 @@ internal object ECPoint {
doublePoint(out, out)
val nib = U256.getNibble(scalar, nibbleIdx)
if (nib != 0) {
addPoints(tmp, out, table[nib - 1])
addMixed(tmp, out, table[nib - 1].x, table[nib - 1].y)
out.copyFrom(tmp)
}
}
}
/**
* Shamir's trick: out = s*G + e*P in a single pass.
* Much faster than computing s*G and e*P separately for verification.
*/
fun mulDoubleG(
out: MutablePoint,
s: IntArray,
p: MutablePoint,
e: IntArray,
) {
// Build 4-bit window table for P
val pTable = Array(16) { MutablePoint() }
pTable[0].copyFrom(p)
for (i in 1 until 16) {
addPoints(pTable[i], pTable[i - 1], p)
}
// Shamir's trick: s*G + e*P in one pass with 4-bit windows
// G table: precomputed affine (mixed addition = 8M+3S)
// P table: Jacobian on-the-fly (full addition = 11M+5S, but no inversion cost)
val gTab = gTable
val tmp = MutablePoint()
val pTab = Array(16) { MutablePoint() }
pTab[0].copyFrom(p)
for (i in 1 until 16) addPoints(pTab[i], pTab[i - 1], pTab[0])
out.setInfinity()
val tmp = MutablePoint()
for (nibbleIdx in 63 downTo 0) {
doublePoint(out, out)
doublePoint(out, out)
doublePoint(out, out)
doublePoint(out, out)
val sNib = U256.getNibble(s, nibbleIdx)
val eNib = U256.getNibble(e, nibbleIdx)
if (sNib != 0) {
addPoints(tmp, out, gTab[sNib - 1])
addMixed(tmp, out, gTab[sNib - 1].x, gTab[sNib - 1].y)
out.copyFrom(tmp)
}
val eNib = U256.getNibble(e, nibbleIdx)
if (eNib != 0) {
addPoints(tmp, out, pTable[eNib - 1])
addPoints(tmp, out, pTab[eNib - 1])
out.copyFrom(tmp)
}
}
}
/** Convert Jacobian to affine. Writes x, y into outX, outY. Returns false if infinity. */
// ============ Coordinate conversion and serialization ============
fun toAffine(
p: MutablePoint,
outX: IntArray,
@@ -349,7 +785,6 @@ internal object ECPoint {
return true
}
/** Lift x-coordinate to even-y point. Returns false if not on curve. */
fun liftX(
outX: IntArray,
outY: IntArray,
@@ -358,18 +793,16 @@ internal object ECPoint {
if (U256.cmp(x, FieldP.P) >= 0) return false
val t = IntArray(8)
FieldP.sqr(t, x)
FieldP.mul(t, t, x) // x³
FieldP.add(t, t, B) // x³+7
FieldP.mul(t, t, x)
FieldP.add(t, t, B)
if (!FieldP.sqrt(outY, t)) return false
U256.copyInto(outX, x)
// Ensure even y
if (outY[0] and 1 != 0) FieldP.neg(outY, outY)
return true
}
fun hasEvenY(y: IntArray): Boolean = y[0] and 1 == 0
/** Parse serialized public key -> affine (outX, outY). Returns false on failure. */
fun parsePublicKey(
pubkey: ByteArray,
outX: IntArray,
@@ -434,7 +867,6 @@ internal object ECPoint {
return r
}
// Convenience wrappers for non-hot paths
fun toAffinePair(p: MutablePoint): Pair<IntArray, IntArray>? {
val x = IntArray(8)
val y = IntArray(8)