perf: pass PointScratch through mul and mulG, eliminate ThreadLocal

Add optional PointScratch parameter to ECPoint.mul and ECPoint.mulG
(default to scratch.get() for backward compat). All callers in
Secp256k1.kt now pass their already-fetched scratch through.

From the ECDH trace: ECPoint.mul was calling scratch.get() (ThreadLocal)
redundantly — the caller already had the scratch. On ART, each
ThreadLocal.get costs ~6µs (hash table probe), and ECDH had 10 calls
totaling 64µs.

With this change, all hot-path EC operations (verify, sign, ECDH,
pubkey create, tweak mul) fetch the scratch once at the entry point
and pass it through the entire call chain.

https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
This commit is contained in:
Claude
2026-04-09 21:33:38 +00:00
parent e148ff1ed7
commit 9aa3e211b9
2 changed files with 7 additions and 9 deletions
@@ -455,13 +455,12 @@ internal object ECPoint {
out: MutablePoint,
p: MutablePoint,
scalar: LongArray,
s: PointScratch = scratch.get(),
) {
if (U256.isZero(scalar) || p.isInfinity()) {
out.setInfinity()
return
}
val s = scratch.get()
val wnd = 5
val tableSize = 1 shl (wnd - 2) // 8 entries
@@ -538,13 +537,12 @@ internal object ECPoint {
fun mulG(
out: MutablePoint,
scalar: LongArray,
s: PointScratch = scratch.get(),
) {
if (U256.isZero(scalar)) {
out.setInfinity()
return
}
val s = scratch.get()
val table = combTable
// Ping-pong: alternate between out and s.mixTmp to avoid copyFrom after
@@ -131,7 +131,7 @@ object Secp256k1 {
val scalar = sc.scalarTmp1
U256.fromBytesInto(scalar, seckey, 0)
require(ScalarN.isValid(scalar))
ECPoint.mulG(sc.entryResult, scalar)
ECPoint.mulG(sc.entryResult, scalar, sc)
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
return KeyCodec.serializeUncompressed(sc.entryPx, sc.entryPy)
}
@@ -239,7 +239,7 @@ object Secp256k1 {
val d0 = U256.fromBytes(seckey)
require(ScalarN.isValid(d0))
val sc = ECPoint.getScratch()
ECPoint.mulG(sc.entryResult, d0)
ECPoint.mulG(sc.entryResult, d0, sc)
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
// Allocate xOnlyPub — signSchnorrInternal reuses bytesTmp1/2 internally.
@@ -361,7 +361,7 @@ object Secp256k1 {
require(!U256.isZero(k0))
// R = k0·G
ECPoint.mulG(sc.entryResult, k0)
ECPoint.mulG(sc.entryResult, k0, sc)
val rx = sc.entryPx
val ry = sc.entryPy
check(ECPoint.toAffine(sc.entryResult, rx, ry, sc))
@@ -552,7 +552,7 @@ object Secp256k1 {
require(ScalarN.isValid(scalar))
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
ECPoint.mul(sc.entryResult, sc.entryPoint, scalar)
ECPoint.mul(sc.entryResult, sc.entryPoint, scalar, sc)
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
return if (pubkey.size == 33) {
@@ -594,7 +594,7 @@ object Secp256k1 {
}
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
ECPoint.mul(sc.entryResult, sc.entryPoint, k)
ECPoint.mul(sc.entryResult, sc.entryPoint, k, sc)
check(ECPoint.toAffineX(sc.entryResult, sc.entryPx, sc))
return U256.toBytes(sc.entryPx)
}