perf: eliminate heap allocations in key operations
Replace U256.fromBytes (allocates LongArray(4)) with U256.fromBytesInto (writes to pre-allocated scratch) across all key operations: - pubkeyCreate: scalar → sc.scalarTmp1 (safe: mulG doesn't use it) - signSchnorrWithPubKey: d0 → sc.zInv (safe: signSchnorrInternal doesn't use it; d0 is read at lines 322-324 and 391 before zInv is needed) - signSchnorrWithXOnlyPubKey: same pattern with sc.zInv - pubKeyTweakMul: scalar → sc.scalarTmp1 (safe: ECPoint.mul doesn't use scalarTmp fields) - ecdhXOnly: k → sc.scalarTmp1 (same) - privKeyTweakAdd: already fixed in previous commit signSchnorr keeps its allocation because d0 must survive through mulG (which destroys splitK*) AND signSchnorrInternal (which uses scalarTmp*). The single LongArray(4) alloc is negligible vs mulG cost (~100μs). Each eliminated allocation saves ~10-20ns of GC pressure on ART and ~5ns on K/Native. For signSchnorrWithPubKey (the cached-pk fast path), this removes the only unnecessary allocation. https://claude.ai/code/session_015CtM5k88rF7WFgX8o2AGNR
This commit is contained in:
@@ -127,9 +127,10 @@ object Secp256k1 {
|
|||||||
/** Create a 65-byte uncompressed public key (04 || x || y) from a 32-byte secret key. */
|
/** Create a 65-byte uncompressed public key (04 || x || y) from a 32-byte secret key. */
|
||||||
fun pubkeyCreate(seckey: ByteArray): ByteArray {
|
fun pubkeyCreate(seckey: ByteArray): ByteArray {
|
||||||
require(seckey.size == 32)
|
require(seckey.size == 32)
|
||||||
val scalar = U256.fromBytes(seckey)
|
|
||||||
require(ScalarN.isValid(scalar))
|
|
||||||
val sc = ECPoint.getScratch()
|
val sc = ECPoint.getScratch()
|
||||||
|
val scalar = sc.scalarTmp1
|
||||||
|
U256.fromBytesInto(scalar, seckey, 0)
|
||||||
|
require(ScalarN.isValid(scalar))
|
||||||
ECPoint.mulG(sc.entryResult, scalar)
|
ECPoint.mulG(sc.entryResult, scalar)
|
||||||
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||||
return KeyCodec.serializeUncompressed(sc.entryPx, sc.entryPy)
|
return KeyCodec.serializeUncompressed(sc.entryPx, sc.entryPy)
|
||||||
@@ -233,11 +234,10 @@ object Secp256k1 {
|
|||||||
auxrand: ByteArray?,
|
auxrand: ByteArray?,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
require(seckey.size == 32)
|
require(seckey.size == 32)
|
||||||
// Allocate d0 separately — signSchnorrInternal uses all scalar scratch buffers.
|
// d0 must survive through mulG (which destroys splitK*) and signSchnorrInternal
|
||||||
|
// (which uses scalarTmp*). The allocation is negligible vs mulG cost (~100μs).
|
||||||
val d0 = U256.fromBytes(seckey)
|
val d0 = U256.fromBytes(seckey)
|
||||||
require(ScalarN.isValid(d0))
|
require(ScalarN.isValid(d0))
|
||||||
|
|
||||||
// Derive public key (one G multiplication + one inversion)
|
|
||||||
val sc = ECPoint.getScratch()
|
val sc = ECPoint.getScratch()
|
||||||
ECPoint.mulG(sc.entryResult, d0)
|
ECPoint.mulG(sc.entryResult, d0)
|
||||||
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||||
@@ -270,7 +270,10 @@ object Secp256k1 {
|
|||||||
auxrand: ByteArray?,
|
auxrand: ByteArray?,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
require(seckey.size == 32 && compressedPub.size == 33)
|
require(seckey.size == 32 && compressedPub.size == 33)
|
||||||
val d0 = U256.fromBytes(seckey)
|
// Use zInv for d0 — not used by signSchnorrInternal.
|
||||||
|
val sc = ECPoint.getScratch()
|
||||||
|
val d0 = sc.zInv
|
||||||
|
U256.fromBytesInto(d0, seckey, 0)
|
||||||
require(ScalarN.isValid(d0))
|
require(ScalarN.isValid(d0))
|
||||||
val hasEvenY = compressedPub[0] == 0x02.toByte()
|
val hasEvenY = compressedPub[0] == 0x02.toByte()
|
||||||
val xOnlyPub = compressedPub.copyOfRange(1, 33)
|
val xOnlyPub = compressedPub.copyOfRange(1, 33)
|
||||||
@@ -293,7 +296,10 @@ object Secp256k1 {
|
|||||||
auxrand: ByteArray?,
|
auxrand: ByteArray?,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
require(seckey.size == 32 && xOnlyPub.size == 32)
|
require(seckey.size == 32 && xOnlyPub.size == 32)
|
||||||
val d0 = U256.fromBytes(seckey)
|
// Use zInv for d0 — not used by signSchnorrInternal.
|
||||||
|
val sc = ECPoint.getScratch()
|
||||||
|
val d0 = sc.zInv
|
||||||
|
U256.fromBytesInto(d0, seckey, 0)
|
||||||
require(ScalarN.isValid(d0))
|
require(ScalarN.isValid(d0))
|
||||||
// BIP-340: x-only pubkeys always have even y
|
// BIP-340: x-only pubkeys always have even y
|
||||||
return signSchnorrInternal(data, d0, xOnlyPub, true, auxrand)
|
return signSchnorrInternal(data, d0, xOnlyPub, true, auxrand)
|
||||||
@@ -541,7 +547,8 @@ object Secp256k1 {
|
|||||||
require(tweak.size == 32)
|
require(tweak.size == 32)
|
||||||
val sc = ECPoint.getScratch()
|
val sc = ECPoint.getScratch()
|
||||||
check(KeyCodec.parsePublicKey(pubkey, sc.entryPx, sc.entryPy))
|
check(KeyCodec.parsePublicKey(pubkey, sc.entryPx, sc.entryPy))
|
||||||
val scalar = U256.fromBytes(tweak)
|
val scalar = sc.scalarTmp1
|
||||||
|
U256.fromBytesInto(scalar, tweak, 0)
|
||||||
require(ScalarN.isValid(scalar))
|
require(ScalarN.isValid(scalar))
|
||||||
|
|
||||||
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
|
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
|
||||||
@@ -576,7 +583,8 @@ object Secp256k1 {
|
|||||||
val sc = ECPoint.getScratch()
|
val sc = ECPoint.getScratch()
|
||||||
U256.fromBytesInto(sc.entryTmp, xOnlyPub, 0)
|
U256.fromBytesInto(sc.entryTmp, xOnlyPub, 0)
|
||||||
require(U256.cmp(sc.entryTmp, FieldP.P) < 0)
|
require(U256.cmp(sc.entryTmp, FieldP.P) < 0)
|
||||||
val k = U256.fromBytes(scalar)
|
val k = sc.scalarTmp1
|
||||||
|
U256.fromBytesInto(k, scalar, 0)
|
||||||
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user