1"""Python baselines for the implementation-choice spread (WP34 Axis B, research question B5):2pure-Python `src/wp34` (exact integer arithmetic, no tables), the pure-Python `ecdsa` package,3and `coincurve` (CFFI bindings to libsecp256k1). Small samples (default 20 ops x 7 reps),4median / IQR reported in ns per op. Synthetic keys only (random.Random(seed)).56Usage: PYTHONPATH=src python experiments/signature_benchmarks/python_baselines.py [--seed 34] [--ops 20] [--reps 7]7Prints a JSON document on stdout. Libraries that are not importable are reported as null.8"""9from __future__ import annotations10import argparse, hashlib, json, random, statistics, sys, time11from pathlib import Path1213ROOT = Path(__file__).resolve().parents[2]14sys.path.insert(0, str(ROOT / "src"))151617def quantile(sorted_xs, q):18 pos = q * (len(sorted_xs) - 1)19 lo, hi = int(pos // 1), min(int(pos // 1) + 1, len(sorted_xs) - 1)20 return sorted_xs[lo] + (sorted_xs[hi] - sorted_xs[lo]) * (pos - lo)212223def timing(reps, ops, fn):24 fn() # warm-up25 samples = []26 for _ in range(reps):27 t = time.perf_counter_ns()28 fn()29 samples.append((time.perf_counter_ns() - t) / ops)30 v = sorted(samples)31 med = quantile(v, 0.5)32 return {33 "reps": reps, "ops_per_rep": ops, "ns_per_op_samples": samples,34 "median_ns": med, "q1_ns": quantile(v, 0.25), "q3_ns": quantile(v, 0.75),35 "iqr_ns": quantile(v, 0.75) - quantile(v, 0.25), "min_ns": v[0],36 "mean_ns": statistics.fmean(v), "sd_ns": statistics.stdev(v) if len(v) > 1 else 0.0,37 "ops_per_s": 1e9 / med,38 }394041def main():42 ap = argparse.ArgumentParser()43 ap.add_argument("--seed", type=int, default=34)44 ap.add_argument("--ops", type=int, default=20)45 ap.add_argument("--reps", type=int, default=7)46 ap.add_argument("--batch", type=int, default=16, help="batch size for the pure-Python batch verification")47 a = ap.parse_args()48 rng = random.Random(a.seed)49 from wp34 import secp256k1 as k150 n_ord = k1.n51 keys = [rng.randrange(1, n_ord) for _ in range(a.ops)]52 msgs = [bytes(rng.getrandbits(8) for _ in range(32)) for _ in range(a.ops)]53 auxs = [bytes(rng.getrandbits(8) for _ in range(32)) for _ in range(a.ops)]54 out = {"seed": a.seed, "ops": a.ops, "reps": a.reps, "python": sys.version.split()[0], "results": {}, "versions": {}}55 R = out["results"]5657 # ---- pure Python src/wp34 (exact arithmetic, affine double-and-add, no precomputation)58 from wp34 import ecdsa as D, schnorr as B59 Qs = [D.pubkey(d) for d in keys]60 sigs = [D.sign(d, m) for d, m in zip(keys, msgs)]61 R["wp34_pure_python_ecdsa_sign"] = timing(a.reps, a.ops, lambda: [D.sign(d, m) for d, m in zip(keys, msgs)])62 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)])63 assert all(D.verify(Q, m, s) for Q, m, s in zip(Qs, msgs, sigs))64 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)])65 skb = [d.to_bytes(32, "big") for d in keys]66 pks = [B.pubkey_gen(s) for s in skb]67 ssigs = [B.sign(m, s, x) for m, s, x in zip(msgs, skb, auxs)]68 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)])69 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)])70 assert all(B.verify(m, p, s) for m, p, s in zip(msgs, pks, ssigs))71 b = min(a.batch, a.ops)72 items = list(zip(msgs[:b], pks[:b], ssigs[:b]))73 brng = random.Random(a.seed + 1)74 R["wp34_pure_python_schnorr_batch_verify_per_sig"] = timing(a.reps, b, lambda: B.batch_verify(items, brng))75 R["wp34_pure_python_schnorr_batch_verify_per_sig"]["batch_size"] = b76 assert B.batch_verify(items, brng)77 bad = list(items)78 bad[1] = (bad[1][0], bad[1][1], bad[1][2][:63] + bytes([bad[1][2][63] ^ 1]))79 out["pure_python_batch_negative_control_rejected"] = not B.batch_verify(bad, brng)8081 # ---- python-ecdsa package (pure Python too, but with its own arithmetic)82 try:83 import ecdsa as E84 out["versions"]["ecdsa"] = E.__version__85 sk = [E.SigningKey.from_string(s, curve=E.SECP256k1, hashfunc=hashlib.sha256) for s in skb]86 vk = [k.get_verifying_key() for k in sk]87 es = [k.sign_digest_deterministic(m, hashfunc=hashlib.sha256) for k, m in zip(sk, msgs)]88 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)])89 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)])90 except Exception as e: # pragma: no cover91 out["versions"]["ecdsa"] = None92 out["python_ecdsa_pkg_error"] = repr(e)9394 # ---- coincurve (libsecp256k1 through CFFI)95 try:96 import coincurve as C97 out["versions"]["coincurve"] = C.__version__98 ck = [C.PrivateKey(s) for s in skb]99 cpk = [k.public_key for k in ck]100 cs = [k.sign(m, hasher=None) for k, m in zip(ck, msgs)] # DER, low-S, RFC 6979101 R["coincurve_ecdsa_sign_der"] = timing(a.reps, a.ops, lambda: [k.sign(m, hasher=None) for k, m in zip(ck, msgs)])102 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)])103 assert all(p.verify(s, m, hasher=None) for p, s, m in zip(cpk, cs, msgs))104 xo = [C.PublicKeyXOnly.from_secret(s) for s in skb]105 css = [k.sign_schnorr(m, x) for k, m, x in zip(ck, msgs, auxs)]106 R["coincurve_schnorr_sign"] = timing(a.reps, a.ops, lambda: [k.sign_schnorr(m, x) for k, m, x in zip(ck, msgs, auxs)])107 R["coincurve_schnorr_verify"] = timing(a.reps, a.ops, lambda: [p.verify(s, m) for p, s, m in zip(xo, css, msgs)])108 assert all(p.verify(s, m) for p, s, m in zip(xo, css, msgs))109 # agreement with the pure-Python reference (bytes must be identical: same aux_rand)110 out["coincurve_schnorr_identical_to_wp34"] = sum(x == y for x, y in zip(css, ssigs))111 except Exception as e: # pragma: no cover112 out["versions"]["coincurve"] = None113 out["coincurve_error"] = repr(e)114115 print(json.dumps(out))116117118if __name__ == "__main__":119 main()120