From 8745fb89e422293b39cef4eaab69101530836e31 Mon Sep 17 00:00:00 2001 From: mleku Date: Sun, 2 Nov 2025 15:45:07 +0000 Subject: [PATCH] Add benchmarking for Schnorr signature verification This commit introduces a new benchmark function, `BenchmarkSchnorrVerify`, in `schnorr_test.go` to evaluate the performance of the Schnorr signature verification process. Additionally, it optimizes the `SchnorrVerify` function in `schnorr.go` by implementing a global precomputed context, reducing overhead during verification calls. The changes aim to enhance performance and provide insights into the efficiency of the verification process. --- schnorr.go | 30 +++++++++++++++++++++++++----- schnorr_test.go | 36 ++++++++++++++++++++++++++++++++++++ verify.go | 25 +++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/schnorr.go b/schnorr.go index 79a4d42..9c2aeb4 100644 --- a/schnorr.go +++ b/schnorr.go @@ -2,6 +2,7 @@ package p256k1 import ( "errors" + "sync" "unsafe" ) @@ -22,6 +23,27 @@ var zeroMask = [32]byte{ 170, 247, 175, 105, 39, 10, 165, 20, } +// Global precomputed context for Schnorr verification +// This eliminates the overhead of context creation per verification call +var ( + schnorrVerifyContext *secp256k1_context + schnorrVerifyContextOnce sync.Once +) + +// initSchnorrVerifyContext initializes the global Schnorr verification context +func initSchnorrVerifyContext() { + schnorrVerifyContext = &secp256k1_context{ + ecmult_gen_ctx: secp256k1_ecmult_gen_context{built: 1}, + declassify: 0, + } +} + +// getSchnorrVerifyContext returns the precomputed Schnorr verification context +func getSchnorrVerifyContext() *secp256k1_context { + schnorrVerifyContextOnce.Do(initSchnorrVerifyContext) + return schnorrVerifyContext +} + // NonceFunctionBIP340 implements BIP-340 nonce generation func NonceFunctionBIP340(nonce32 []byte, msg []byte, key32 []byte, xonlyPk32 []byte, auxRand32 []byte) error { if len(nonce32) != 32 { @@ -295,6 +317,7 @@ func SchnorrVerifyOld(sig64 []byte, msg32 []byte, xonlyPubkey *XOnlyPubkey) bool // SchnorrVerify verifies a Schnorr signature following BIP-340. // This is the new implementation translated from C secp256k1_schnorrsig_verify. +// Uses precomputed context for optimal performance. func SchnorrVerify(sig64 []byte, msg32 []byte, xonlyPubkey *XOnlyPubkey) bool { if len(sig64) != 64 { return false @@ -306,11 +329,8 @@ func SchnorrVerify(sig64 []byte, msg32 []byte, xonlyPubkey *XOnlyPubkey) bool { return false } - // Create a context (required by secp256k1_schnorrsig_verify) - ctx := &secp256k1_context{ - ecmult_gen_ctx: secp256k1_ecmult_gen_context{built: 1}, - declassify: 0, - } + // Use precomputed context (initialized once, reused across calls) + ctx := getSchnorrVerifyContext() // Convert x-only pubkey to secp256k1_xonly_pubkey format var secp_xonly secp256k1_xonly_pubkey diff --git a/schnorr_test.go b/schnorr_test.go index d715dea..3b9dd85 100644 --- a/schnorr_test.go +++ b/schnorr_test.go @@ -236,3 +236,39 @@ func TestSchnorrMultipleSignatures(t *testing.T) { t.Error("with different aux_rand, signatures should differ") } } + +func BenchmarkSchnorrVerify(b *testing.B) { + // Generate keypair + kp, err := KeyPairGenerate() + if err != nil { + b.Fatalf("failed to generate keypair: %v", err) + } + defer kp.Clear() + + // Get x-only pubkey + xonly, err := kp.XOnlyPubkey() + if err != nil { + b.Fatalf("failed to get x-only pubkey: %v", err) + } + + // Create message + msg := make([]byte, 32) + for i := range msg { + msg[i] = byte(i) + } + + // Sign + var sig [64]byte + if err := SchnorrSign(sig[:], msg, kp, nil); err != nil { + b.Fatalf("failed to sign: %v", err) + } + + // Benchmark verification + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if !SchnorrVerify(sig[:], msg, xonly) { + b.Fatal("verification failed") + } + } +} diff --git a/verify.go b/verify.go index ee1941e..c9e6eb6 100644 --- a/verify.go +++ b/verify.go @@ -2,6 +2,8 @@ package p256k1 import ( "crypto/sha256" + "hash" + "sync" "unsafe" ) @@ -667,6 +669,23 @@ func secp256k1_gej_add_zinv_var(r *secp256k1_gej, a *secp256k1_gej, b *secp256k1 secp256k1_gej_add_ge_var(r, a, b, nil) } +// ============================================================================ +// GLOBAL PRE-ALLOCATED RESOURCES +// ============================================================================ + +// Global pre-allocated hash context for challenge computation to avoid allocations +var ( + challengeHashContext hash.Hash + challengeHashContextOnce sync.Once +) + +func getChallengeHashContext() hash.Hash { + challengeHashContextOnce.Do(func() { + challengeHashContext = sha256.New() + }) + return challengeHashContext +} + // ============================================================================ // EC MULTIPLICATION OPERATIONS // ============================================================================ @@ -899,14 +918,16 @@ func secp256k1_schnorrsig_sha256_tagged(sha *secp256k1_sha256) { // secp256k1_schnorrsig_challenge computes challenge hash func secp256k1_schnorrsig_challenge(e *secp256k1_scalar, r32 []byte, msg []byte, msglen int, pubkey32 []byte) { - // Optimized challenge computation - avoid allocations by writing directly to hash + // Optimized challenge computation using pre-allocated hash context to avoid allocations var challengeHash [32]byte // First hash: SHA256(tag) tagHash := sha256.Sum256(bip340ChallengeTag) // Second hash: SHA256(SHA256(tag) || SHA256(tag) || r32 || pubkey32 || msg) - h := sha256.New() + // Use pre-allocated hash context to avoid allocations + h := getChallengeHashContext() + h.Reset() h.Write(tagHash[:]) // SHA256(tag) h.Write(tagHash[:]) // SHA256(tag) again h.Write(r32[:32]) // r32