#!/usr/bin/env python3 """ shor-ecdsa-sim.py — toy demonstration of Shor's ECDLP structure on a small curve. This is a CLASSICAL simulation of the structure Shor's algorithm exploits. On real quantum hardware the period extraction is done by the Quantum Fourier Transform; here we brute the structure so you can see it directly. Curve: E: y^2 = x^3 + a*x + b (mod p) Default toy curve: a=2, b=3, p=97. The order of the chosen base point is small (5), so the demo is instantaneous. Try larger primes (still tiny vs secp256k1) to see the cost wall: Shor would crack secp256k1 in poly time on a sufficiently large fault-tolerant quantum computer; classically the brute force is ~sqrt(n). This file ships with: secp256k1 group law, a 5-bit demo, and an estimator that gives the logical-qubit count Shor would need for a curve of size n. Usage: python shor-ecdsa-sim.py demo python shor-ecdsa-sim.py estimate --bits 256 """ from __future__ import annotations from dataclasses import dataclass from math import gcd import argparse import sys @dataclass(frozen=True) class Curve: a: int b: int p: int @dataclass(frozen=True) class Point: x: int | None y: int | None @property def is_infinity(self) -> bool: return self.x is None and self.y is None INF = Point(None, None) def mod_inv(a: int, p: int) -> int: return pow(a % p, -1, p) def point_add(P: Point, Q: Point, E: Curve) -> Point: if P.is_infinity: return Q if Q.is_infinity: return P if P.x == Q.x and (P.y + Q.y) % E.p == 0: return INF if P.x == Q.x: m = (3 * P.x * P.x + E.a) * mod_inv(2 * P.y, E.p) % E.p else: m = (Q.y - P.y) * mod_inv(Q.x - P.x, E.p) % E.p x = (m * m - P.x - Q.x) % E.p y = (m * (P.x - x) - P.y) % E.p return Point(x, y) def scalar_mult(k: int, P: Point, E: Curve) -> Point: R = INF Q = P k = k % (curve_order_brute(E) or (E.p + 1)) while k > 0: if k & 1: R = point_add(R, Q, E) Q = point_add(Q, Q, E) k >>= 1 return R def curve_order_brute(E: Curve) -> int: # Only viable for tiny curves. Used for the demo. count = 1 # point at infinity for x in range(E.p): rhs = (x * x * x + E.a * x + E.b) % E.p if rhs == 0: count += 1 continue # Euler's criterion if pow(rhs, (E.p - 1) // 2, E.p) == 1: count += 2 return count def find_base_point(E: Curve) -> Point: for x in range(E.p): rhs = (x ** 3 + E.a * x + E.b) % E.p for y in range(E.p): if (y * y) % E.p == rhs: return Point(x, y) raise ValueError("no point found on curve") def shor_ecdlp_classical(P: Point, Q: Point, n: int, E: Curve) -> int | None: """ Classically search for (a, b) with a*Q == b*G ⇒ d ≡ b * a^-1 mod n. On a fault-tolerant quantum computer this is what the QFT extracts in O((log n)^3) time. Classically, this brute is O(n^2) — fine for demo n, infeasible for secp256k1 n ≈ 2^256. """ for a in range(1, n): aQ = scalar_mult(a, Q, E) for b in range(1, n): bG = scalar_mult(b, P, E) if aQ.x == bG.x and aQ.y == bG.y: # a*d ≡ b (mod n) → d = b * a^-1 mod n if gcd(a, n) != 1: continue d = (b * mod_inv(a, n)) % n return d return None def estimate_logical_qubits(bits: int) -> dict: """ Rough Shor resource estimate for ECDLP of an n-bit curve. Based on Roetteler et al. 2017 — extrapolated linearly for didactic use. """ # ~9n + 2 qubits for the modular-arithmetic register qubits = 9 * bits + 2 # Roughly 10 * n^3 Toffoli gates (order-of-magnitude) toffoli = 10 * (bits ** 3) # Surface code overhead at 1e-3 physical error: ~10000 phys / logical phys = qubits * 10_000 return { "curve_bits": bits, "logical_qubits": qubits, "toffoli_gates_orderof": toffoli, "physical_qubits_orderof": phys, "wallclock_hours_orderof": round(toffoli / (10 ** 4 * 3600), 1), } def demo() -> None: E = Curve(a=2, b=3, p=97) n = curve_order_brute(E) G = find_base_point(E) d_secret = 7 % n Q = scalar_mult(d_secret, G, E) print(f"curve E: y^2 = x^3 + {E.a}x + {E.b} (mod {E.p})") print(f"#E = {n}") print(f"G = {G}") print(f"private d = {d_secret}") print(f"public Q = {Q}") recovered = shor_ecdlp_classical(G, Q, n, E) print(f"shor (classical sim) recovered d = {recovered}") assert recovered == d_secret def main(): p = argparse.ArgumentParser() sub = p.add_subparsers(dest="cmd", required=True) sub.add_parser("demo") e = sub.add_parser("estimate") e.add_argument("--bits", type=int, required=True) args = p.parse_args() if args.cmd == "demo": demo() elif args.cmd == "estimate": for k, v in estimate_logical_qubits(args.bits).items(): print(f"{k:30s} {v}") if __name__ == "__main__": main()