1"""Regenerate the Axis B signature figures from results/signature_benchmarks/bench_*.json.23 .venv/bin/python figures/make_sig_figures.py45Outputs (PDF, serif, publication quality):6 figures/sig_batch_verification.pdf - per-signature time vs batch size for every MSM algorithm,7 one panel per host, single-verify reference lines (k256 and8 libsecp256k1); second row: speedup S(b) with bootstrap 95 % CI.9 figures/sig_implementations.pdf - sign / verify ns per op across implementations (pure Python10 src/wp34, python-ecdsa, coincurve, k256, libsecp256k1), log scale.11Pass --copy-to-paper to also copy them into paper/figures/.12"""13from __future__ import annotations14import json, shutil15from pathlib import Path1617import matplotlib18matplotlib.use("Agg")19import matplotlib.pyplot as plt # noqa: E40220from matplotlib.ticker import FuncFormatter, LogLocator # noqa: E4022122ROOT = Path(__file__).resolve().parents[1]23RES = ROOT / "results" / "signature_benchmarks"24OUT = ROOT / "figures"2526# fixed categorical order (validated with the dataviz palette checker: light surface, all pairs pass)27C = {"pippenger": "#2a78d6", "straus_wnaf5": "#eb6834", "k256_lincomb_vartime": "#1baf7a",28 "k256_lincomb_ct": "#6250d6", "single": "#52514e"}29ALG_LABEL = {"pippenger": "own Pippenger (bucket, mixed adds)", "straus_wnaf5": "own Straus (wNAF-5, interleaved)",30 "k256_lincomb_vartime": "k256 lincomb_vartime (GLV + wNAF)", "k256_lincomb_ct": "k256 lincomb (constant time)"}31ALG_ORDER = ["pippenger", "straus_wnaf5", "k256_lincomb_vartime", "k256_lincomb_ct"]3233plt.rcParams.update({34 "font.family": "serif", "font.serif": ["Times New Roman", "Times", "DejaVu Serif"], "font.size": 9,35 "axes.titlesize": 9.5, "axes.labelsize": 9, "legend.fontsize": 7.5, "xtick.labelsize": 8, "ytick.labelsize": 8,36 "axes.spines.top": False, "axes.spines.right": False, "axes.grid": True, "grid.color": "#e4e3df",37 "grid.linewidth": 0.5, "axes.edgecolor": "#8a8984", "axes.linewidth": 0.6, "pdf.fonttype": 42,38 "figure.dpi": 150, "savefig.bbox": "tight",39})404142def load():43 docs = [json.loads(f.read_text()) for f in sorted(RES.glob("bench_*.json"))]44 docs.sort(key=lambda d: d["label"])45 return docs464748def short_label(d):49 return f"{d['label']}\n{d['host']['cpu']}, {d['host']['cores']} cores, macOS {d['host']['os'].split('(')[-1].rstrip(')')}"505152def fig_batch(docs):53 n = len(docs)54 fig, axes = plt.subplots(2, n, figsize=(3.4 * n + 0.6, 5.6), sharex=True, sharey="row", squeeze=False)55 for j, d in enumerate(docs):56 m = d["measurements"]["batch_verification"]57 ax, ax2 = axes[0][j], axes[1][j]58 ref = m["single_verify_reference_ns"]59 ax.axhline(ref["libsecp256k1_schnorr_verify"] / 1e3, color=C["single"], lw=1.0, ls="--",60 label="single verify, libsecp256k1 (reference)" if j == 0 else None)61 ax.axhline(ref["k256_verify_raw"] / 1e3, color=C["single"], lw=1.0, ls=":",62 label="single verify, k256 verify_raw (reference)" if j == 0 else None)63 for alg in ALG_ORDER:64 rows = sorted([r for r in m["results"] if r["algorithm"] == alg], key=lambda r: r["batch_size"])65 if not rows:66 continue67 xs = [r["batch_size"] for r in rows]68 ys = [r["per_signature"]["median_ns"] / 1e3 for r in rows]69 lo = [r["per_signature"]["q1_ns"] / 1e3 for r in rows]70 hi = [r["per_signature"]["q3_ns"] / 1e3 for r in rows]71 ax.fill_between(xs, lo, hi, color=C[alg], alpha=0.18, lw=0)72 ax.plot(xs, ys, color=C[alg], lw=1.6, marker="o", ms=3.2, label=ALG_LABEL[alg])73 sp = [r["speedup_vs_k256_single_verify"] for r in rows]74 ax2.fill_between(xs, [s["ci95_low"] for s in sp], [s["ci95_high"] for s in sp], color=C[alg], alpha=0.18, lw=0)75 ax2.plot(xs, [s["point"] for s in sp], color=C[alg], lw=1.6, marker="o", ms=3.2)76 if alg == "pippenger":77 sp2 = [r["speedup_vs_libsecp256k1_single_verify"] for r in rows]78 ax2.plot(xs, [s["point"] for s in sp2], color=C[alg], lw=1.2, ls="--", marker="s", ms=2.8,79 label="own Pippenger vs libsecp256k1 single verify")80 last = rows[-1]81 ax.annotate(f"{last['per_signature']['median_ns']/1e3:.1f} µs/sig", (xs[-1], ys[-1]), xytext=(-6, 6),82 textcoords="offset points", ha="right", va="bottom", fontsize=7, color=C[alg])83 ax2.axhline(1.0, color=C["single"], lw=0.8, ls="-")84 ax.set_xscale("log", base=2)85 ax.set_yscale("log")86 ax.set_ylim(5, 160)87 ax.set_title(short_label(d), loc="left")88 ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v:g}"))89 ax.yaxis.set_minor_locator(LogLocator(base=10, subs=(2, 5)))90 ax.yaxis.set_minor_formatter(FuncFormatter(lambda v, _: f"{v:g}"))91 ax2.set_xlabel("batch size $b$ (signatures per batch)")92 ax2.set_xticks([1, 4, 16, 64, 256, 1024, 2048])93 ax2.set_xticklabels(["1", "4", "16", "64", "256", "1024", "2048"])94 ax2.set_ylim(0, 3.6)95 if j == 0:96 ax.set_ylabel("time per signature (µs, median; band = IQR)")97 ax2.set_ylabel("speed-up $S(b)$ vs single k256 verify\n(band = bootstrap 95 % CI)")98 else:99 ax.tick_params(labelleft=False)100 ax2.tick_params(labelleft=False)101 h1, l1 = axes[0][0].get_legend_handles_labels()102 h2, l2 = axes[1][0].get_legend_handles_labels()103 fig.legend(h1 + h2, l1 + l2, loc="lower center", ncol=3, frameon=False, bbox_to_anchor=(0.5, -0.06))104 fig.suptitle("BIP340 batch verification on secp256k1 (k256 group arithmetic; 128-bit random weights, $a_1 = 1$; synthetic keys, seed 34)",105 fontsize=9.5, y=1.0)106 fig.tight_layout(h_pad=0.6)107 fig.savefig(OUT / "sig_batch_verification.pdf")108 plt.close(fig)109110111def fig_impl(docs):112 impls = [ # (label, ecdsa_sign, ecdsa_verify, schnorr_sign, schnorr_verify) -> keys in sign_verify or python baselines113 ("pure Python\nsrc/wp34", "py:wp34_pure_python_ecdsa_sign", "py:wp34_pure_python_ecdsa_verify",114 "py:wp34_pure_python_schnorr_sign", "py:wp34_pure_python_schnorr_verify"),115 ("python-ecdsa\n(pure Python)", "py:python_ecdsa_pkg_ecdsa_sign", "py:python_ecdsa_pkg_ecdsa_verify", None, None),116 ("coincurve\n(CFFI, libsecp256k1)", "py:coincurve_ecdsa_sign_der", "py:coincurve_ecdsa_verify_der",117 "py:coincurve_schnorr_sign", "py:coincurve_schnorr_verify"),118 ("k256 0.14\n(Rust)", "rs:ecdsa_sign_k256", "rs:ecdsa_verify_k256", "rs:schnorr_sign_k256", "rs:schnorr_verify_k256"),119 ("libsecp256k1\n(C, via Rust)", "rs:ecdsa_sign_libsecp256k1", "rs:ecdsa_verify_libsecp256k1",120 "rs:schnorr_sign_libsecp256k1", "rs:schnorr_verify_libsecp256k1"),121 ]122 series = [("ECDSA sign", 1, "#2a78d6", ""), ("ECDSA verify", 2, "#2a78d6", "////"),123 ("Schnorr sign", 3, "#eb6834", ""), ("Schnorr verify", 4, "#eb6834", "////")]124 n = len(docs)125 fig, axes = plt.subplots(1, n, figsize=(4.6 * n + 0.4, 3.7), sharey=True, squeeze=False)126 for j, d in enumerate(docs):127 ax = axes[0][j]128 sv = d["measurements"]["sign_verify"]129 py = (d["measurements"].get("python_baselines") or {}).get("results") or {}130131 def val(key):132 if key is None:133 return None134 src, k = key.split(":", 1)135 t = sv.get(k) if src == "rs" else py.get(k)136 return None if t is None else t["median_ns"]137138 w = 0.2139 for s_i, (sname, col, color, hatch) in enumerate(series):140 xs, ys = [], []141 for i, imp in enumerate(impls):142 v = val(imp[col])143 if v is not None:144 xs.append(i + (s_i - 1.5) * w)145 ys.append(v)146 bars = ax.bar(xs, ys, width=w * 0.92, color=color, hatch=hatch, edgecolor="white", linewidth=0.6,147 label=sname if j == 0 else None, alpha=0.95 if not hatch else 0.75)148 for b_, v in zip(bars, ys):149 txt = f"{v/1e3:.0f}" if v >= 1e5 else f"{v/1e3:.1f}"150 ax.text(b_.get_x() + b_.get_width() / 2, v * 1.12, txt, ha="center", va="bottom", fontsize=5.8, rotation=90, color="#0b0b0b")151 for i, imp in enumerate(impls):152 if all(val(k) is None for k in imp[1:]):153 ax.text(i, 2.5e4, "not installed\non this host", ha="center", va="center", fontsize=6.5, color="#52514e")154 ax.set_yscale("log")155 ax.set_ylim(3e3, 5e7)156 ax.set_xticks(range(len(impls)))157 ax.set_xticklabels([i[0] for i in impls], fontsize=6.8)158 ax.set_title(short_label(d), loc="left")159 ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v/1e3:g}"))160 if j == 0:161 ax.set_ylabel("time per operation (µs, median of ≥ 7 reps; log scale)")162 ax.grid(axis="x", visible=False)163 fig.legend(loc="lower center", ncol=4, frameon=False, bbox_to_anchor=(0.5, -0.06))164 fig.suptitle("Implementation spread for secp256k1 signatures (32-byte prehash; Rust: 2000 ops/rep, Python: 20 ops/rep; labels in µs)",165 fontsize=9.5, y=1.0)166 fig.tight_layout()167 fig.savefig(OUT / "sig_implementations.pdf")168 plt.close(fig)169170171def main():172 import sys173 docs = load()174 if not docs:175 raise SystemExit("no results/signature_benchmarks/bench_*.json")176 fig_batch(docs)177 fig_impl(docs)178 paper = ROOT / "paper" / "figures"179 if "--copy-to-paper" in sys.argv and paper.exists():180 for f in ("sig_batch_verification.pdf", "sig_implementations.pdf"):181 shutil.copy(OUT / f, paper / f)182 print("wrote", OUT / "sig_batch_verification.pdf", OUT / "sig_implementations.pdf")183184185if __name__ == "__main__":186 main()187