diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1Benchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1Benchmark.kt index 9a6496af5..2909e8d5f 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1Benchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1Benchmark.kt @@ -35,20 +35,17 @@ import org.junit.runner.RunWith /** * Android microbenchmark for all secp256k1 operations used by Nostr. * - * Uses AndroidX Benchmark (Jetpack Microbenchmark) for accurate, warmed-up - * measurements on real Android hardware. Results appear in Android Studio's - * benchmark output with ns/op, allocations, and percentiles. + * Measures the same operations as the JVM benchmark (Secp256k1Benchmark.kt) and the + * K/Native benchmark (Secp256k1NativeBenchmark.kt) for cross-platform comparability. * * Tests are structured as matched pairs: "Foo" uses the native C library (ACINQ/JNI), - * "FooOurs" uses the pure-Kotlin implementation. This allows direct comparison of - * each operation between native and Kotlin on the same device. + * "FooOurs" uses the pure-Kotlin implementation. * * NOTE: verifySchnorr uses the same pubkey repeatedly, so it benefits from the * pubkey decompression cache and P-table cache. This is realistic for Nostr * (feed from one author) but not representative of first-time verification. * - * Test data is the same across all 3 platform benchmarks (JVM, Android, K/Native) - * for cross-platform comparability. + * Test data is the same across all 3 platform benchmarks for cross-platform comparability. * * Run with: ./gradlew :benchmark:connectedAndroidTest */ @@ -70,7 +67,22 @@ class Secp256k1Benchmark { private val privKey2 = "3982F19BEF1615BCCFBB05E321C10E1D4CBA3DF0E841C2E41EEB6016347653C3".hexToByteArray() private val pubKey2XOnly = Secp256k1Instance.compressedPubKeyFor(privKey2).copyOfRange(1, 33) - // ==================== Core Nostr operations (Native C via ACINQ/JNI) ==================== + // Batch verification data + private val batch8 = buildBatch(8) + private val batch16 = buildBatch(16) + + private fun buildBatch(size: Int): Pair, List> { + val sigs = mutableListOf() + val msgs = mutableListOf() + for (i in 0 until size) { + val m = ByteArray(32) { (i * 7 + it).toByte() } + sigs.add(Secp256k1InstanceOurs.signSchnorr(m, privKey, auxRand)) + msgs.add(m) + } + return Pair(sigs, msgs) + } + + // ==================== Native C (ACINQ/JNI) ==================== @Test fun verifySchnorr() { @@ -108,13 +120,13 @@ class Secp256k1Benchmark { } @Test - fun pubKeyTweakMulCompact() { + fun ecdhXOnly() { benchmarkRule.measureRepeated { assertNotNull(Secp256k1Instance.pubKeyTweakMulCompact(pubKey2XOnly, privKey)) } } - // ==================== Core Nostr operations (Pure Kotlin) ==================== + // ==================== Pure Kotlin ==================== @Test fun verifySchnorrOurs() { @@ -137,6 +149,13 @@ class Secp256k1Benchmark { } } + @Test + fun signSchnorrCachedPkOurs() { + benchmarkRule.measureRepeated { + assertNotNull(Secp256k1InstanceOurs.signSchnorrWithXOnlyPubKey(msg32, privKey, xOnlyPub, auxRand)) + } + } + @Test fun compressedPubKeyForOurs() { benchmarkRule.measureRepeated { @@ -159,31 +178,15 @@ class Secp256k1Benchmark { } @Test - fun pubKeyTweakMulCompactOurs() { + fun ecdhXOnlyOurs() { benchmarkRule.measureRepeated { assertNotNull(Secp256k1InstanceOurs.pubKeyTweakMulCompact(pubKey2XOnly, privKey)) } } // ==================== Batch verification (Pure Kotlin) ==================== - // Same pubkey, n events — the typical Nostr pattern (feed from one author). - // Each iteration verifies the entire batch. Compare per-event cost: - // ns/op ÷ batchSize = per-event cost - // JVM benchmark showed 4-7× speedup over individual verify. - - private fun buildBatch(size: Int): Pair, List> { - val sigs = mutableListOf() - val msgs = mutableListOf() - for (i in 0 until size) { - val m = ByteArray(32) { (i * 7 + it).toByte() } - sigs.add(Secp256k1InstanceOurs.signSchnorr(m, privKey, auxRand)) - msgs.add(m) - } - return Pair(sigs, msgs) - } - - private val batch8 = buildBatch(8) - private val batch16 = buildBatch(16) + // Same pubkey, n events — typical Nostr pattern (feed from one author). + // Per-event cost = ns/op ÷ batchSize. @Test fun verifyBatch8Ours() { 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 149165c18..03a7356b2 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 @@ -201,36 +201,6 @@ class Secp256k1Benchmark { }, ) - // --- pubkeyCreate --- - results += - bench( - name = "pubkeyCreate", - warmup = 1000, - iterations = 5000, - nativeOp = { native.pubkeyCreate(privKey) }, - kotlinOp = { - com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 - .pubkeyCreate(privKey) - }, - ) - - // --- pubKeyCompress --- - val uncompressedNative = native.pubkeyCreate(privKey) - val uncompressedKotlin = - com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 - .pubkeyCreate(privKey) - results += - bench( - name = "pubKeyCompress", - warmup = 2000, - iterations = 50000, - nativeOp = { native.pubKeyCompress(uncompressedNative) }, - kotlinOp = { - com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 - .pubKeyCompress(uncompressedKotlin) - }, - ) - // --- compressedPubKeyFor (create + compress combined) --- results += bench( diff --git a/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt b/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt index 442ea0d57..74570ee2d 100644 --- a/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt +++ b/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt @@ -125,6 +125,12 @@ class Secp256k1NativeBenchmark { Secp256k1.verifySchnorr(sig, msg32, xOnlyPub) } + // --- verifySchnorrFast (Nostr: x-check only, no y-parity inversion) --- + results += + bench("verifySchnorrFast", 2000, 5000) { + Secp256k1.verifySchnorrFast(sig, msg32, xOnlyPub) + } + // --- signSchnorr (derives pubkey each time) --- results += bench("signSchnorr", 1000, 3000) {