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 ====================
|
// ==================== Coordinate Conversion ====================
|
||||||
|
|
||||||
/**
|
/** Convert Jacobian → affine (convenience, allocates temps). For one-time init paths. */
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
fun toAffine(
|
fun toAffine(
|
||||||
p: MutablePoint,
|
p: MutablePoint,
|
||||||
outX: LongArray,
|
outX: LongArray,
|
||||||
@@ -877,21 +873,32 @@ internal object ECPoint {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Convert Jacobian → affine using pre-allocated scratch (hot path). */
|
||||||
* Convert from Jacobian to affine, returning only the x-coordinate: x = X/Z².
|
fun toAffine(
|
||||||
* Saves 2 multiplications vs full toAffine (no zInv3, no outY computation).
|
p: MutablePoint,
|
||||||
* Used by ecdhXOnly where only the x-coordinate of the shared point is needed.
|
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(
|
fun toAffineX(
|
||||||
p: MutablePoint,
|
p: MutablePoint,
|
||||||
outX: LongArray,
|
outX: LongArray,
|
||||||
|
s: PointScratch,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (p.isInfinity()) return false
|
if (p.isInfinity()) return false
|
||||||
val zInv = LongArray(4)
|
FieldP.inv(s.zInv, p.z)
|
||||||
val zInv2 = LongArray(4)
|
FieldP.sqr(s.zInv2, s.zInv)
|
||||||
FieldP.inv(zInv, p.z)
|
FieldP.mul(outX, p.x, s.zInv2)
|
||||||
FieldP.sqr(zInv2, zInv)
|
|
||||||
FieldP.mul(outX, p.x, zInv2)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,23 @@ internal object KeyCodec {
|
|||||||
return true
|
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). */
|
/** Check if y-coordinate is even (LSB = 0). */
|
||||||
fun hasEvenY(y: LongArray): Boolean = y[0] and 1L == 0L
|
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 splitK1 = LongArray(4) // output k1
|
||||||
val splitK2 = LongArray(4) // output k2
|
val splitK2 = LongArray(4) // output k2
|
||||||
|
|
||||||
// Pre-allocated scratch for verifySchnorr (avoids per-call allocations)
|
// Pre-allocated scratch for toAffine / toAffineX (avoids 3 LongArray allocs per call)
|
||||||
val verifyPx = LongArray(4)
|
val zInv = LongArray(4)
|
||||||
val verifyPy = LongArray(4)
|
val zInv2 = LongArray(4)
|
||||||
val verifyR = LongArray(4)
|
val zInv3 = LongArray(4)
|
||||||
val verifyS = LongArray(4)
|
|
||||||
val verifyE = LongArray(4)
|
// Pre-allocated scratch for Secp256k1 entry points (avoids per-call allocations)
|
||||||
val verifyRx = LongArray(4)
|
val entryPx = LongArray(4) // liftX / parsePublicKey output
|
||||||
val verifyRy = LongArray(4)
|
val entryPy = LongArray(4)
|
||||||
val verifyPPoint = MutablePoint()
|
val entryPoint = MutablePoint() // pubkeyCreate, signSchnorr, ecdhXOnly
|
||||||
val verifyResult = MutablePoint()
|
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 lo1 = w[1]
|
||||||
val lo2 = w[2]
|
val lo2 = w[2]
|
||||||
val lo3 = w[3]
|
val lo3 = w[3]
|
||||||
val hi0 = w[4]
|
// Use `out` as temporary storage for hi limbs (avoids longArrayOf allocation)
|
||||||
val hi1 = w[5]
|
out[0] = w[4]
|
||||||
val hi2 = w[6]
|
out[1] = w[5]
|
||||||
val hi3 = w[7]
|
out[2] = w[6]
|
||||||
val hiArr = longArrayOf(hi0, hi1, hi2, hi3)
|
out[3] = w[7]
|
||||||
|
|
||||||
val hiTimesNC = w // reuse w as scratch
|
val hiTimesNC = w // reuse w as scratch
|
||||||
U256.mulWide(hiTimesNC, hiArr, N_COMPLEMENT)
|
U256.mulWide(hiTimesNC, out, N_COMPLEMENT)
|
||||||
|
|
||||||
// sum = hiTimesNC + lo
|
// sum = hiTimesNC + lo
|
||||||
var carry = 0L
|
var carry = 0L
|
||||||
@@ -213,14 +213,17 @@ internal object ScalarN {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Round 2
|
// Round 2: reuse out for hi2 limbs (avoids longArrayOf allocation)
|
||||||
val hi2Arr = longArrayOf(w[4], w[5], w[6], w[7])
|
out[0] = w[4]
|
||||||
|
out[1] = w[5]
|
||||||
|
out[2] = w[6]
|
||||||
|
out[3] = w[7]
|
||||||
val saved0 = w[0]
|
val saved0 = w[0]
|
||||||
val saved1 = w[1]
|
val saved1 = w[1]
|
||||||
val saved2 = w[2]
|
val saved2 = w[2]
|
||||||
val saved3 = w[3]
|
val saved3 = w[3]
|
||||||
val hi2NC = w
|
val hi2NC = w
|
||||||
U256.mulWide(hi2NC, hi2Arr, N_COMPLEMENT)
|
U256.mulWide(hi2NC, out, N_COMPLEMENT)
|
||||||
|
|
||||||
var c2 = 0L
|
var c2 = 0L
|
||||||
for (i in 0 until 4) {
|
for (i in 0 until 4) {
|
||||||
|
|||||||
+61
-71
@@ -136,12 +136,10 @@ object Secp256k1 {
|
|||||||
require(seckey.size == 32)
|
require(seckey.size == 32)
|
||||||
val scalar = U256.fromBytes(seckey)
|
val scalar = U256.fromBytes(seckey)
|
||||||
require(ScalarN.isValid(scalar))
|
require(ScalarN.isValid(scalar))
|
||||||
val p = MutablePoint()
|
val sc = ECPoint.getScratch()
|
||||||
ECPoint.mulG(p, scalar)
|
ECPoint.mulG(sc.entryResult, scalar)
|
||||||
val x = LongArray(4)
|
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||||
val y = LongArray(4)
|
return KeyCodec.serializeUncompressed(sc.entryPx, sc.entryPy)
|
||||||
check(ECPoint.toAffine(p, x, y))
|
|
||||||
return KeyCodec.serializeUncompressed(x, y)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,14 +244,12 @@ object Secp256k1 {
|
|||||||
require(ScalarN.isValid(d0))
|
require(ScalarN.isValid(d0))
|
||||||
|
|
||||||
// Derive public key (one G multiplication + one inversion)
|
// Derive public key (one G multiplication + one inversion)
|
||||||
val pubPoint = MutablePoint()
|
val sc = ECPoint.getScratch()
|
||||||
ECPoint.mulG(pubPoint, d0)
|
ECPoint.mulG(sc.entryResult, d0)
|
||||||
val px = LongArray(4)
|
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||||
val py = LongArray(4)
|
|
||||||
check(ECPoint.toAffine(pubPoint, px, py))
|
|
||||||
|
|
||||||
val xOnlyPub = U256.toBytes(px)
|
val xOnlyPub = U256.toBytes(sc.entryPx)
|
||||||
return signSchnorrInternal(data, d0, xOnlyPub, KeyCodec.hasEvenY(py), auxrand)
|
return signSchnorrInternal(data, d0, xOnlyPub, KeyCodec.hasEvenY(sc.entryPy), auxrand)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -297,23 +293,31 @@ object Secp256k1 {
|
|||||||
pubKeyHasEvenY: Boolean,
|
pubKeyHasEvenY: Boolean,
|
||||||
auxrand: ByteArray?,
|
auxrand: ByteArray?,
|
||||||
): 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 dBytes = U256.toBytes(d)
|
||||||
|
|
||||||
val t =
|
val tBytes: ByteArray
|
||||||
if (auxrand != null) {
|
if (auxrand != null) {
|
||||||
require(auxrand.size == 32)
|
require(auxrand.size == 32)
|
||||||
val auxHash = sha256(AUX_PREFIX + auxrand)
|
val auxHash = sha256(AUX_PREFIX + auxrand)
|
||||||
val tArr = LongArray(4)
|
U256.xorTo(sc.entryTmp2, U256.fromBytes(dBytes), U256.fromBytes(auxHash))
|
||||||
U256.xorTo(tArr, U256.fromBytes(dBytes), U256.fromBytes(auxHash))
|
tBytes = U256.toBytes(sc.entryTmp2)
|
||||||
U256.toBytes(tArr)
|
} else {
|
||||||
} else {
|
tBytes = dBytes
|
||||||
dBytes
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val nonceInput = ByteArray(64 + 32 + 32 + data.size)
|
val nonceInput = ByteArray(64 + 32 + 32 + data.size)
|
||||||
NONCE_PREFIX.copyInto(nonceInput, 0)
|
NONCE_PREFIX.copyInto(nonceInput, 0)
|
||||||
t.copyInto(nonceInput, 64)
|
tBytes.copyInto(nonceInput, 64)
|
||||||
pBytes.copyInto(nonceInput, 96)
|
pBytes.copyInto(nonceInput, 96)
|
||||||
data.copyInto(nonceInput, 128)
|
data.copyInto(nonceInput, 128)
|
||||||
val rand = sha256(nonceInput)
|
val rand = sha256(nonceInput)
|
||||||
@@ -321,11 +325,10 @@ object Secp256k1 {
|
|||||||
require(!U256.isZero(k0))
|
require(!U256.isZero(k0))
|
||||||
|
|
||||||
// R = k0·G
|
// R = k0·G
|
||||||
val rPoint = MutablePoint()
|
ECPoint.mulG(sc.entryResult, k0)
|
||||||
ECPoint.mulG(rPoint, k0)
|
val rx = sc.entryPx
|
||||||
val rx = LongArray(4)
|
val ry = sc.entryPy
|
||||||
val ry = LongArray(4)
|
check(ECPoint.toAffine(sc.entryResult, rx, ry, sc))
|
||||||
check(ECPoint.toAffine(rPoint, rx, ry))
|
|
||||||
|
|
||||||
val k = if (KeyCodec.hasEvenY(ry)) k0 else ScalarN.neg(k0)
|
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.
|
// Use thread-local scratch to avoid per-verify allocations.
|
||||||
// Saves ~10 LongArray(4) + 2 MutablePoint = ~14 object allocations per call.
|
// Saves ~10 LongArray(4) + 2 MutablePoint = ~14 object allocations per call.
|
||||||
val sc = ECPoint.getScratch()
|
val sc = ECPoint.getScratch()
|
||||||
val px = sc.verifyPx
|
if (!liftXCached(sc.entryPx, sc.entryPy, pub)) return false
|
||||||
val py = sc.verifyPy
|
|
||||||
if (!liftXCached(px, py, pub)) return false
|
|
||||||
|
|
||||||
val r = sc.verifyR
|
val r = sc.entryTmp
|
||||||
U256.fromBytesInto(r, signature, 0)
|
U256.fromBytesInto(r, signature, 0)
|
||||||
if (U256.cmp(r, FieldP.P) >= 0) return false
|
if (U256.cmp(r, FieldP.P) >= 0) return false
|
||||||
val s = sc.verifyS
|
val s = sc.entryTmp2
|
||||||
U256.fromBytesInto(s, signature, 32)
|
U256.fromBytesInto(s, signature, 32)
|
||||||
if (U256.cmp(s, ScalarN.N) >= 0) return false
|
if (U256.cmp(s, ScalarN.N) >= 0) return false
|
||||||
|
|
||||||
@@ -389,23 +390,20 @@ object Secp256k1 {
|
|||||||
pub.copyInto(hashInput, 96)
|
pub.copyInto(hashInput, 96)
|
||||||
data.copyInto(hashInput, 128)
|
data.copyInto(hashInput, 128)
|
||||||
val eHash = sha256(hashInput)
|
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)
|
U256.fromBytesInto(e, eHash, 0)
|
||||||
if (U256.cmp(e, ScalarN.N) >= 0) U256.subTo(e, e, ScalarN.N) // inline reduce
|
if (U256.cmp(e, ScalarN.N) >= 0) U256.subTo(e, e, ScalarN.N) // inline reduce
|
||||||
|
|
||||||
// R = s·G + (-e)·P via Shamir's trick
|
// R = s·G + (-e)·P via Shamir's trick
|
||||||
ScalarN.negTo(e, e) // negate in-place
|
ScalarN.negTo(e, e) // negate in-place
|
||||||
val pPoint = sc.verifyPPoint
|
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy) // copies px/py, so entryPx is free
|
||||||
pPoint.setAffine(px, py)
|
ECPoint.mulDoubleG(sc.entryResult, s, sc.entryPoint, e)
|
||||||
val result = sc.verifyResult
|
|
||||||
ECPoint.mulDoubleG(result, s, pPoint, e)
|
|
||||||
|
|
||||||
if (result.isInfinity()) return false
|
if (sc.entryResult.isInfinity()) return false
|
||||||
val rx = sc.verifyRx
|
if (!ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc)) return false
|
||||||
val ry = sc.verifyRy
|
if (!KeyCodec.hasEvenY(sc.entryPy)) return false
|
||||||
if (!ECPoint.toAffine(result, rx, ry)) return false
|
return U256.cmp(sc.entryPx, r) == 0
|
||||||
if (!KeyCodec.hasEvenY(ry)) return false
|
|
||||||
return U256.cmp(rx, r) == 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Tweak operations ====================
|
// ==================== Tweak operations ====================
|
||||||
@@ -427,24 +425,19 @@ object Secp256k1 {
|
|||||||
tweak: ByteArray,
|
tweak: ByteArray,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
require(tweak.size == 32)
|
require(tweak.size == 32)
|
||||||
val x = LongArray(4)
|
val sc = ECPoint.getScratch()
|
||||||
val y = LongArray(4)
|
check(KeyCodec.parsePublicKey(pubkey, sc.entryPx, sc.entryPy))
|
||||||
check(KeyCodec.parsePublicKey(pubkey, x, y))
|
|
||||||
val scalar = U256.fromBytes(tweak)
|
val scalar = U256.fromBytes(tweak)
|
||||||
require(ScalarN.isValid(scalar))
|
require(ScalarN.isValid(scalar))
|
||||||
|
|
||||||
val p = MutablePoint()
|
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
|
||||||
p.setAffine(x, y)
|
ECPoint.mul(sc.entryResult, sc.entryPoint, scalar)
|
||||||
val result = MutablePoint()
|
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||||
ECPoint.mul(result, p, scalar)
|
|
||||||
val rx = LongArray(4)
|
|
||||||
val ry = LongArray(4)
|
|
||||||
check(ECPoint.toAffine(result, rx, ry))
|
|
||||||
|
|
||||||
return if (pubkey.size == 33) {
|
return if (pubkey.size == 33) {
|
||||||
KeyCodec.serializeCompressed(rx, ry)
|
KeyCodec.serializeCompressed(sc.entryPx, sc.entryPy)
|
||||||
} else {
|
} else {
|
||||||
KeyCodec.serializeUncompressed(rx, ry)
|
KeyCodec.serializeUncompressed(sc.entryPx, sc.entryPy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,25 +459,22 @@ object Secp256k1 {
|
|||||||
scalar: ByteArray,
|
scalar: ByteArray,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
require(xOnlyPub.size == 32 && scalar.size == 32)
|
require(xOnlyPub.size == 32 && scalar.size == 32)
|
||||||
val x = U256.fromBytes(xOnlyPub)
|
val sc = ECPoint.getScratch()
|
||||||
require(U256.cmp(x, FieldP.P) < 0)
|
U256.fromBytesInto(sc.entryTmp, xOnlyPub, 0)
|
||||||
|
require(U256.cmp(sc.entryTmp, FieldP.P) < 0)
|
||||||
val k = U256.fromBytes(scalar)
|
val k = U256.fromBytes(scalar)
|
||||||
require(ScalarN.isValid(k))
|
require(ScalarN.isValid(k))
|
||||||
|
|
||||||
// Compute y = sqrt(x³ + 7). We need SOME valid y for EC point operations,
|
// 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.
|
// but the result's x-coordinate is the same regardless of y sign.
|
||||||
// Use liftX which returns the even-y variant.
|
check(KeyCodec.liftX(sc.entryPx, sc.entryPy, sc.entryTmp, sc.entryTmp2)) {
|
||||||
val px = LongArray(4)
|
"Not a valid x-coordinate on secp256k1"
|
||||||
val py = LongArray(4)
|
}
|
||||||
check(KeyCodec.liftX(px, py, x)) { "Not a valid x-coordinate on secp256k1" }
|
|
||||||
|
|
||||||
val p = MutablePoint()
|
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
|
||||||
p.setAffine(px, py)
|
ECPoint.mul(sc.entryResult, sc.entryPoint, k)
|
||||||
val result = MutablePoint()
|
check(ECPoint.toAffineX(sc.entryResult, sc.entryPx, sc))
|
||||||
ECPoint.mul(result, p, k)
|
return U256.toBytes(sc.entryPx)
|
||||||
val rx = LongArray(4)
|
|
||||||
check(ECPoint.toAffineX(result, rx))
|
|
||||||
return U256.toBytes(rx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** BIP-340 tagged hash (for tags not cached above). */
|
/** BIP-340 tagged hash (for tags not cached above). */
|
||||||
|
|||||||
Reference in New Issue
Block a user