#!/usr/bin/env python3
# qcrack rerun kit for QC-0003 (4-BIT, curve y^2 = x^3 + 2x + 1 over F_11, key k = 11).
# Runs the published circuit on YOUR OWN IBM Quantum account (free Open plan works) and recomputes hits with the same
# post-processing qcrack uses, next to a fresh /dev/urandom control.
#   pip install qiskit qiskit-ibm-runtime qiskit-qasm3-import
#   IBM_QUANTUM_TOKEN=... IBM_QUANTUM_INSTANCE=... python rerun-QC-0003.py [backend]
import hashlib, json, os, sys, urllib.request
SITE = "https://qcrack.live"
ID, N, K, SHOTS = "QC-0003", 4, 11, 4096
QASM_SHA256 = "7d4346fc1be174f8ebf06ed34aa2dd11b535a133f54ed39a8767b6009567d99a"
def fetch(path):
    with urllib.request.urlopen(SITE + path, timeout=30) as r:
        return r.read().decode()
def inv(x, n):
    t, nt, r, nr = 0, 1, n, x % n
    while nr:
        q = r // nr; t, nt = nt, t - q * nt; r, nr = nr, r - q * nr
    return None if r != 1 else t % n
def candidate(bs):
    s = "".join(bs.split()); n = 2 ** N
    if len(s) != 2 * N: return None
    v, u = int(s[:N], 2), int(s[N:], 2); ui = inv(u, n)
    return None if ui is None else (v * ui) % n
def hits(counts):
    return sum(c for b, c in counts.items() if candidate(b) == K)
qasm = fetch(f"/a/{ID}/circuit.qasm")
assert hashlib.sha256(qasm.encode()).hexdigest() == QASM_SHA256, "circuit.qasm does not match the published sha256"
from qiskit import qasm3
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2
svc = QiskitRuntimeService(channel="ibm_quantum_platform", token=os.environ["IBM_QUANTUM_TOKEN"], instance=os.environ.get("IBM_QUANTUM_INSTANCE") or None)
be = svc.backend(sys.argv[1]) if len(sys.argv) > 1 else svc.least_busy(operational=True, simulator=False)
isa = generate_preset_pass_manager(optimization_level=3, backend=be).run(qasm3.loads(qasm))
job = SamplerV2(mode=be).run([isa], shots=SHOTS)
print("submitted", job.job_id(), "on", be.name, "- waiting for the queue...")
counts = job.result()[0].data.c.get_counts()
draws = []
raw = os.urandom(((2 * N + 7) // 8) * SHOTS)
for i in range(SHOTS):
    v = int.from_bytes(raw[i * ((2 * N + 7) // 8):(i + 1) * ((2 * N + 7) // 8)], "big") & ((1 << (2 * N)) - 1)
    draws.append(format(v, f"0{2 * N}b"))
uc = {}
for d in draws: uc[d] = uc.get(d, 0) + 1
q, u = hits(counts), hits(uc)
print(f"your chip: {q} / {SHOTS} hits ({100 * q / SHOTS:.1f}%)   your coin flip: {u} / {SHOTS} ({100 * u / SHOTS:.1f}%)")
print("KEY RECOVERED" if q >= 2 * u and q > 0 else "NO EDGE OVER RANDOM", f"(threshold 2x)")
