SPB Git

spb/wp34_uqo

Public
9.4 KB

# 2026-11-01 — Research Axis B: ECDSA / BIP340 Schnorr benchmarks (questions B2, B5)

Research question. B2: can BIP340 verification be accelerated without changing consensus semantics (batch verification)? B5: which implementation choices produce measurable differences? Null hypothesis (B2). Batch verification gives no per-signature saving over single verification at any batch size. Alternative. The per-signature cost decreases with b and saturates.

Setup. experiments/signature_benchmarks/sigbench (Rust 1.95/1.96, rust-secp256k1 0.33.1 → libsecp256k1 via secp256k1-sys 0.14.1, k256 0.14.0, elliptic-curve 0.14.3, ecdsa 0.17.0, wnaf 0.14.1), seed 34, 7 repetitions × ≥ 2000 operations per measurement, single thread, bootstrap CI with 2000 resamples. Two hosts: laptop (Apple M5 Max, 18 cores, macOS 27.0) and cluster node M3U96a (Apple M3 Ultra, 28 cores, macOS 26.6). Results: results/signature_benchmarks/bench_MacBookPro-de-simon-pierre-2.json, bench_MacStudnpierre2.json, summary.json, logs run_laptop.log, run_m3u96a.log. Commands are stored in each JSON (command, params.rust_command). Synthetic keys only.

# Correctness gates (both hosts, identical because the RNG is seeded)

  • BIP340 official vectors: 19/19 verification outcomes and 8/8 byte-identical signatures with both libsecp256k1 and k256 (vectors 5–14 with invalid keys / r ≥ p / s ≥ n / wrong parity all rejected as expected).
  • Cross-library: 1000/1000 k256-signed ECDSA signatures verified by libsecp256k1 and vice versa; same for Schnorr; 1000/1000 byte-identical in both schemes (RFC 6979 nonces for ECDSA, the BIP340 aux-rand nonce derivation for Schnorr — deterministic by construction).
  • Python reference (src/wp34, exact integer arithmetic): 200/200 Rust signatures verified for all four (scheme × library) combinations, 200/200 re-signed identically in pure Python, 200/200 tampered signatures rejected. tests/test_sig_crosscheck.py passes (2 tests, also checks the Python reference against the 19 vectors independently of Rust).
  • Own MSM: Straus with one term agrees with k256 on 64 scalar multiplications; batch negative controls (s+1, flipped message bit, r+1 inside a batch of 64) rejected by all four MSM back-ends, while the unmodified batch is accepted.

# Findings

# F1 — Batch verification speed-up (classification: known_reproduced)

Per-signature cost of Σ a_i s_i G = Σ a_i R_i + Σ a_i e_i P_i with a_1 = 1 and random 128-bit a_i, as a function of b (laptop; M3U96a shows the same shape at ×1.3 cost):

b own Pippenger own Straus (wNAF-5) k256 lincomb_vartime k256 lincomb (ct)
1 91.9 µs (S 0.27) 30.4 µs (S 0.81) 25.1 µs (S 0.97) 38.5 µs (S 0.64)
16 24.9 µs (0.99) 12.9 µs (1.90) 14.8 µs (1.65) 22.5 µs (1.09)
128 12.8 µs (1.92) 12.2 µs (2.01) 15.3 µs (1.60) 21.9 µs (1.12)
256 11.3 µs (2.16) 12.4 µs (1.98) 15.0 µs (1.63) 21.8 µs (1.12)
1024 8.6 µs (2.84) 12.7 µs (1.93) 15.8 µs (1.55) —
2048 7.9 µs (3.10 [3.05, 3.16]) 13.4 µs (1.82) 17.1 µs (1.43) —

S = speed-up versus a single k256 verify_raw (24.5 µs); versus libsecp256k1's single verify (13.2 µs) the best point is 1.67 [1.65, 1.68] (M3U96a: 3.03 [3.00, 3.04] and 1.58). The null hypothesis is rejected for every back-end at b ≥ 4 (CIs exclude 1); the gain saturates. This is the expected behaviour described in the BIP340 text ("batch verification ... up to a factor of 2") and measured for Ed25519 by Bernstein, Duif, Lange, Schwabe and Yang (2012); our numbers reproduce the known result in k256 arithmetic and do not constitute a new algorithm. Op counts (per signature, 2b + 1 terms): Straus plateaus at ≈ 79 projective additions + 2 doublings per signature exactly as predicted by wNAF-5 density (≈ 256/6 + 7 table additions for the 256-bit P_i term, ≈ 128/6 + 7 for the 128-bit R_i term); Pippenger at b = 2048 (c = 8) uses 47.8 mixed additions + 8.0 additions + 0.1 doublings per signature, i.e. about 0.7× the group operations of Straus, which is the whole source of its advantage.

# F2 — The attainable speed-up is capped by the per-signature non-MSM work (empirical_observation)

The MSM-only time falls to 4.9 µs/sig (laptop, b = 2048) but the total is 7.9 µs/sig: ≈ 3.0 µs per signature (3.6 µs on M3U96a) is spent outside the MSM — lift_x(r) (one square root = a 255-bit field exponentiation), the tagged SHA-256 challenge hash and the scalar parse. Single verification pays the same hash but no square root for R (it compares x-coordinates). Hence the asymptotic speed-up is bounded by (single verify) / (non-MSM floor + MSM/sig), roughly 4–5× in this arithmetic even with an ideal MSM — consistent with the saturation seen above. This is a well-understood cost model, reported here with measured constants.

# F3 — Library choice matters more than batching (empirical_observation)

  • libsecp256k1 verifies ≈ 2× faster than k256 (ECDSA 12.6 vs 25.0 µs; Schnorr 13.2 vs 24.5 µs on the laptop), consistent with its 5×52-bit field representation, GLV + wNAF strategy with effective-affine precomputation and years of tuning. Conversely k256 signs faster than libsecp256k1 (ECDSA 15.3 vs 23.6 µs; Schnorr 11.6 vs 20.5 µs): rust-secp256k1's signing path goes through the blinded, constant-time ecmult_gen (19.8 µs for a generator multiplication through PublicKey::from_secret_key) whereas k256's mul_by_generator uses a 30 kB fixed table (8.3 µs; 6.6 µs in variable time). This was not anticipated and is worth stating in the paper as an implementation-choice effect, not a mathematical one.
  • Scalar multiplication (laptop): own wNAF-5 double-and-add without GLV 21.1 µs (257 doublings + 52 additions); k256 constant-time with GLV 16.4–17.2 µs; k256 variable-time GLV + wNAF 14.0 µs; libsecp256k1 ecmult_const 11.5 µs. The GLV endomorphism therefore buys ≈ 1.3–1.5× here, in line with Gallant–Lambert–Vanstone (2001). GLV is not switchable from either public API; this is stated in the JSON (scalar_mult.glv_note).
  • k256's own lincomb_vartime is slower than our naive Straus for b ≥ 8 and degrades for large b (17.1 µs/sig at b = 2048 vs 15.0 at b = 256); its per-term wNAF table precomputation (doubled by the GLV split) dominates when the terms are used once. Surprising at first sight, but it is a table-cost effect, not an arithmetic one.
  • Implementation spread for the same mathematics (Schnorr verify, laptop): pure Python src/wp34 7.4 ms → python-ecdsa (ECDSA) 419 µs → coincurve (CFFI) 12.5 µs → k256 24.5 µs → libsecp256k1 13.2 µs: a factor ≈ 560 between the pure-Python reference and C. coincurve adds ≈ 0–8 µs of FFI/object overhead (its sign_schnorr creates a keypair per call: 28.3 µs vs 20.5 µs in Rust).
  • Host effect: the M5 Max is 1.25–1.35× faster than the M3 Ultra on every single-threaded operation; the speed-up curves S(b) are the same within CI on both hosts, as expected for a ratio.

# F4 — ECDSA vs Schnorr cost (known_reproduced)

In libsecp256k1 the two verifications cost the same within 5 % (12.6 vs 13.2 µs): both are one double-scalar multiplication plus hashing; Schnorr's extra cost is the tagged hash and the x-only key lift, ECDSA's the modular inversion of s. Schnorr signing is cheaper than ECDSA signing in both libraries (no inversion). Nothing here contradicts the standard comparison (BIP340 motivation section).

# Classification summary

finding classification note
batch verification speed-up (F1) known_reproduced BIP340 text; Bernstein et al. 2012 (Ed25519)
speed-up cap from lift_x + hash floor (F2) empirical_observation measured constants, standard cost model
library / implementation spread, k256 signs faster, libsecp256k1 verifies faster (F3) empirical_observation implementation, not mathematics
ECDSA ≈ Schnorr verification cost (F4) known_reproduced

No algebraic shortcut was sought or found in this family; every measured gain is a constant factor from a known algorithm (Straus, Pippenger, GLV) and is not a cryptographic weakness.

# Negative control and reproducibility

  • Negative controls: three corrupted batches (s+1, message bit flip, r+1) rejected by all back-ends; pure-Python batch_verify likewise rejects a flipped bit (pure_python_batch_negative_control_rejected).
  • Reproduce: PYTHONPATH=src .venv/bin/python experiments/signature_benchmarks/run_bench.py --seed 34 (≈ 20 s Rust + 11 s Python cross-check + 35 s Python baselines on the laptop), then summarize.py, figures/make_sig_figures.py. The crosscheck triple files produced on both hosts are byte-identical.

# Files produced today

experiments/signature_benchmarks/{sigbench/, run_bench.py, python_crosscheck.py, python_baselines.py, summarize.py, README.md}, results/signature_benchmarks/{bench_MacBookPro-de-simon-pierre-2.json, bench_MacStudnpierre2.json, summary.json, crosscheck_triples_*.json, run_*.log}, figures/{make_sig_figures.py, sig_batch_verification.pdf, sig_implementations.pdf}, tests/test_sig_crosscheck.py.

Open items for the paper: cite bernstein2012highspeed, bip340, gallant2001glv, pippenger1976, straus1964 (verify keys exist in paper/references.bib before use); a hypothesis file for B2 (hypotheses/) was not created here because this run was restricted to the directories above.

Files · main
Menu

Theme