"""End-to-end tests of the native ECDLP engine (experiments/synthetic_ecdlp/rho_engine). Runs the release binary on every instance of tests/fixtures/ecdlp_instances.json (generated by experiments/synthetic_ecdlp/rho_engine/scripts/gen_fixtures.py from the pure-Python reference src/wp34/ec.py) with every algorithm / option combination and checks d against the fixture, plus d·G == Q recomputed in Python. Build first: cargo build --release --manifest-path experiments/synthetic_ecdlp/rho_engine/Cargo.toml """ import json, pathlib, subprocess, sys import pytest ROOT = pathlib.Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) from wp34.ec import Curve # noqa: E402 CRATE = ROOT / "experiments" / "synthetic_ecdlp" / "rho_engine" BIN = CRATE / "target" / "release" / "rho_engine" FIXTURE = ROOT / "tests" / "fixtures" / "ecdlp_instances.json" pytestmark = pytest.mark.skipif(not BIN.exists(), reason="rho_engine release binary not built") def _instances(): if not FIXTURE.exists(): return [] return json.loads(FIXTURE.read_text())["instances"] INSTANCES = _instances() GENERIC = [i for i in INSTANCES if not i["j0"]] J0 = [i for i in INSTANCES if i["j0"]] COMBOS = { "bsgs": ["--alg", "bsgs"], "rho-floyd": ["--alg", "rho", "--cycle", "floyd"], "rho-brent": ["--alg", "rho", "--cycle", "brent"], "rho-dp-1t": ["--alg", "rho", "--threads", "1"], "rho-dp-4t": ["--alg", "rho", "--threads", "4"], "rho-neg": ["--alg", "rho", "--neg"], "rho-mixed": ["--alg", "rho", "--mixed", "4"], "kangaroo": ["--alg", "kangaroo", "--lo", "0"], # --hi defaults to n-1 } J0_COMBOS = { "rho-aut3": ["--alg", "rho", "--aut3"], "rho-neg-aut3": ["--alg", "rho", "--neg", "--aut3"], "rho-neg-aut3-brent": ["--alg", "rho", "--neg", "--aut3", "--cycle", "brent"], } def solve(inst, extra, seed=1, expect_ok=True): args = [str(BIN), "solve"] for k in ("p", "a", "b", "n", "gx", "gy", "qx", "qy"): args += [f"--{k}", inst[k]] if "beta" in inst: args += ["--beta", inst["beta"], "--lambda", inst["lambda"]] args += ["--seed", str(seed), "--json", "--expected-d", inst["d"]] + extra proc = subprocess.run(args, capture_output=True, text=True, timeout=600) out = json.loads(proc.stdout) if expect_ok: assert proc.returncode == 0, (proc.stdout, proc.stderr) return out def check_output(inst, out): assert out["ok"] is True assert out["match"] is True assert out["d"] == inst["d"] # independent verification in pure Python E = Curve(int(inst["p"]), int(inst["a"]), int(inst["b"]), n=int(inst["n"])) G = (int(inst["gx"]), int(inst["gy"])) assert E.mul(int(out["d"]), G) == (int(inst["qx"]), int(inst["qy"])) for key in ("ops", "iterations", "dp_count", "dp_bytes", "fruitless_cycles", "threads", "wall_s", "cpu_s", "table_entries", "peak_rss_bytes", "params"): assert key in out, key assert set(out["ops"]) == {"add", "dbl", "inv", "mul"} assert out["peak_rss_bytes"] > 0 @pytest.mark.parametrize("combo", sorted(COMBOS)) @pytest.mark.parametrize("inst", INSTANCES, ids=[i["label"] for i in INSTANCES]) def test_all_instances_all_algorithms(inst, combo): out = solve(inst, COMBOS[combo]) check_output(inst, out) if combo == "bsgs": assert out["table_entries"] > 0 if combo == "rho-dp-4t": assert out["threads"] == 4 if combo == "rho-neg": assert out["params"]["neg"] is True @pytest.mark.parametrize("combo", sorted(J0_COMBOS)) @pytest.mark.parametrize("inst", J0, ids=[i["label"] for i in J0]) def test_j0_instances_automorphism_classes(inst, combo): out = solve(inst, J0_COMBOS[combo]) check_output(inst, out) assert out["params"]["aut3"] is True @pytest.mark.parametrize("inst", GENERIC[::7] + J0[::5], ids=lambda i: i["label"]) def test_single_thread_runs_are_deterministic(inst): for extra in (["--alg", "rho"], ["--alg", "rho", "--neg"], ["--alg", "kangaroo"], ["--alg", "rho", "--cycle", "brent"]): a = solve(inst, extra, seed=42) b = solve(inst, extra, seed=42) for key in ("d", "iterations", "ops", "dp_count", "fruitless_cycles"): assert a[key] == b[key], key def test_wrong_beta_aborts(): inst = J0[0] bad = dict(inst, beta=str(int(inst["beta"]) + 1)) out = solve(bad, ["--alg", "rho", "--aut3"], expect_ok=False) assert out["ok"] is False and "error" in out def test_kangaroo_interval_and_memory_override(): inst = GENERIC[-1] d, n = int(inst["d"]), int(inst["n"]) out = solve(inst, ["--alg", "kangaroo", "--lo", str(max(0, d - 10**5)), "--hi", str(min(n - 1, d + 10**5))]) check_output(inst, out) assert out["iterations"] < 20_000 # ~2·sqrt(2e5) ≈ 900 expected steps, plus DP overshoot out = solve(inst, ["--alg", "bsgs", "--memory", "4096"]) check_output(inst, out) assert out["table_entries"] <= 4096 def test_batch_mode(tmp_path): jobs = [] for inst in GENERIC[:3] + J0[:1]: base = {k: inst[k] for k in ("p", "a", "b", "n", "gx", "gy", "qx", "qy")} base["expected_d"] = inst["d"] jobs.append(dict(base, alg="bsgs", seed=1, label=inst["label"] + "/bsgs")) jobs.append(dict(base, alg="rho", seed=2, neg=True, threads=2, label=inst["label"] + "/rho")) jobs.append(dict(base, alg="kangaroo", seed=3, label=inst["label"] + "/kangaroo")) if "beta" in inst: jobs.append(dict(base, alg="rho", seed=4, neg=True, aut3=True, beta=inst["beta"], lambda_=None, **{"lambda": inst["lambda"]}, label=inst["label"] + "/rho-neg-aut3")) for j in jobs: j.pop("lambda_", None) f = tmp_path / "jobs.json" f.write_text(json.dumps(jobs)) outf = tmp_path / "results.json" proc = subprocess.run([str(BIN), "batch", "--file", str(f), "--out", str(outf)], capture_output=True, text=True, timeout=600) assert proc.returncode == 0, proc.stderr results = json.loads(outf.read_text()) assert len(results) == len(jobs) for job, res in zip(jobs, results): assert res["label"] == job["label"] assert res["ok"] and res["match"] and res["d"] == job["expected_d"]