From b3206b8e48c874c7bae6868aba956435b8f4f133 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 14:45:14 +0000 Subject: [PATCH] feat: add batch verify to Android benchmark + expose in Secp256k1InstanceOurs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add verifySchnorrBatch to Secp256k1InstanceOurs wrapper and add Android benchmark tests for batch(8) and batch(16). To get per-event cost: ns/op ÷ batchSize. JVM benchmark showed 4-7× speedup over individual verify. Android results TBD. https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY --- .../quartz/benchmark/Secp256k1Benchmark.kt | 34 +++++++++++++++++++ .../quartz/utils/Secp256k1InstanceOurs.kt | 10 ++++++ 2 files changed, 44 insertions(+) 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 c72ca82a5..9a6496af5 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1Benchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1Benchmark.kt @@ -164,4 +164,38 @@ class Secp256k1Benchmark { 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) + + @Test + fun verifyBatch8Ours() { + benchmarkRule.measureRepeated { + assertTrue(Secp256k1InstanceOurs.verifySchnorrBatch(xOnlyPub, batch8.first, batch8.second)) + } + } + + @Test + fun verifyBatch16Ours() { + benchmarkRule.measureRepeated { + assertTrue(Secp256k1InstanceOurs.verifySchnorrBatch(xOnlyPub, batch16.first, batch16.second)) + } + } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceOurs.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceOurs.kt index 02b3f0036..0f73bd6de 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceOurs.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceOurs.kt @@ -72,6 +72,16 @@ object Secp256k1InstanceOurs { pubKey: ByteArray, ): Boolean = Secp256k1.verifySchnorrFast(signature, hash, pubKey) + /** + * Batch-verify multiple signatures from the same pubkey. + * Uses scalar+point summation — one mulDoubleG for the whole batch. + */ + fun verifySchnorrBatch( + pubKey: ByteArray, + signatures: List, + messages: List, + ): Boolean = Secp256k1.verifySchnorrBatch(pubKey, signatures, messages) + fun privateKeyAdd( first: ByteArray, second: ByteArray,