perf: reuse pre-allocated scratch for all entry-point allocations
Pool toAffine/toAffineX temp LongArrays (zInv, zInv2, zInv3) into PointScratch. Add scratch-aware overloads that hot paths now use, keeping allocating convenience overloads for one-time init paths (buildCombTable). Consolidate verify/sign/pubCreate/ECDH scratch into shared entry* fields in PointScratch (entryPx, entryPy, entryPoint, entryResult, entryTmp, entryTmp2). This replaces the old verify-specific fields (verifyPx/Py/R/S/E/Rx/Ry/PPoint/Result) with fewer, shared buffers. Update all Secp256k1 entry points (pubkeyCreate, signSchnorr, signSchnorrInternal, verifySchnorr, pubKeyTweakMul, ecdhXOnly) to use scratch instead of per-call allocations. Add KeyCodec.liftX overload accepting a temp buffer to avoid 1 LongArray allocation per call. Eliminate 2 longArrayOf allocations in ScalarN.reduceWideTo by reusing the output array as temporary storage for hi limbs. https://claude.ai/code/session_017UbWduFi1sLUsgVUMUH2nx
This commit is contained in:
@@ -855,11 +855,7 @@ internal object ECPoint {
|
||||
|
||||
// ==================== Coordinate Conversion ====================
|
||||
|
||||
/**
|
||||
* Convert from Jacobian (X, Y, Z) to affine (x, y) = (X/Z², Y/Z³).
|
||||
* Requires one field inversion (the most expensive single operation).
|
||||
* Returns false if the point is at infinity.
|
||||
*/
|
||||
/** Convert Jacobian → affine (convenience, allocates temps). For one-time init paths. */
|
||||
fun toAffine(
|
||||
p: MutablePoint,
|
||||
outX: LongArray,
|
||||
@@ -877,21 +873,32 @@ internal object ECPoint {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from Jacobian to affine, returning only the x-coordinate: x = X/Z².
|
||||
* Saves 2 multiplications vs full toAffine (no zInv3, no outY computation).
|
||||
* Used by ecdhXOnly where only the x-coordinate of the shared point is needed.
|
||||
*/
|
||||
/** Convert Jacobian → affine using pre-allocated scratch (hot path). */
|
||||
fun toAffine(
|
||||
p: MutablePoint,
|
||||
outX: LongArray,
|
||||
outY: LongArray,
|
||||
s: PointScratch,
|
||||
): Boolean {
|
||||
if (p.isInfinity()) return false
|
||||
FieldP.inv(s.zInv, p.z)
|
||||
FieldP.sqr(s.zInv2, s.zInv)
|
||||
FieldP.mul(s.zInv3, s.zInv2, s.zInv)
|
||||
FieldP.mul(outX, p.x, s.zInv2)
|
||||
FieldP.mul(outY, p.y, s.zInv3)
|
||||
return true
|
||||
}
|
||||
|
||||
/** Convert Jacobian → affine x-only using pre-allocated scratch (hot path). */
|
||||
fun toAffineX(
|
||||
p: MutablePoint,
|
||||
outX: LongArray,
|
||||
s: PointScratch,
|
||||
): Boolean {
|
||||
if (p.isInfinity()) return false
|
||||
val zInv = LongArray(4)
|
||||
val zInv2 = LongArray(4)
|
||||
FieldP.inv(zInv, p.z)
|
||||
FieldP.sqr(zInv2, zInv)
|
||||
FieldP.mul(outX, p.x, zInv2)
|
||||
FieldP.inv(s.zInv, p.z)
|
||||
FieldP.sqr(s.zInv2, s.zInv)
|
||||
FieldP.mul(outX, p.x, s.zInv2)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,23 @@ internal object KeyCodec {
|
||||
return true
|
||||
}
|
||||
|
||||
/** liftX with caller-provided temp buffer (avoids 1 LongArray alloc). */
|
||||
fun liftX(
|
||||
outX: LongArray,
|
||||
outY: LongArray,
|
||||
x: LongArray,
|
||||
tmp: LongArray,
|
||||
): Boolean {
|
||||
if (U256.cmp(x, FieldP.P) >= 0) return false
|
||||
FieldP.sqr(tmp, x)
|
||||
FieldP.mul(tmp, tmp, x)
|
||||
FieldP.add(tmp, tmp, B)
|
||||
if (!FieldP.sqrt(outY, tmp)) return false
|
||||
U256.copyInto(outX, x)
|
||||
if (outY[0] and 1L != 0L) FieldP.neg(outY, outY)
|
||||
return true
|
||||
}
|
||||
|
||||
/** Check if y-coordinate is even (LSB = 0). */
|
||||
fun hasEvenY(y: LongArray): Boolean = y[0] and 1L == 0L
|
||||
|
||||
|
||||
+12
-10
@@ -127,14 +127,16 @@ internal class PointScratch {
|
||||
val splitK1 = LongArray(4) // output k1
|
||||
val splitK2 = LongArray(4) // output k2
|
||||
|
||||
// Pre-allocated scratch for verifySchnorr (avoids per-call allocations)
|
||||
val verifyPx = LongArray(4)
|
||||
val verifyPy = LongArray(4)
|
||||
val verifyR = LongArray(4)
|
||||
val verifyS = LongArray(4)
|
||||
val verifyE = LongArray(4)
|
||||
val verifyRx = LongArray(4)
|
||||
val verifyRy = LongArray(4)
|
||||
val verifyPPoint = MutablePoint()
|
||||
val verifyResult = MutablePoint()
|
||||
// Pre-allocated scratch for toAffine / toAffineX (avoids 3 LongArray allocs per call)
|
||||
val zInv = LongArray(4)
|
||||
val zInv2 = LongArray(4)
|
||||
val zInv3 = LongArray(4)
|
||||
|
||||
// Pre-allocated scratch for Secp256k1 entry points (avoids per-call allocations)
|
||||
val entryPx = LongArray(4) // liftX / parsePublicKey output
|
||||
val entryPy = LongArray(4)
|
||||
val entryPoint = MutablePoint() // pubkeyCreate, signSchnorr, ecdhXOnly
|
||||
val entryResult = MutablePoint() // mulG / mul output
|
||||
val entryTmp = LongArray(4) // liftX temp, auxrand XOR, nonce, etc.
|
||||
val entryTmp2 = LongArray(4) // secondary temp for signSchnorr R-point
|
||||
}
|
||||
|
||||
@@ -173,14 +173,14 @@ internal object ScalarN {
|
||||
val lo1 = w[1]
|
||||
val lo2 = w[2]
|
||||
val lo3 = w[3]
|
||||
val hi0 = w[4]
|
||||
val hi1 = w[5]
|
||||
val hi2 = w[6]
|
||||
val hi3 = w[7]
|
||||
val hiArr = longArrayOf(hi0, hi1, hi2, hi3)
|
||||
// Use `out` as temporary storage for hi limbs (avoids longArrayOf allocation)
|
||||
out[0] = w[4]
|
||||
out[1] = w[5]
|
||||
out[2] = w[6]
|
||||
out[3] = w[7]
|
||||
|
||||
val hiTimesNC = w // reuse w as scratch
|
||||
U256.mulWide(hiTimesNC, hiArr, N_COMPLEMENT)
|
||||
U256.mulWide(hiTimesNC, out, N_COMPLEMENT)
|
||||
|
||||
// sum = hiTimesNC + lo
|
||||
var carry = 0L
|
||||
@@ -213,14 +213,17 @@ internal object ScalarN {
|
||||
return
|
||||
}
|
||||
|
||||
// Round 2
|
||||
val hi2Arr = longArrayOf(w[4], w[5], w[6], w[7])
|
||||
// Round 2: reuse out for hi2 limbs (avoids longArrayOf allocation)
|
||||
out[0] = w[4]
|
||||
out[1] = w[5]
|
||||
out[2] = w[6]
|
||||
out[3] = w[7]
|
||||
val saved0 = w[0]
|
||||
val saved1 = w[1]
|
||||
val saved2 = w[2]
|
||||
val saved3 = w[3]
|
||||
val hi2NC = w
|
||||
U256.mulWide(hi2NC, hi2Arr, N_COMPLEMENT)
|
||||
U256.mulWide(hi2NC, out, N_COMPLEMENT)
|
||||
|
||||
var c2 = 0L
|
||||
for (i in 0 until 4) {
|
||||
|
||||
+61
-71
@@ -136,12 +136,10 @@ object Secp256k1 {
|
||||
require(seckey.size == 32)
|
||||
val scalar = U256.fromBytes(seckey)
|
||||
require(ScalarN.isValid(scalar))
|
||||
val p = MutablePoint()
|
||||
ECPoint.mulG(p, scalar)
|
||||
val x = LongArray(4)
|
||||
val y = LongArray(4)
|
||||
check(ECPoint.toAffine(p, x, y))
|
||||
return KeyCodec.serializeUncompressed(x, y)
|
||||
val sc = ECPoint.getScratch()
|
||||
ECPoint.mulG(sc.entryResult, scalar)
|
||||
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||
return KeyCodec.serializeUncompressed(sc.entryPx, sc.entryPy)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,14 +244,12 @@ object Secp256k1 {
|
||||
require(ScalarN.isValid(d0))
|
||||
|
||||
// Derive public key (one G multiplication + one inversion)
|
||||
val pubPoint = MutablePoint()
|
||||
ECPoint.mulG(pubPoint, d0)
|
||||
val px = LongArray(4)
|
||||
val py = LongArray(4)
|
||||
check(ECPoint.toAffine(pubPoint, px, py))
|
||||
val sc = ECPoint.getScratch()
|
||||
ECPoint.mulG(sc.entryResult, d0)
|
||||
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||
|
||||
val xOnlyPub = U256.toBytes(px)
|
||||
return signSchnorrInternal(data, d0, xOnlyPub, KeyCodec.hasEvenY(py), auxrand)
|
||||
val xOnlyPub = U256.toBytes(sc.entryPx)
|
||||
return signSchnorrInternal(data, d0, xOnlyPub, KeyCodec.hasEvenY(sc.entryPy), auxrand)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,23 +293,31 @@ object Secp256k1 {
|
||||
pubKeyHasEvenY: Boolean,
|
||||
auxrand: ByteArray?,
|
||||
): ByteArray {
|
||||
val d = if (pubKeyHasEvenY) d0 else ScalarN.neg(d0)
|
||||
val sc = ECPoint.getScratch()
|
||||
val tmp = sc.entryTmp
|
||||
|
||||
val d =
|
||||
if (pubKeyHasEvenY) {
|
||||
d0
|
||||
} else {
|
||||
ScalarN.negTo(tmp, d0)
|
||||
tmp
|
||||
}
|
||||
val dBytes = U256.toBytes(d)
|
||||
|
||||
val t =
|
||||
if (auxrand != null) {
|
||||
require(auxrand.size == 32)
|
||||
val auxHash = sha256(AUX_PREFIX + auxrand)
|
||||
val tArr = LongArray(4)
|
||||
U256.xorTo(tArr, U256.fromBytes(dBytes), U256.fromBytes(auxHash))
|
||||
U256.toBytes(tArr)
|
||||
} else {
|
||||
dBytes
|
||||
}
|
||||
val tBytes: ByteArray
|
||||
if (auxrand != null) {
|
||||
require(auxrand.size == 32)
|
||||
val auxHash = sha256(AUX_PREFIX + auxrand)
|
||||
U256.xorTo(sc.entryTmp2, U256.fromBytes(dBytes), U256.fromBytes(auxHash))
|
||||
tBytes = U256.toBytes(sc.entryTmp2)
|
||||
} else {
|
||||
tBytes = dBytes
|
||||
}
|
||||
|
||||
val nonceInput = ByteArray(64 + 32 + 32 + data.size)
|
||||
NONCE_PREFIX.copyInto(nonceInput, 0)
|
||||
t.copyInto(nonceInput, 64)
|
||||
tBytes.copyInto(nonceInput, 64)
|
||||
pBytes.copyInto(nonceInput, 96)
|
||||
data.copyInto(nonceInput, 128)
|
||||
val rand = sha256(nonceInput)
|
||||
@@ -321,11 +325,10 @@ object Secp256k1 {
|
||||
require(!U256.isZero(k0))
|
||||
|
||||
// R = k0·G
|
||||
val rPoint = MutablePoint()
|
||||
ECPoint.mulG(rPoint, k0)
|
||||
val rx = LongArray(4)
|
||||
val ry = LongArray(4)
|
||||
check(ECPoint.toAffine(rPoint, rx, ry))
|
||||
ECPoint.mulG(sc.entryResult, k0)
|
||||
val rx = sc.entryPx
|
||||
val ry = sc.entryPy
|
||||
check(ECPoint.toAffine(sc.entryResult, rx, ry, sc))
|
||||
|
||||
val k = if (KeyCodec.hasEvenY(ry)) k0 else ScalarN.neg(k0)
|
||||
|
||||
@@ -371,14 +374,12 @@ object Secp256k1 {
|
||||
// Use thread-local scratch to avoid per-verify allocations.
|
||||
// Saves ~10 LongArray(4) + 2 MutablePoint = ~14 object allocations per call.
|
||||
val sc = ECPoint.getScratch()
|
||||
val px = sc.verifyPx
|
||||
val py = sc.verifyPy
|
||||
if (!liftXCached(px, py, pub)) return false
|
||||
if (!liftXCached(sc.entryPx, sc.entryPy, pub)) return false
|
||||
|
||||
val r = sc.verifyR
|
||||
val r = sc.entryTmp
|
||||
U256.fromBytesInto(r, signature, 0)
|
||||
if (U256.cmp(r, FieldP.P) >= 0) return false
|
||||
val s = sc.verifyS
|
||||
val s = sc.entryTmp2
|
||||
U256.fromBytesInto(s, signature, 32)
|
||||
if (U256.cmp(s, ScalarN.N) >= 0) return false
|
||||
|
||||
@@ -389,23 +390,20 @@ object Secp256k1 {
|
||||
pub.copyInto(hashInput, 96)
|
||||
data.copyInto(hashInput, 128)
|
||||
val eHash = sha256(hashInput)
|
||||
val e = sc.verifyE
|
||||
// Reuse entryPx for e (liftX result already copied into pPoint below)
|
||||
val e = sc.zInv // safe: zInv not used until toAffine after mulDoubleG
|
||||
U256.fromBytesInto(e, eHash, 0)
|
||||
if (U256.cmp(e, ScalarN.N) >= 0) U256.subTo(e, e, ScalarN.N) // inline reduce
|
||||
|
||||
// R = s·G + (-e)·P via Shamir's trick
|
||||
ScalarN.negTo(e, e) // negate in-place
|
||||
val pPoint = sc.verifyPPoint
|
||||
pPoint.setAffine(px, py)
|
||||
val result = sc.verifyResult
|
||||
ECPoint.mulDoubleG(result, s, pPoint, e)
|
||||
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy) // copies px/py, so entryPx is free
|
||||
ECPoint.mulDoubleG(sc.entryResult, s, sc.entryPoint, e)
|
||||
|
||||
if (result.isInfinity()) return false
|
||||
val rx = sc.verifyRx
|
||||
val ry = sc.verifyRy
|
||||
if (!ECPoint.toAffine(result, rx, ry)) return false
|
||||
if (!KeyCodec.hasEvenY(ry)) return false
|
||||
return U256.cmp(rx, r) == 0
|
||||
if (sc.entryResult.isInfinity()) return false
|
||||
if (!ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc)) return false
|
||||
if (!KeyCodec.hasEvenY(sc.entryPy)) return false
|
||||
return U256.cmp(sc.entryPx, r) == 0
|
||||
}
|
||||
|
||||
// ==================== Tweak operations ====================
|
||||
@@ -427,24 +425,19 @@ object Secp256k1 {
|
||||
tweak: ByteArray,
|
||||
): ByteArray {
|
||||
require(tweak.size == 32)
|
||||
val x = LongArray(4)
|
||||
val y = LongArray(4)
|
||||
check(KeyCodec.parsePublicKey(pubkey, x, y))
|
||||
val sc = ECPoint.getScratch()
|
||||
check(KeyCodec.parsePublicKey(pubkey, sc.entryPx, sc.entryPy))
|
||||
val scalar = U256.fromBytes(tweak)
|
||||
require(ScalarN.isValid(scalar))
|
||||
|
||||
val p = MutablePoint()
|
||||
p.setAffine(x, y)
|
||||
val result = MutablePoint()
|
||||
ECPoint.mul(result, p, scalar)
|
||||
val rx = LongArray(4)
|
||||
val ry = LongArray(4)
|
||||
check(ECPoint.toAffine(result, rx, ry))
|
||||
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
|
||||
ECPoint.mul(sc.entryResult, sc.entryPoint, scalar)
|
||||
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||
|
||||
return if (pubkey.size == 33) {
|
||||
KeyCodec.serializeCompressed(rx, ry)
|
||||
KeyCodec.serializeCompressed(sc.entryPx, sc.entryPy)
|
||||
} else {
|
||||
KeyCodec.serializeUncompressed(rx, ry)
|
||||
KeyCodec.serializeUncompressed(sc.entryPx, sc.entryPy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,25 +459,22 @@ object Secp256k1 {
|
||||
scalar: ByteArray,
|
||||
): ByteArray {
|
||||
require(xOnlyPub.size == 32 && scalar.size == 32)
|
||||
val x = U256.fromBytes(xOnlyPub)
|
||||
require(U256.cmp(x, FieldP.P) < 0)
|
||||
val sc = ECPoint.getScratch()
|
||||
U256.fromBytesInto(sc.entryTmp, xOnlyPub, 0)
|
||||
require(U256.cmp(sc.entryTmp, FieldP.P) < 0)
|
||||
val k = U256.fromBytes(scalar)
|
||||
require(ScalarN.isValid(k))
|
||||
|
||||
// Compute y = sqrt(x³ + 7). We need SOME valid y for EC point operations,
|
||||
// but the result's x-coordinate is the same regardless of y sign.
|
||||
// Use liftX which returns the even-y variant.
|
||||
val px = LongArray(4)
|
||||
val py = LongArray(4)
|
||||
check(KeyCodec.liftX(px, py, x)) { "Not a valid x-coordinate on secp256k1" }
|
||||
check(KeyCodec.liftX(sc.entryPx, sc.entryPy, sc.entryTmp, sc.entryTmp2)) {
|
||||
"Not a valid x-coordinate on secp256k1"
|
||||
}
|
||||
|
||||
val p = MutablePoint()
|
||||
p.setAffine(px, py)
|
||||
val result = MutablePoint()
|
||||
ECPoint.mul(result, p, k)
|
||||
val rx = LongArray(4)
|
||||
check(ECPoint.toAffineX(result, rx))
|
||||
return U256.toBytes(rx)
|
||||
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
|
||||
ECPoint.mul(sc.entryResult, sc.entryPoint, k)
|
||||
check(ECPoint.toAffineX(sc.entryResult, sc.entryPx, sc))
|
||||
return U256.toBytes(sc.entryPx)
|
||||
}
|
||||
|
||||
/** BIP-340 tagged hash (for tags not cached above). */
|
||||
|
||||
Reference in New Issue
Block a user