# experiments/signature_benchmarks — Research Axis B (ECDSA / BIP340 Schnorr) Benchmarks of legitimate secp256k1 signature implementations on synthetic keys (mandate §5, §9; research questions B2 and B5). Nothing here touches real keys: every secret key, message, aux-rand value and batch weight is drawn from a ChaCha20 RNG seeded with `--seed` (default 34). ## Layout | Path | Role | |---|---| | `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. | | `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`. | | `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). | | `sigbench/src/stats.rs` | Median / quartiles / IQR / mean / sd and a bootstrap (percentile) 95 % CI for a ratio of medians. | | `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_.json`. Standard library only (runs with a bare `python3` on cluster nodes). | | `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`. | | `python_baselines.py` | Pure-Python `src/wp34`, pure-Python `ecdsa` package, and `coincurve` (CFFI → libsecp256k1) timings, 20 ops × 7 reps. | | `summarize.py` | Collects every `bench_*.json` into `results/signature_benchmarks/summary.json` and prints a Markdown digest. | | `../../figures/make_sig_figures.py` | Regenerates `figures/sig_batch_verification.pdf` and `figures/sig_implementations.pdf` from the JSON files. | ## Running ```bash # laptop (repository root) PYTHONPATH=src .venv/bin/python experiments/signature_benchmarks/run_bench.py --seed 34 --label "Laptop (Apple M5 Max)" # cluster node (crate + src/wp34 + data/vectors rsync'ed to ~/wp34-sigbench, cargo in PATH) ssh M3U96a 'cd ~/wp34-sigbench && PYTHONPATH=src python3 experiments/signature_benchmarks/run_bench.py --seed 34 --label "M3U96a (Apple M3 Ultra)"' rsync -a M3U96a:~/wp34-sigbench/results/signature_benchmarks/ results/signature_benchmarks/ .venv/bin/python experiments/signature_benchmarks/summarize.py .venv/bin/python figures/make_sig_figures.py .venv/bin/python -m pytest tests/test_sig_crosscheck.py -q ``` Binary options (`sigbench --help`): `--seed`, `--reps` (7), `--batch-ops` (2000 operations per repetition), `--batch-min-sigs` (≥ 2000 signatures per repetition in the batch benchmark), `--batch-sizes`, `--crosscheck-n` (1000), `--python-crosscheck-n` (200), `--bootstrap` (2000 resamples), `--pippenger-c` (0 = size heuristic), `--ct-lincomb-max-batch` (256). Everything runs on the main thread; macOS has no hard CPU pinning from user space, so "pinned single thread" means single-threaded with no parallelism. ## What is measured 1. **Correctness gates** (all recorded in the JSON under `measurements.correctness_gates`): (a) 19/19 BIP340 vectors with both libraries (verification column + byte-identical signing for the 8 vectors with a secret key); (b) 1000 cross-library checks in each direction for ECDSA (RFC 6979, SHA-256 prehash, low-S) and Schnorr (with aux_rand) — signatures are also byte-identical across libraries because both nonce derivations are deterministic; (c) 200 triples verified and re-signed by the pure-Python references (`python_reference_crosscheck`); (d) own wNAF scalar multiplication agrees with k256 on 64 cases; (e) batch negative controls (one signature with s+1, a flipped message bit, or r+1 inside a batch of 64) rejected by all four MSM back-ends. 2. **Sign / verify** ns per op (median, IQR over 7 reps × 2000 ops): ECDSA and Schnorr, libsecp256k1 and k256. 3. **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), merged into one MSM with 2b + 1 terms (points negated instead of scalars so the weights stay 128-bit), b ∈ {1, …, 2048}, with four back-ends: own Straus, own Pippenger, k256 `lincomb_vartime` (GLV + wNAF), k256 `lincomb` (constant time, up to b = 256). Reported per signature: wall-clock, MSM-only time, operation counts, and speed-up S(b) relative to single verification (k256 `verify_raw` and libsecp256k1) with bootstrap 95 % CI. 4. **Scalar multiplication**: k256 constant-time (GLV, generic point and generator), k256 `mul_by_generator` (precomputed table), k256 vartime (GLV + wNAF), own wNAF-5 double-and-add without GLV (with op counts), libsecp256k1 `ecmult_gen` (`PublicKey::from_secret_key`) and `ecmult_const` (`PublicKey::mul_tweak`). GLV cannot be toggled from either library's public API (see `scalar_mult.glv_note` in the JSON); the only non-GLV data point is our own implementation. 5. **Python baselines** for the implementation spread (pure Python ≈ milliseconds, CFFI ≈ C). ## Headline numbers (seed 34, 2026-11-01) | | Laptop, Apple M5 Max | M3U96a, Apple M3 Ultra | |---|---|---| | ECDSA sign / verify, libsecp256k1 | 23.6 / 12.6 µs | 29.2 / 16.7 µs | | ECDSA sign / verify, k256 | 15.3 / 25.0 µs | 18.8 / 33.5 µs | | Schnorr sign / verify, libsecp256k1 | 20.5 / 13.2 µs | 24.7 / 16.9 µs | | Schnorr sign / verify, k256 | 11.6 / 24.5 µs | 14.8 / 32.5 µs | | 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 | | Batch verify, own Straus (plateau b ≥ 32) | ≈ 12.1–12.7 µs/sig, S ≈ 2.0 | ≈ 16.1 µs/sig, S ≈ 2.0 | | Pure Python `src/wp34` Schnorr verify | 7.4 ms | 9.2 ms | Full digest: `results/signature_benchmarks/summary.json`; interpretation and classification: `research_log/2026-11-01-signatures.md`.