1# experiments/signature_benchmarks — Research Axis B (ECDSA / BIP340 Schnorr)23Benchmarks of legitimate secp256k1 signature implementations on synthetic keys (mandate §5, §9;4research questions B2 and B5). Nothing here touches real keys: every secret key, message, aux-rand5value and batch weight is drawn from a ChaCha20 RNG seeded with `--seed` (default 34).67## Layout89| Path | Role |10|---|---|11| `sigbench/` | Rust crate (binary). Dependencies: `secp256k1` 0.33 (rust-secp256k1 → vendored libsecp256k1 C, features `rand, global-context, std`), `k256` 0.14 (RustCrypto, features `ecdsa, schnorr, arithmetic, precomputed-tables, std, alloc`), `sha2`, `rand`/`rand_chacha` 0.9, `serde(_json)`, `clap`. Release profile: opt-level 3, fat LTO, 1 codegen unit. |12| `sigbench/src/msm.rs` | Own multi-scalar multiplication on `k256::{AffinePoint, ProjectivePoint, Scalar}`: **Straus** (interleaved width-5 wNAF, per-term odd-multiple tables) and **Pippenger** (bucket method, c-bit windows, mixed additions, running-sum trick), both instrumented with counters (projective adds, mixed adds, doublings, identity adds). Also `lift_x`, the BIP340 challenge hash and `batch_verify`. |13| `sigbench/src/vectors.rs` | Parser + checker for `data/vectors/bip340_test_vectors.csv` against both libraries (sign where a secret key is given, verify everywhere, compare with the expected column). |14| `sigbench/src/stats.rs` | Median / quartiles / IQR / mean / sd and a bootstrap (percentile) 95 % CI for a ratio of medians. |15| `run_bench.py` | Thin wrapper: `cargo build --release`, run the binary, run `python_crosscheck.py` on the dumped triples, run `python_baselines.py`, add the `wp34.provenance.record()` block plus rustc/cargo/crate versions (`cargo tree`), write `results/signature_benchmarks/bench_<hostname>.json`. Standard library only (runs with a bare `python3` on cluster nodes). |16| `python_crosscheck.py` | Verifies Rust-produced (seckey, msg, sig) triples with the exact-arithmetic references `src/wp34/ecdsa.py` and `src/wp34/schnorr.py`, re-signs in Python and compares bytes, and rejects a tampered signature. Used by `tests/test_sig_crosscheck.py`. |17| `python_baselines.py` | Pure-Python `src/wp34`, pure-Python `ecdsa` package, and `coincurve` (CFFI → libsecp256k1) timings, 20 ops × 7 reps. |18| `summarize.py` | Collects every `bench_*.json` into `results/signature_benchmarks/summary.json` and prints a Markdown digest. |19| `../../figures/make_sig_figures.py` | Regenerates `figures/sig_batch_verification.pdf` and `figures/sig_implementations.pdf` from the JSON files. |2021## Running2223```bash24# laptop (repository root)25PYTHONPATH=src .venv/bin/python experiments/signature_benchmarks/run_bench.py --seed 34 --label "Laptop (Apple M5 Max)"2627# cluster node (crate + src/wp34 + data/vectors rsync'ed to ~/wp34-sigbench, cargo in PATH)28ssh M3U96a 'cd ~/wp34-sigbench && PYTHONPATH=src python3 experiments/signature_benchmarks/run_bench.py --seed 34 --label "M3U96a (Apple M3 Ultra)"'29rsync -a M3U96a:~/wp34-sigbench/results/signature_benchmarks/ results/signature_benchmarks/3031.venv/bin/python experiments/signature_benchmarks/summarize.py32.venv/bin/python figures/make_sig_figures.py33.venv/bin/python -m pytest tests/test_sig_crosscheck.py -q34```3536Binary options (`sigbench --help`): `--seed`, `--reps` (7), `--batch-ops` (2000 operations per repetition),37`--batch-min-sigs` (≥ 2000 signatures per repetition in the batch benchmark), `--batch-sizes`,38`--crosscheck-n` (1000), `--python-crosscheck-n` (200), `--bootstrap` (2000 resamples), `--pippenger-c`39(0 = size heuristic), `--ct-lincomb-max-batch` (256). Everything runs on the main thread; macOS has no40hard CPU pinning from user space, so "pinned single thread" means single-threaded with no parallelism.4142## What is measured43441. **Correctness gates** (all recorded in the JSON under `measurements.correctness_gates`):45 (a) 19/19 BIP340 vectors with both libraries (verification column + byte-identical signing for the46 8 vectors with a secret key); (b) 1000 cross-library checks in each direction for ECDSA47 (RFC 6979, SHA-256 prehash, low-S) and Schnorr (with aux_rand) — signatures are also byte-identical48 across libraries because both nonce derivations are deterministic; (c) 200 triples verified and49 re-signed by the pure-Python references (`python_reference_crosscheck`); (d) own wNAF scalar50 multiplication agrees with k256 on 64 cases; (e) batch negative controls (one signature with51 s+1, a flipped message bit, or r+1 inside a batch of 64) rejected by all four MSM back-ends.522. **Sign / verify** ns per op (median, IQR over 7 reps × 2000 ops): ECDSA and Schnorr, libsecp256k1 and k256.533. **Batch verification** Σ a_i s_i G = Σ a_i R_i + Σ a_i e_i P_i (a_1 = 1, a_i random 128-bit), merged54 into one MSM with 2b + 1 terms (points negated instead of scalars so the weights stay 128-bit),55 b ∈ {1, …, 2048}, with four back-ends: own Straus, own Pippenger, k256 `lincomb_vartime`56 (GLV + wNAF), k256 `lincomb` (constant time, up to b = 256). Reported per signature: wall-clock,57 MSM-only time, operation counts, and speed-up S(b) relative to single verification (k25658 `verify_raw` and libsecp256k1) with bootstrap 95 % CI.594. **Scalar multiplication**: k256 constant-time (GLV, generic point and generator), k25660 `mul_by_generator` (precomputed table), k256 vartime (GLV + wNAF), own wNAF-5 double-and-add61 without GLV (with op counts), libsecp256k1 `ecmult_gen` (`PublicKey::from_secret_key`) and62 `ecmult_const` (`PublicKey::mul_tweak`). GLV cannot be toggled from either library's public API63 (see `scalar_mult.glv_note` in the JSON); the only non-GLV data point is our own implementation.645. **Python baselines** for the implementation spread (pure Python ≈ milliseconds, CFFI ≈ C).6566## Headline numbers (seed 34, 2026-11-01)6768| | Laptop, Apple M5 Max | M3U96a, Apple M3 Ultra |69|---|---|---|70| ECDSA sign / verify, libsecp256k1 | 23.6 / 12.6 µs | 29.2 / 16.7 µs |71| ECDSA sign / verify, k256 | 15.3 / 25.0 µs | 18.8 / 33.5 µs |72| Schnorr sign / verify, libsecp256k1 | 20.5 / 13.2 µs | 24.7 / 16.9 µs |73| Schnorr sign / verify, k256 | 11.6 / 24.5 µs | 14.8 / 32.5 µs |74| Batch verify, own Pippenger, b = 2048 | 7.9 µs/sig, S = 3.10 [3.05, 3.16] vs k256, 1.67 vs libsecp256k1 | 10.7 µs/sig, S = 3.03 [3.00, 3.04] vs k256, 1.58 vs libsecp256k1 |75| Batch verify, own Straus (plateau b ≥ 32) | ≈ 12.1–12.7 µs/sig, S ≈ 2.0 | ≈ 16.1 µs/sig, S ≈ 2.0 |76| Pure Python `src/wp34` Schnorr verify | 7.4 ms | 9.2 ms |7778Full digest: `results/signature_benchmarks/summary.json`; interpretation and classification:79`research_log/2026-11-01-signatures.md`.80