perf: eliminate ThreadLocal.get() from hot paths — 27-52% faster across all EC ops

FieldP.mul/sqr were calling ThreadLocal.get() for every invocation (~500+
times per scalar multiplication, ~20-30ns each on JVM). Point operations
(doublePoint, addMixed, addPoints) each did an additional ThreadLocal.get()
for their scratch buffers.

Fix: add overloads that accept a pre-fetched wide buffer (LongArray(8))
and PointScratch. Top-level entry points (mulG, mul, mulDoubleG) fetch
the ThreadLocal once and thread it through all inner calls.

Results (ops/s, vs native JNI):
- pubkeyCreate:       19,163 → 29,205 (+52%, 3.0x → 2.2x)
- signSchnorr cached: 13,007 → 18,397 (+41%, 2.1x → 1.5x)
- signSchnorr:         5,365 →  7,490 (+40%, 5.7x → 3.7x)
- verifySchnorr:       3,840 →  4,873 (+27%, 7.2x → 5.4x)
- ECDH:                5,569 →  7,870 (+41%, 5.5x → 3.8x)

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
Claude
2026-04-06 13:38:01 +00:00
parent 36a7ae147e
commit 547be89577
2 changed files with 208 additions and 130 deletions
@@ -22,7 +22,10 @@ package com.vitorpamplona.quartz.utils.secp256k1
/**
* Arithmetic modulo the secp256k1 field prime: p = 2^256 - 2^32 - 977.
* Uses LongArray(4) limbs (4×64-bit). Thread-local LongArray(8) scratch for mul/sqr.
* Uses LongArray(4) limbs (4×64-bit).
*
* Hot-path mul/sqr accept a pre-fetched LongArray(8) wide buffer to avoid
* ThreadLocal.get() overhead (~20-30ns per call, 500+ calls per scalar mul).
*/
internal object FieldP {
// p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
@@ -36,6 +39,9 @@ internal object FieldP {
private val wide = ThreadLocal.withInitial { LongArray(8) }
/** Get a thread-local wide buffer. Call once at the top-level entry point, then pass through. */
fun getWide(): LongArray = wide.get()
// ==================== Core arithmetic ====================
fun add(
@@ -69,6 +75,7 @@ internal object FieldP {
if (borrow != 0) U256.addTo(out, out, P)
}
/** Multiply with ThreadLocal wide buffer (convenience for non-hot paths). */
fun mul(
out: LongArray,
a: LongArray,
@@ -79,6 +86,18 @@ internal object FieldP {
reduceWide(out, w)
}
/** Multiply with caller-provided wide buffer (hot path — no ThreadLocal lookup). */
fun mul(
out: LongArray,
a: LongArray,
b: LongArray,
w: LongArray,
) {
U256.mulWide(w, a, b)
reduceWide(out, w)
}
/** Square with ThreadLocal wide buffer (convenience for non-hot paths). */
fun sqr(
out: LongArray,
a: LongArray,
@@ -88,6 +107,16 @@ internal object FieldP {
reduceWide(out, w)
}
/** Square with caller-provided wide buffer (hot path — no ThreadLocal lookup). */
fun sqr(
out: LongArray,
a: LongArray,
w: LongArray,
) {
U256.sqrWide(w, a)
reduceWide(out, w)
}
fun neg(
out: LongArray,
a: LongArray,
@@ -131,6 +160,7 @@ internal object FieldP {
a: LongArray,
) {
require(!U256.isZero(a))
val w = wide.get()
val x2 = LongArray(4)
val x3 = LongArray(4)
val x6 = LongArray(4)
@@ -143,43 +173,44 @@ internal object FieldP {
val x220 = LongArray(4)
val x223 = LongArray(4)
sqr(x2, a)
mul(x2, x2, a)
sqr(x3, x2)
mul(x3, x3, a)
sqrN(x6, x3, 3)
mul(x6, x6, x3)
sqrN(x9, x6, 3)
mul(x9, x9, x3)
sqrN(x11, x9, 2)
mul(x11, x11, x2)
sqrN(x22, x11, 11)
mul(x22, x22, x11)
sqrN(x44, x22, 22)
mul(x44, x44, x22)
sqrN(x88, x44, 44)
mul(x88, x88, x44)
sqrN(x176, x88, 88)
mul(x176, x176, x88)
sqrN(x220, x176, 44)
mul(x220, x220, x44)
sqrN(x223, x220, 3)
mul(x223, x223, x3)
sqr(x2, a, w)
mul(x2, x2, a, w)
sqr(x3, x2, w)
mul(x3, x3, a, w)
sqrN(x6, x3, 3, w)
mul(x6, x6, x3, w)
sqrN(x9, x6, 3, w)
mul(x9, x9, x3, w)
sqrN(x11, x9, 2, w)
mul(x11, x11, x2, w)
sqrN(x22, x11, 11, w)
mul(x22, x22, x11, w)
sqrN(x44, x22, 22, w)
mul(x44, x44, x22, w)
sqrN(x88, x44, 44, w)
mul(x88, x88, x44, w)
sqrN(x176, x88, 88, w)
mul(x176, x176, x88, w)
sqrN(x220, x176, 44, w)
mul(x220, x220, x44, w)
sqrN(x223, x220, 3, w)
mul(x223, x223, x3, w)
sqrN(out, x223, 23)
mul(out, out, x22)
sqrN(out, out, 5)
mul(out, out, a)
sqrN(out, out, 3)
mul(out, out, x2)
sqrN(out, out, 2)
mul(out, out, a)
sqrN(out, x223, 23, w)
mul(out, out, x22, w)
sqrN(out, out, 5, w)
mul(out, out, a, w)
sqrN(out, out, 3, w)
mul(out, out, x2, w)
sqrN(out, out, 2, w)
mul(out, out, a, w)
}
fun sqrt(
out: LongArray,
a: LongArray,
): Boolean {
val w = wide.get()
val x2 = LongArray(4)
val x3 = LongArray(4)
val x6 = LongArray(4)
@@ -192,43 +223,53 @@ internal object FieldP {
val x220 = LongArray(4)
val x223 = LongArray(4)
sqr(x2, a)
mul(x2, x2, a)
sqr(x3, x2)
mul(x3, x3, a)
sqrN(x6, x3, 3)
mul(x6, x6, x3)
sqrN(x9, x6, 3)
mul(x9, x9, x3)
sqrN(x11, x9, 2)
mul(x11, x11, x2)
sqrN(x22, x11, 11)
mul(x22, x22, x11)
sqrN(x44, x22, 22)
mul(x44, x44, x22)
sqrN(x88, x44, 44)
mul(x88, x88, x44)
sqrN(x176, x88, 88)
mul(x176, x176, x88)
sqrN(x220, x176, 44)
mul(x220, x220, x44)
sqrN(x223, x220, 3)
mul(x223, x223, x3)
sqr(x2, a, w)
mul(x2, x2, a, w)
sqr(x3, x2, w)
mul(x3, x3, a, w)
sqrN(x6, x3, 3, w)
mul(x6, x6, x3, w)
sqrN(x9, x6, 3, w)
mul(x9, x9, x3, w)
sqrN(x11, x9, 2, w)
mul(x11, x11, x2, w)
sqrN(x22, x11, 11, w)
mul(x22, x22, x11, w)
sqrN(x44, x22, 22, w)
mul(x44, x44, x22, w)
sqrN(x88, x44, 44, w)
mul(x88, x88, x44, w)
sqrN(x176, x88, 88, w)
mul(x176, x176, x88, w)
sqrN(x220, x176, 44, w)
mul(x220, x220, x44, w)
sqrN(x223, x220, 3, w)
mul(x223, x223, x3, w)
sqrN(out, x223, 23)
mul(out, out, x22)
sqrN(out, out, 6)
mul(out, out, x2)
sqrN(out, out, 2)
sqrN(out, x223, 23, w)
mul(out, out, x22, w)
sqrN(out, out, 6, w)
mul(out, out, x2, w)
sqrN(out, out, 2, w)
val check = LongArray(4)
mul(check, out, out)
mul(check, out, out, w)
val ar = LongArray(4)
U256.copyInto(ar, a)
reduceSelf(ar)
return U256.cmp(check, ar) == 0
}
private fun sqrN(
out: LongArray,
a: LongArray,
n: Int,
w: LongArray,
) {
U256.copyInto(out, a)
repeat(n) { sqr(out, out, w) }
}
private fun sqrN(
out: LongArray,
a: LongArray,
@@ -242,18 +242,27 @@ internal object ECPoint {
/**
* Scratch space for point operations. Each thread gets its own set of temporary
* field elements to avoid allocation in the inner loops. The 12 temp buffers
* (t[0]..t[11]) are shared across doublePoint and addPoints — this is safe because
* these functions only call each other in the equal-point degenerate case, which
* returns immediately after the recursive call without using the temps further.
* field elements and a wide buffer to avoid allocation and ThreadLocal lookups
* in the inner loops. The 12 temp buffers (t[0]..t[11]) are shared across
* doublePoint and addPoints — this is safe because these functions only call
* each other in the equal-point degenerate case, which returns immediately
* after the recursive call without using the temps further.
*
* The wide buffer (LongArray(8)) is pre-fetched once per top-level operation
* and passed through to FieldP.mul/sqr, avoiding ~500+ ThreadLocal.get() calls
* per scalar multiplication (~20-30ns each on JVM).
*/
private class PointScratch {
internal class PointScratch {
val t = Array(12) { LongArray(4) }
val dblCopy = MutablePoint() // Copy buffer for in-place doubling (out === input)
val w = LongArray(8) // Wide buffer for FieldP.mul/sqr — shared, avoids ThreadLocal
}
private val scratch = ThreadLocal.withInitial { PointScratch() }
/** Get thread-local scratch. Call once at the top-level entry point. */
internal fun getScratch(): PointScratch = scratch.get()
// ==================== Point Doubling (3M + 4S) ====================
/**
@@ -269,15 +278,22 @@ internal object ECPoint {
*
* Safe for out === inp (in-place doubling) via internal copy buffer.
*/
/** doublePoint with ThreadLocal scratch (convenience for non-hot paths). */
fun doublePoint(
out: MutablePoint,
inp: MutablePoint,
) = doublePoint(out, inp, scratch.get())
/** doublePoint with caller-provided scratch (hot path — no ThreadLocal lookup). */
fun doublePoint(
out: MutablePoint,
inp: MutablePoint,
s: PointScratch,
) {
if (inp.isInfinity()) {
out.setInfinity()
return
}
val s = scratch.get()
val p =
if (out === inp) {
s.dblCopy.copyFrom(inp)
@@ -286,23 +302,24 @@ internal object ECPoint {
inp
}
val t = s.t
val w = s.w
FieldP.sqr(t[0], p.y) // S = Y²
FieldP.sqr(t[1], p.x) // X²
FieldP.sqr(t[0], p.y, w) // S = Y²
FieldP.sqr(t[1], p.x, w) // 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²
FieldP.mul(t[3], p.x, t[0]) // X·S
FieldP.mul(t[3], p.x, t[0], w) // X·S
FieldP.neg(t[3], t[3]) // T = -X·S
FieldP.sqr(out.x, t[2]) // X₃ = L²
FieldP.sqr(out.x, t[2], w) // X₃ = L²
FieldP.add(out.x, out.x, t[3]) // + T
FieldP.add(out.x, out.x, t[3]) // + T
FieldP.add(t[4], out.x, t[3]) // X₃ + T
FieldP.mul(t[4], t[2], t[4]) // L·(X₃+T)
FieldP.sqr(t[5], t[0]) // S²
FieldP.mul(t[4], t[2], t[4], w) // L·(X₃+T)
FieldP.sqr(t[5], t[0], w) // S²
FieldP.add(t[4], t[4], t[5]) // L·(X₃+T) + S²
FieldP.neg(out.y, t[4]) // Y₃ = negate
FieldP.mul(out.z, p.y, p.z) // Z₃ = Y·Z
FieldP.mul(out.z, p.y, p.z, w) // Z₃ = Y·Z
}
// ==================== Mixed Addition: Jacobian + Affine (8M + 3S) ====================
@@ -318,49 +335,58 @@ internal object ECPoint {
*
* Handles degenerate cases: p is infinity, or p equals/negates q.
*/
/** addMixed with ThreadLocal scratch (convenience for non-hot paths). */
fun addMixed(
out: MutablePoint,
p: MutablePoint,
qx: LongArray,
qy: LongArray,
) = addMixed(out, p, qx, qy, scratch.get())
/** addMixed with caller-provided scratch (hot path — no ThreadLocal lookup). */
fun addMixed(
out: MutablePoint,
p: MutablePoint,
qx: LongArray,
qy: LongArray,
s: PointScratch,
) {
if (p.isInfinity()) {
out.setAffine(qx, qy)
return
}
val s = scratch.get()
val t = s.t
val w = s.w
FieldP.sqr(t[0], p.z) // Z₁²
FieldP.mul(t[1], t[0], p.z) // Z₁³
FieldP.mul(t[2], qx, t[0]) // U₂ = qx·Z₁² (U₁ = X₁ since Z₂=1)
FieldP.mul(t[3], qy, t[1]) // S₂ = qy·Z₁³ (S₁ = Y₁ since Z₂=1)
FieldP.sqr(t[0], p.z, w) // Z₁²
FieldP.mul(t[1], t[0], p.z, w) // Z₁³
FieldP.mul(t[2], qx, t[0], w) // U₂ = qx·Z₁² (U₁ = X₁ since Z₂=1)
FieldP.mul(t[3], qy, t[1], w) // S₂ = qy·Z₁³ (S₁ = Y₁ since Z₂=1)
FieldP.sub(t[4], t[2], p.x) // H = U₂ - U₁
if (U256.isZero(t[4])) {
// Same x-coordinate: either same point (double) or inverse (infinity)
val tmp = LongArray(4)
FieldP.sub(tmp, t[3], p.y)
if (U256.isZero(tmp)) doublePoint(out, p) else out.setInfinity()
if (U256.isZero(tmp)) doublePoint(out, p, s) else out.setInfinity()
return
}
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
FieldP.sqr(t[5], t[5], w) // I = (2H)²
FieldP.mul(t[6], t[4], t[5], w) // J = H·I
FieldP.sub(t[7], t[3], p.y)
FieldP.add(t[7], t[7], t[7]) // r = 2·(S₂ - S₁)
FieldP.mul(t[8], p.x, t[5]) // V = U₁·I
FieldP.sqr(out.x, t[7]) // X₃ = r²
FieldP.mul(t[8], p.x, t[5], w) // V = U₁·I
FieldP.sqr(out.x, t[7], w) // X₃ = r²
FieldP.sub(out.x, out.x, t[6]) // - J
FieldP.sub(out.x, out.x, t[8]) // - V
FieldP.sub(out.x, out.x, t[8]) // - V
FieldP.sub(t[9], t[8], out.x) // V - X₃
FieldP.mul(out.y, t[7], t[9]) // Y₃ = r·(V-X₃)
FieldP.mul(t[9], p.y, t[6]) // - 2·S₁·J
FieldP.mul(out.y, t[7], t[9], w) // Y₃ = r·(V-X₃)
FieldP.mul(t[9], p.y, t[6], w) // - 2·S₁·J
FieldP.add(t[9], t[9], t[9])
FieldP.sub(out.y, out.y, t[9])
FieldP.mul(out.z, p.z, t[4]) // Z₃ = 2·Z₁·H
FieldP.mul(out.z, p.z, t[4], w) // Z₃ = 2·Z₁·H
FieldP.add(out.z, out.z, out.z)
}
@@ -378,6 +404,13 @@ internal object ECPoint {
out: MutablePoint,
p: MutablePoint,
q: MutablePoint,
) = addPoints(out, p, q, scratch.get())
fun addPoints(
out: MutablePoint,
p: MutablePoint,
q: MutablePoint,
s: PointScratch,
) {
if (p.isInfinity()) {
out.copyFrom(q)
@@ -387,45 +420,45 @@ internal object ECPoint {
out.copyFrom(p)
return
}
val s = scratch.get()
val t = s.t
val w = s.w
FieldP.sqr(t[0], p.z) // Z₁²
FieldP.sqr(t[1], q.z) // Z₂²
FieldP.mul(t[2], p.x, t[1]) // U₁ = X₁·Z₂²
FieldP.mul(t[3], q.x, t[0]) // U₂ = X₂·Z₁²
FieldP.mul(t[4], q.z, t[1]) // Z₂³
FieldP.mul(t[4], p.y, t[4]) // S₁ = Y₁·Z₂³
FieldP.mul(t[5], p.z, t[0]) // Z₁³
FieldP.mul(t[5], q.y, t[5]) // S₂ = Y₂·Z₁³
FieldP.sqr(t[0], p.z, w) // Z₁²
FieldP.sqr(t[1], q.z, w) // Z₂²
FieldP.mul(t[2], p.x, t[1], w) // U₁ = X₁·Z₂²
FieldP.mul(t[3], q.x, t[0], w) // U₂ = X₂·Z₁²
FieldP.mul(t[4], q.z, t[1], w) // Z₂³
FieldP.mul(t[4], p.y, t[4], w) // S₁ = Y₁·Z₂³
FieldP.mul(t[5], p.z, t[0], w) // Z₁³
FieldP.mul(t[5], q.y, t[5], w) // S₂ = Y₂·Z₁³
if (U256.cmp(t[2], t[3]) == 0) {
if (U256.cmp(t[4], t[5]) == 0) doublePoint(out, p) else out.setInfinity()
if (U256.cmp(t[4], t[5]) == 0) doublePoint(out, p, s) else out.setInfinity()
return
}
FieldP.sub(t[6], t[3], t[2]) // H = U₂ - U₁
FieldP.add(t[7], t[6], t[6])
FieldP.sqr(t[7], t[7]) // I = (2H)²
FieldP.mul(t[8], t[6], t[7]) // J = H·I
FieldP.sqr(t[7], t[7], w) // I = (2H)²
FieldP.mul(t[8], t[6], t[7], w) // J = H·I
FieldP.sub(t[9], t[5], t[4])
FieldP.add(t[9], t[9], t[9]) // r = 2·(S₂-S₁)
FieldP.mul(t[10], t[2], t[7]) // V = U₁·I
FieldP.mul(t[10], t[2], t[7], w) // V = U₁·I
FieldP.sqr(out.x, t[9])
FieldP.sqr(out.x, t[9], w)
FieldP.sub(out.x, out.x, t[8])
FieldP.sub(out.x, out.x, t[10])
FieldP.sub(out.x, out.x, t[10])
FieldP.sub(t[11], t[10], out.x)
FieldP.mul(out.y, t[9], t[11])
FieldP.mul(t[11], t[4], t[8])
FieldP.mul(out.y, t[9], t[11], w)
FieldP.mul(t[11], t[4], t[8], w)
FieldP.add(t[11], t[11], t[11])
FieldP.sub(out.y, out.y, t[11])
FieldP.add(out.z, p.z, q.z)
FieldP.sqr(out.z, out.z)
FieldP.sqr(out.z, out.z, w)
FieldP.sub(out.z, out.z, t[0])
FieldP.sub(out.z, out.z, t[1])
FieldP.mul(out.z, out.z, t[6])
FieldP.mul(out.z, out.z, t[6], w)
}
// ==================== Scalar Multiplication ====================
@@ -449,26 +482,27 @@ internal object ECPoint {
return
}
val w = 5
val tableSize = 1 shl (w - 2) // 8 entries
val s = scratch.get()
val wnd = 5
val tableSize = 1 shl (wnd - 2) // 8 entries
// Split scalar via GLV: scalar = k₁ + k₂·λ
val split = Glv.splitScalar(scalar)
val wnaf1 = Glv.wnaf(split.k1, w, 129)
val wnaf2 = Glv.wnaf(split.k2, w, 129)
val wnaf1 = Glv.wnaf(split.k1, wnd, 129)
val wnaf2 = Glv.wnaf(split.k2, wnd, 129)
// P odd-multiples [1P, 3P, 5P, ..., 15P] via 2P stepping
val p2 = MutablePoint()
doublePoint(p2, p)
doublePoint(p2, p, s)
val pOdd = Array(tableSize) { MutablePoint() }
pOdd[0].copyFrom(p)
for (i in 1 until tableSize) addPoints(pOdd[i], pOdd[i - 1], p2)
for (i in 1 until tableSize) addPoints(pOdd[i], pOdd[i - 1], p2, s)
// λ(P) odd-multiples: (β·X, Y, Z) in Jacobian
val pLamOdd =
Array(tableSize) { i ->
val lp = MutablePoint()
FieldP.mul(lp.x, pOdd[i].x, Glv.BETA)
FieldP.mul(lp.x, pOdd[i].x, Glv.BETA, s.w)
pOdd[i].y.copyInto(lp.y)
pOdd[i].z.copyInto(lp.z)
lp
@@ -487,9 +521,9 @@ internal object ECPoint {
val negJac = MutablePoint()
for (i in bits - 1 downTo 0) {
doublePoint(out, out)
addWnafJacobian(out, tmp, negJac, wnaf1, i, pOdd, split.negK1)
addWnafJacobian(out, tmp, negJac, wnaf2, i, pLamOdd, split.negK2)
doublePoint(out, out, s)
addWnafJacobian(out, tmp, negJac, wnaf1, i, pOdd, split.negK1, s)
addWnafJacobian(out, tmp, negJac, wnaf2, i, pLamOdd, split.negK2, s)
}
}
@@ -512,13 +546,14 @@ internal object ECPoint {
return
}
val s = scratch.get()
val table = combTable
out.setInfinity()
val tmp = MutablePoint()
for (combOff in COMB_SPACING - 1 downTo 0) {
if (combOff < COMB_SPACING - 1) {
doublePoint(out, out)
doublePoint(out, out, s)
}
for (block in 0 until COMB_BLOCKS) {
var mask = 0
@@ -530,7 +565,7 @@ internal object ECPoint {
}
if (mask != 0) {
val entry = table[block * COMB_POINTS + mask]
addMixed(tmp, out, entry.x, entry.y)
addMixed(tmp, out, entry.x, entry.y, s)
out.copyFrom(tmp)
}
}
@@ -555,6 +590,7 @@ internal object ECPoint {
p: MutablePoint,
e: LongArray,
) {
val sc = scratch.get()
val wP = 5 // Window for P-side (table built per-call, keep small)
val pTableSize = 1 shl (wP - 2) // 8 entries for P
@@ -574,15 +610,15 @@ internal object ECPoint {
// P odd-multiples [1P, 3P, 5P, ..., 15P] via 2P stepping (1 double + 7 adds)
val p2 = MutablePoint()
doublePoint(p2, p)
doublePoint(p2, p, sc)
val pOdd = Array(pTableSize) { MutablePoint() }
pOdd[0].copyFrom(p)
for (i in 1 until pTableSize) addPoints(pOdd[i], pOdd[i - 1], p2)
for (i in 1 until pTableSize) addPoints(pOdd[i], pOdd[i - 1], p2, sc)
// λ(P) table: (β·X, Y, Z) in Jacobian — endomorphism preserves projective coords
val pLamOdd =
Array(pTableSize) { i ->
val lp = MutablePoint()
FieldP.mul(lp.x, pOdd[i].x, Glv.BETA)
FieldP.mul(lp.x, pOdd[i].x, Glv.BETA, sc.w)
pOdd[i].y.copyInto(lp.y)
pOdd[i].z.copyInto(lp.z)
lp
@@ -600,16 +636,16 @@ internal object ECPoint {
out.setInfinity()
val tmp = MutablePoint()
val negY = LongArray(4)
val negJac = MutablePoint() // Reused scratch for Jacobian negation
val negJac = MutablePoint()
for (i in bits - 1 downTo 0) {
doublePoint(out, out)
doublePoint(out, out, sc)
// Streams 1-2: G-side (affine tables, mixed addition)
addWnafMixed(out, tmp, negY, wnafS1, i, gOdd, sSplit.negK1)
addWnafMixed(out, tmp, negY, wnafS2, i, gLam, sSplit.negK2)
addWnafMixed(out, tmp, negY, wnafS1, i, gOdd, sSplit.negK1, sc)
addWnafMixed(out, tmp, negY, wnafS2, i, gLam, sSplit.negK2, sc)
// Streams 3-4: P-side (Jacobian tables, full addition)
addWnafJacobian(out, tmp, negJac, wnafE1, i, pOdd, eSplit.negK1)
addWnafJacobian(out, tmp, negJac, wnafE2, i, pLamOdd, eSplit.negK2)
addWnafJacobian(out, tmp, negJac, wnafE1, i, pOdd, eSplit.negK1, sc)
addWnafJacobian(out, tmp, negJac, wnafE2, i, pLamOdd, eSplit.negK2, sc)
}
}
@@ -626,6 +662,7 @@ internal object ECPoint {
bitIndex: Int,
table: Array<AffinePoint>,
glvNeg: Boolean,
s: PointScratch,
) {
if (bitIndex >= wnafDigits.size) return
val d = wnafDigits[bitIndex]
@@ -633,10 +670,10 @@ internal object ECPoint {
val idx = (if (d > 0) d else -d) / 2
val effectiveNeg = (d < 0) xor glvNeg
if (!effectiveNeg) {
addMixed(tmp, out, table[idx].x, table[idx].y)
addMixed(tmp, out, table[idx].x, table[idx].y, s)
} else {
FieldP.neg(negY, table[idx].y)
addMixed(tmp, out, table[idx].x, negY)
addMixed(tmp, out, table[idx].x, negY, s)
}
out.copyFrom(tmp)
}
@@ -650,6 +687,7 @@ internal object ECPoint {
bitIndex: Int,
table: Array<MutablePoint>,
glvNeg: Boolean,
s: PointScratch,
) {
if (bitIndex >= wnafDigits.size) return
val d = wnafDigits[bitIndex]
@@ -657,13 +695,12 @@ internal object ECPoint {
val idx = (if (d > 0) d else -d) / 2
val effectiveNeg = (d < 0) xor glvNeg
if (!effectiveNeg) {
addPoints(tmp, out, table[idx])
addPoints(tmp, out, table[idx], s)
} else {
// Negate the Jacobian point: (X, -Y, Z) using pre-allocated scratch
table[idx].x.copyInto(negScratch.x)
FieldP.neg(negScratch.y, table[idx].y)
table[idx].z.copyInto(negScratch.z)
addPoints(tmp, out, negScratch)
addPoints(tmp, out, negScratch, s)
}
out.copyFrom(tmp)
}