From 9aa3e211b97cb80079769209a532d3620dca00d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 21:33:38 +0000 Subject: [PATCH] perf: pass PointScratch through mul and mulG, eliminate ThreadLocal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../vitorpamplona/quartz/utils/secp256k1/ECPoint.kt | 6 ++---- .../vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt | 10 +++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ECPoint.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ECPoint.kt index 9feaeaf18..c9279fd1c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ECPoint.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ECPoint.kt @@ -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 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt index 33d5e5509..9eea4728c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt @@ -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) }