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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user