1"""End-to-end tests of the native ECDLP engine (experiments/synthetic_ecdlp/rho_engine).23Runs the release binary on every instance of tests/fixtures/ecdlp_instances.json (generated by4experiments/synthetic_ecdlp/rho_engine/scripts/gen_fixtures.py from the pure-Python reference5src/wp34/ec.py) with every algorithm / option combination and checks d against the fixture,6plus d·G == Q recomputed in Python. Build first: cargo build --release --manifest-path7experiments/synthetic_ecdlp/rho_engine/Cargo.toml8"""9import json, pathlib, subprocess, sys10import pytest1112ROOT = pathlib.Path(__file__).resolve().parents[1]13sys.path.insert(0, str(ROOT / "src"))14from wp34.ec import Curve # noqa: E4021516CRATE = ROOT / "experiments" / "synthetic_ecdlp" / "rho_engine"17BIN = CRATE / "target" / "release" / "rho_engine"18FIXTURE = ROOT / "tests" / "fixtures" / "ecdlp_instances.json"1920pytestmark = pytest.mark.skipif(not BIN.exists(), reason="rho_engine release binary not built")212223def _instances():24 if not FIXTURE.exists():25 return []26 return json.loads(FIXTURE.read_text())["instances"]272829INSTANCES = _instances()30GENERIC = [i for i in INSTANCES if not i["j0"]]31J0 = [i for i in INSTANCES if i["j0"]]3233COMBOS = {34 "bsgs": ["--alg", "bsgs"],35 "rho-floyd": ["--alg", "rho", "--cycle", "floyd"],36 "rho-brent": ["--alg", "rho", "--cycle", "brent"],37 "rho-dp-1t": ["--alg", "rho", "--threads", "1"],38 "rho-dp-4t": ["--alg", "rho", "--threads", "4"],39 "rho-neg": ["--alg", "rho", "--neg"],40 "rho-mixed": ["--alg", "rho", "--mixed", "4"],41 "kangaroo": ["--alg", "kangaroo", "--lo", "0"], # --hi defaults to n-142}43J0_COMBOS = {44 "rho-aut3": ["--alg", "rho", "--aut3"],45 "rho-neg-aut3": ["--alg", "rho", "--neg", "--aut3"],46 "rho-neg-aut3-brent": ["--alg", "rho", "--neg", "--aut3", "--cycle", "brent"],47}484950def solve(inst, extra, seed=1, expect_ok=True):51 args = [str(BIN), "solve"]52 for k in ("p", "a", "b", "n", "gx", "gy", "qx", "qy"):53 args += [f"--{k}", inst[k]]54 if "beta" in inst:55 args += ["--beta", inst["beta"], "--lambda", inst["lambda"]]56 args += ["--seed", str(seed), "--json", "--expected-d", inst["d"]] + extra57 proc = subprocess.run(args, capture_output=True, text=True, timeout=600)58 out = json.loads(proc.stdout)59 if expect_ok:60 assert proc.returncode == 0, (proc.stdout, proc.stderr)61 return out626364def check_output(inst, out):65 assert out["ok"] is True66 assert out["match"] is True67 assert out["d"] == inst["d"]68 # independent verification in pure Python69 E = Curve(int(inst["p"]), int(inst["a"]), int(inst["b"]), n=int(inst["n"]))70 G = (int(inst["gx"]), int(inst["gy"]))71 assert E.mul(int(out["d"]), G) == (int(inst["qx"]), int(inst["qy"]))72 for key in ("ops", "iterations", "dp_count", "dp_bytes", "fruitless_cycles", "threads", "wall_s", "cpu_s",73 "table_entries", "peak_rss_bytes", "params"):74 assert key in out, key75 assert set(out["ops"]) == {"add", "dbl", "inv", "mul"}76 assert out["peak_rss_bytes"] > 0777879@pytest.mark.parametrize("combo", sorted(COMBOS))80@pytest.mark.parametrize("inst", INSTANCES, ids=[i["label"] for i in INSTANCES])81def test_all_instances_all_algorithms(inst, combo):82 out = solve(inst, COMBOS[combo])83 check_output(inst, out)84 if combo == "bsgs":85 assert out["table_entries"] > 086 if combo == "rho-dp-4t":87 assert out["threads"] == 488 if combo == "rho-neg":89 assert out["params"]["neg"] is True909192@pytest.mark.parametrize("combo", sorted(J0_COMBOS))93@pytest.mark.parametrize("inst", J0, ids=[i["label"] for i in J0])94def test_j0_instances_automorphism_classes(inst, combo):95 out = solve(inst, J0_COMBOS[combo])96 check_output(inst, out)97 assert out["params"]["aut3"] is True9899100@pytest.mark.parametrize("inst", GENERIC[::7] + J0[::5], ids=lambda i: i["label"])101def test_single_thread_runs_are_deterministic(inst):102 for extra in (["--alg", "rho"], ["--alg", "rho", "--neg"], ["--alg", "kangaroo"], ["--alg", "rho", "--cycle", "brent"]):103 a = solve(inst, extra, seed=42)104 b = solve(inst, extra, seed=42)105 for key in ("d", "iterations", "ops", "dp_count", "fruitless_cycles"):106 assert a[key] == b[key], key107108109def test_wrong_beta_aborts():110 inst = J0[0]111 bad = dict(inst, beta=str(int(inst["beta"]) + 1))112 out = solve(bad, ["--alg", "rho", "--aut3"], expect_ok=False)113 assert out["ok"] is False and "error" in out114115116def test_kangaroo_interval_and_memory_override():117 inst = GENERIC[-1]118 d, n = int(inst["d"]), int(inst["n"])119 out = solve(inst, ["--alg", "kangaroo", "--lo", str(max(0, d - 10**5)), "--hi", str(min(n - 1, d + 10**5))])120 check_output(inst, out)121 assert out["iterations"] < 20_000 # ~2·sqrt(2e5) ≈ 900 expected steps, plus DP overshoot122 out = solve(inst, ["--alg", "bsgs", "--memory", "4096"])123 check_output(inst, out)124 assert out["table_entries"] <= 4096125126127def test_batch_mode(tmp_path):128 jobs = []129 for inst in GENERIC[:3] + J0[:1]:130 base = {k: inst[k] for k in ("p", "a", "b", "n", "gx", "gy", "qx", "qy")}131 base["expected_d"] = inst["d"]132 jobs.append(dict(base, alg="bsgs", seed=1, label=inst["label"] + "/bsgs"))133 jobs.append(dict(base, alg="rho", seed=2, neg=True, threads=2, label=inst["label"] + "/rho"))134 jobs.append(dict(base, alg="kangaroo", seed=3, label=inst["label"] + "/kangaroo"))135 if "beta" in inst:136 jobs.append(dict(base, alg="rho", seed=4, neg=True, aut3=True, beta=inst["beta"], lambda_=None,137 **{"lambda": inst["lambda"]}, label=inst["label"] + "/rho-neg-aut3"))138 for j in jobs:139 j.pop("lambda_", None)140 f = tmp_path / "jobs.json"141 f.write_text(json.dumps(jobs))142 outf = tmp_path / "results.json"143 proc = subprocess.run([str(BIN), "batch", "--file", str(f), "--out", str(outf)], capture_output=True, text=True, timeout=600)144 assert proc.returncode == 0, proc.stderr145 results = json.loads(outf.read_text())146 assert len(results) == len(jobs)147 for job, res in zip(jobs, results):148 assert res["label"] == job["label"]149 assert res["ok"] and res["match"] and res["d"] == job["expected_d"]150