diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Point.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Point.kt index 4031bca66..20906dad6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Point.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Point.kt @@ -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( 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 f141b0f84..c3c98c99e 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 @@ -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) } diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1Benchmark.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1Benchmark.kt index 086f12543..1fbfd58a4 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1Benchmark.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1Benchmark.kt @@ -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))