"""Python baselines for the implementation-choice spread (WP34 Axis B, research question B5): pure-Python `src/wp34` (exact integer arithmetic, no tables), the pure-Python `ecdsa` package, and `coincurve` (CFFI bindings to libsecp256k1). Small samples (default 20 ops x 7 reps), median / IQR reported in ns per op. Synthetic keys only (random.Random(seed)). Usage: PYTHONPATH=src python experiments/signature_benchmarks/python_baselines.py [--seed 34] [--ops 20] [--reps 7] Prints a JSON document on stdout. Libraries that are not importable are reported as null. """ from __future__ import annotations import argparse, hashlib, json, random, statistics, sys, time from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "src")) def quantile(sorted_xs, q): pos = q * (len(sorted_xs) - 1) lo, hi = int(pos // 1), min(int(pos // 1) + 1, len(sorted_xs) - 1) return sorted_xs[lo] + (sorted_xs[hi] - sorted_xs[lo]) * (pos - lo) def timing(reps, ops, fn): fn() # warm-up samples = [] for _ in range(reps): t = time.perf_counter_ns() fn() samples.append((time.perf_counter_ns() - t) / ops) v = sorted(samples) med = quantile(v, 0.5) return { "reps": reps, "ops_per_rep": ops, "ns_per_op_samples": samples, "median_ns": med, "q1_ns": quantile(v, 0.25), "q3_ns": quantile(v, 0.75), "iqr_ns": quantile(v, 0.75) - quantile(v, 0.25), "min_ns": v[0], "mean_ns": statistics.fmean(v), "sd_ns": statistics.stdev(v) if len(v) > 1 else 0.0, "ops_per_s": 1e9 / med, } def main(): ap = argparse.ArgumentParser() ap.add_argument("--seed", type=int, default=34) ap.add_argument("--ops", type=int, default=20) ap.add_argument("--reps", type=int, default=7) ap.add_argument("--batch", type=int, default=16, help="batch size for the pure-Python batch verification") a = ap.parse_args() rng = random.Random(a.seed) from wp34 import secp256k1 as k1 n_ord = k1.n keys = [rng.randrange(1, n_ord) for _ in range(a.ops)] msgs = [bytes(rng.getrandbits(8) for _ in range(32)) for _ in range(a.ops)] auxs = [bytes(rng.getrandbits(8) for _ in range(32)) for _ in range(a.ops)] out = {"seed": a.seed, "ops": a.ops, "reps": a.reps, "python": sys.version.split()[0], "results": {}, "versions": {}} R = out["results"] # ---- pure Python src/wp34 (exact arithmetic, affine double-and-add, no precomputation) from wp34 import ecdsa as D, schnorr as B Qs = [D.pubkey(d) for d in keys] sigs = [D.sign(d, m) for d, m in zip(keys, msgs)] R["wp34_pure_python_ecdsa_sign"] = timing(a.reps, a.ops, lambda: [D.sign(d, m) for d, m in zip(keys, msgs)]) R["wp34_pure_python_ecdsa_verify"] = timing(a.reps, a.ops, lambda: [D.verify(Q, m, s) for Q, m, s in zip(Qs, msgs, sigs)]) assert all(D.verify(Q, m, s) for Q, m, s in zip(Qs, msgs, sigs)) R["wp34_pure_python_ecdsa_verify_shamir"] = timing(a.reps, a.ops, lambda: [D.verify_shamir(Q, m, s) for Q, m, s in zip(Qs, msgs, sigs)]) skb = [d.to_bytes(32, "big") for d in keys] pks = [B.pubkey_gen(s) for s in skb] ssigs = [B.sign(m, s, x) for m, s, x in zip(msgs, skb, auxs)] R["wp34_pure_python_schnorr_sign"] = timing(a.reps, a.ops, lambda: [B.sign(m, s, x) for m, s, x in zip(msgs, skb, auxs)]) R["wp34_pure_python_schnorr_verify"] = timing(a.reps, a.ops, lambda: [B.verify(m, p, s) for m, p, s in zip(msgs, pks, ssigs)]) assert all(B.verify(m, p, s) for m, p, s in zip(msgs, pks, ssigs)) b = min(a.batch, a.ops) items = list(zip(msgs[:b], pks[:b], ssigs[:b])) brng = random.Random(a.seed + 1) R["wp34_pure_python_schnorr_batch_verify_per_sig"] = timing(a.reps, b, lambda: B.batch_verify(items, brng)) R["wp34_pure_python_schnorr_batch_verify_per_sig"]["batch_size"] = b assert B.batch_verify(items, brng) bad = list(items) bad[1] = (bad[1][0], bad[1][1], bad[1][2][:63] + bytes([bad[1][2][63] ^ 1])) out["pure_python_batch_negative_control_rejected"] = not B.batch_verify(bad, brng) # ---- python-ecdsa package (pure Python too, but with its own arithmetic) try: import ecdsa as E out["versions"]["ecdsa"] = E.__version__ sk = [E.SigningKey.from_string(s, curve=E.SECP256k1, hashfunc=hashlib.sha256) for s in skb] vk = [k.get_verifying_key() for k in sk] es = [k.sign_digest_deterministic(m, hashfunc=hashlib.sha256) for k, m in zip(sk, msgs)] R["python_ecdsa_pkg_ecdsa_sign"] = timing(a.reps, a.ops, lambda: [k.sign_digest_deterministic(m, hashfunc=hashlib.sha256) for k, m in zip(sk, msgs)]) R["python_ecdsa_pkg_ecdsa_verify"] = timing(a.reps, a.ops, lambda: [v.verify_digest(s, m) for v, s, m in zip(vk, es, msgs)]) except Exception as e: # pragma: no cover out["versions"]["ecdsa"] = None out["python_ecdsa_pkg_error"] = repr(e) # ---- coincurve (libsecp256k1 through CFFI) try: import coincurve as C out["versions"]["coincurve"] = C.__version__ ck = [C.PrivateKey(s) for s in skb] cpk = [k.public_key for k in ck] cs = [k.sign(m, hasher=None) for k, m in zip(ck, msgs)] # DER, low-S, RFC 6979 R["coincurve_ecdsa_sign_der"] = timing(a.reps, a.ops, lambda: [k.sign(m, hasher=None) for k, m in zip(ck, msgs)]) R["coincurve_ecdsa_verify_der"] = timing(a.reps, a.ops, lambda: [p.verify(s, m, hasher=None) for p, s, m in zip(cpk, cs, msgs)]) assert all(p.verify(s, m, hasher=None) for p, s, m in zip(cpk, cs, msgs)) xo = [C.PublicKeyXOnly.from_secret(s) for s in skb] css = [k.sign_schnorr(m, x) for k, m, x in zip(ck, msgs, auxs)] R["coincurve_schnorr_sign"] = timing(a.reps, a.ops, lambda: [k.sign_schnorr(m, x) for k, m, x in zip(ck, msgs, auxs)]) R["coincurve_schnorr_verify"] = timing(a.reps, a.ops, lambda: [p.verify(s, m) for p, s, m in zip(xo, css, msgs)]) assert all(p.verify(s, m) for p, s, m in zip(xo, css, msgs)) # agreement with the pure-Python reference (bytes must be identical: same aux_rand) out["coincurve_schnorr_identical_to_wp34"] = sum(x == y for x, y in zip(css, ssigs)) except Exception as e: # pragma: no cover out["versions"]["coincurve"] = None out["coincurve_error"] = repr(e) print(json.dumps(out)) if __name__ == "__main__": main()