SPB Git

spb/wp34_uqo

Public
9.8 KB · 187 lines python
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
Files · main
Menu

Theme