perf: add toAffineX for x-only ECDH, benchmark ecdhXOnly path
- Add ECPoint.toAffineX that computes only x = X/Z² (saves 2M vs full toAffine which also computes Y/Z³) - Use toAffineX in ecdhXOnly since only the x-coordinate is needed - Add ecdhXOnly benchmark measuring the actual Nostr ECDH production path (Secp256k1Instance.pubKeyTweakMulCompact delegates to ecdhXOnly) - Fix ktlint KDoc-inside-class-body violations The old benchmark measured pubKeyTweakMul(02||x, key) which pays for: array allocation, compressed key parsing (sqrt), full toAffine, and re-serialization. ecdhXOnly avoids the array overhead and serialization. https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
@@ -265,20 +265,16 @@ internal object ECPoint {
|
||||
|
||||
// ==================== Point Doubling (3M + 4S) ====================
|
||||
|
||||
/**
|
||||
* Point doubling: out = 2·p.
|
||||
*
|
||||
* Uses the optimized formula from libsecp256k1 for curves with a=0:
|
||||
* S = Y², L = (3/2)·X², T = -X·S
|
||||
* X₃ = L² + 2T, Y₃ = -(L·(X₃+T) + S²), Z₃ = Y·Z
|
||||
*
|
||||
* Cost: 3 multiplications + 4 squarings + field halving + adds/negations.
|
||||
* The key trick is computing L = (3/2)·X² using fe_half instead of an extra
|
||||
* multiplication — halving is just a carry-propagating right shift.
|
||||
*
|
||||
* Safe for out === inp (in-place doubling) via internal copy buffer.
|
||||
*/
|
||||
/** doublePoint with ThreadLocal scratch (convenience for non-hot paths). */
|
||||
// Point doubling: out = 2·p.
|
||||
//
|
||||
// Uses the optimized formula from libsecp256k1 for curves with a=0:
|
||||
// S = Y², L = (3/2)·X², T = -X·S
|
||||
// X₃ = L² + 2T, Y₃ = -(L·(X₃+T) + S²), Z₃ = Y·Z
|
||||
//
|
||||
// Cost: 3 multiplications + 4 squarings + field halving + adds/negations.
|
||||
// Safe for out === inp (in-place doubling) via internal copy buffer.
|
||||
|
||||
// doublePoint with ThreadLocal scratch (convenience for non-hot paths).
|
||||
fun doublePoint(
|
||||
out: MutablePoint,
|
||||
inp: MutablePoint,
|
||||
@@ -324,18 +320,10 @@ internal object ECPoint {
|
||||
|
||||
// ==================== Mixed Addition: Jacobian + Affine (8M + 3S) ====================
|
||||
|
||||
/**
|
||||
* Mixed point addition: out = p + (qx, qy) where q is an affine point (Z=1).
|
||||
*
|
||||
* When one input is affine, we can skip computing Z₂², Z₂³, and the Z₃ formula
|
||||
* simplifies. This saves 4 multiplications and 1 squaring vs full Jacobian addition.
|
||||
*
|
||||
* Cost: 8 multiplications + 3 squarings + adds/subtractions.
|
||||
* Used for additions from the precomputed G table (always stored in affine form).
|
||||
*
|
||||
* Handles degenerate cases: p is infinity, or p equals/negates q.
|
||||
*/
|
||||
/** addMixed with ThreadLocal scratch (convenience for non-hot paths). */
|
||||
// Mixed point addition: out = p + (qx, qy) where q is an affine point (Z=1).
|
||||
// Cost: 8M + 3S. Saves 4M+1S vs full Jacobian by exploiting Z₂=1.
|
||||
|
||||
// addMixed with ThreadLocal scratch (convenience for non-hot paths).
|
||||
fun addMixed(
|
||||
out: MutablePoint,
|
||||
p: MutablePoint,
|
||||
@@ -729,6 +717,24 @@ 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.
|
||||
*/
|
||||
fun toAffineX(
|
||||
p: MutablePoint,
|
||||
outX: LongArray,
|
||||
): 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)
|
||||
return true
|
||||
}
|
||||
|
||||
// ==================== Key Encoding (delegates to KeyCodec) ====================
|
||||
|
||||
fun liftX(
|
||||
|
||||
@@ -412,8 +412,7 @@ object Secp256k1 {
|
||||
val result = MutablePoint()
|
||||
ECPoint.mul(result, p, k)
|
||||
val rx = LongArray(4)
|
||||
val ry = LongArray(4)
|
||||
check(ECPoint.toAffine(result, rx, ry))
|
||||
check(ECPoint.toAffineX(result, rx))
|
||||
return U256.toBytes(rx)
|
||||
}
|
||||
|
||||
|
||||
+15
-2
@@ -267,11 +267,11 @@ class Secp256k1Benchmark {
|
||||
},
|
||||
)
|
||||
|
||||
// --- pubKeyTweakMulCompact (the actual Secp256k1Instance pattern) ---
|
||||
// --- pubKeyTweakMulCompact (old pattern via pubKeyTweakMul) ---
|
||||
val pub2xOnly = hexToBytes("c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133")
|
||||
results +=
|
||||
bench(
|
||||
name = "pubKeyTweakMulCompact",
|
||||
name = "tweakMulCompact (old)",
|
||||
warmup = 10,
|
||||
iterations = 50,
|
||||
nativeOp = { native.pubKeyTweakMul(h02 + pub2xOnly, privKey).copyOfRange(1, 33) },
|
||||
@@ -284,6 +284,19 @@ class Secp256k1Benchmark {
|
||||
},
|
||||
)
|
||||
|
||||
// --- ecdhXOnly (actual Nostr ECDH path) ---
|
||||
results +=
|
||||
bench(
|
||||
name = "ecdhXOnly (Nostr)",
|
||||
warmup = 10,
|
||||
iterations = 50,
|
||||
nativeOp = { native.pubKeyTweakMul(h02 + pub2xOnly, privKey).copyOfRange(1, 33) },
|
||||
kotlinOp = {
|
||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||
.ecdhXOnly(pub2xOnly, privKey)
|
||||
},
|
||||
)
|
||||
|
||||
// Print results
|
||||
println()
|
||||
println("=".repeat(90))
|
||||
|
||||
Reference in New Issue
Block a user