feat: add same-pubkey batch Schnorr verification (5-6x throughput)
Add verifySchnorrBatch(pub, signatures, messages) that verifies n signatures from the same public key using scalar and point summation instead of n individual mulDoubleG calls. The batch equation exploits linearity of Schnorr signatures: (Σ sᵢ)·G = (Σ Rᵢ) + (Σ eᵢ)·P This combines n verifications into: - n scalar additions for S = Σsᵢ and E = Σeᵢ (trivial) - n liftX + (n-1) addMixed for R_sum = ΣRᵢ (cheap point additions) - ONE mulDoubleG(S, P, -E) (the expensive EC operation, done once) - Check result - R_sum = O (no toAffine needed) JVM benchmark results (same pubkey, 1000 iterations, 500 warmup): batch( 4): 4.1x faster than individual (40,121 events/s) batch( 8): 5.3x faster (49,637 events/s) batch(16): 5.5x faster (49,815 events/s) batch(32): 5.9x faster (51,272 events/s) This fits the Nostr pattern perfectly: when loading a profile or connecting to a relay, many events from the same followed author arrive together and can be batch-verified. If the batch fails (returns false), the caller falls back to individual verification to identify which signature(s) are invalid. Security: by linearity, individual errors cannot cancel without solving the discrete log. For extra hardening, duplicate events from multiple relays can be verified individually as cross-checks. https://claude.ai/code/session_017UbWduFi1sLUsgVUMUH2nx
This commit is contained in:
+57
@@ -306,6 +306,63 @@ class Secp256k1Benchmark {
|
||||
println(r)
|
||||
}
|
||||
println("=".repeat(90))
|
||||
|
||||
// ==================== Batch verification benchmark ====================
|
||||
// Same pubkey, n events — the typical Nostr pattern (feed from one author)
|
||||
val batchPub = kotlinXOnlyPub
|
||||
for (batchSize in intArrayOf(4, 8, 16, 32)) {
|
||||
val sigs = mutableListOf<ByteArray>()
|
||||
val msgs = mutableListOf<ByteArray>()
|
||||
for (i in 0 until batchSize) {
|
||||
val m = ByteArray(32) { (i * 7 + it).toByte() }
|
||||
sigs.add(
|
||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||
.signSchnorr(m, privKey, auxRand),
|
||||
)
|
||||
msgs.add(m)
|
||||
}
|
||||
// Warmup both paths
|
||||
repeat(500) {
|
||||
for (j in 0 until batchSize) {
|
||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||
.verifySchnorr(sigs[j], msgs[j], batchPub)
|
||||
}
|
||||
}
|
||||
repeat(500) {
|
||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||
.verifySchnorrBatch(batchPub, sigs, msgs)
|
||||
}
|
||||
// Time individual
|
||||
val iters = 1000
|
||||
val indivStart = System.nanoTime()
|
||||
repeat(iters) {
|
||||
for (j in 0 until batchSize) {
|
||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||
.verifySchnorr(sigs[j], msgs[j], batchPub)
|
||||
}
|
||||
}
|
||||
val indivNs = System.nanoTime() - indivStart
|
||||
val indivPerEvent = iters.toLong() * batchSize * 1_000_000_000L / indivNs
|
||||
// Time batch
|
||||
val batchStart = System.nanoTime()
|
||||
repeat(iters) {
|
||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||
.verifySchnorrBatch(batchPub, sigs, msgs)
|
||||
}
|
||||
val batchNs = System.nanoTime() - batchStart
|
||||
val batchPerEvent = iters.toLong() * batchSize * 1_000_000_000L / batchNs
|
||||
val speedup = indivNs.toDouble() / batchNs.toDouble()
|
||||
println(
|
||||
String.format(
|
||||
" batch(%2d): individual %,7d ev/s batch %,7d ev/s speedup %.1fx",
|
||||
batchSize,
|
||||
indivPerEvent,
|
||||
batchPerEvent,
|
||||
speedup,
|
||||
),
|
||||
)
|
||||
}
|
||||
println("=".repeat(90))
|
||||
println()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user