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:
Claude
2026-04-06 14:27:30 +00:00
parent 547be89577
commit 2661bf783d
3 changed files with 48 additions and 30 deletions
@@ -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))