1"""ECDLP study generator/driver for the Rust engine (Axis A: H04, H08, H09, H10, H11).23Studies (each writes results/synthetic_ecdlp/<study>_<tag>.jobs.json; `--run` executes locally, otherwise the jobs are4shipped to a cluster node and executed with run_batch_remote.py):5 scaling_generic sizes 20..56, 20 random prime-order curves per size, configs bsgs / rho floyd / rho brent / rho dp /6 rho dp+neg (r=128) / kangaroo (full interval)7 scaling_j0 sizes 24..52 on j=0 curves (p ≡ 1 mod 3), configs rho dp (r=128) / +neg / +aut3 / +neg+aut38 sweep_r 36- and 40-bit generic curves × r ∈ {3,4,8,16,20,32,64,128} × mixed ∈ {0,2,4}, 40 instances9 parallel 48-bit generic curves, threads ∈ {1,2,4,8,16,28}, 6 instances10 tmto bsgs memory sweep (36 bits) and rho dp-bit sweep (40 bits), 20 instances11Instances are generated with PARI/GP point counting (laptop); jobs are flat objects in the engine's batch format.12"""13import argparse, json, math, random, sys, pathlib14ROOT = pathlib.Path(__file__).resolve().parents[2]15sys.path.insert(0, str(ROOT / "src"))16from wp34.ec import random_prime_order_curve, cube_roots_of_unity, endomorphism_lambda1718OUT = ROOT / "results/synthetic_ecdlp"192021def gen_instances(sizes, per_size, seed, j0):22 rng = random.Random(seed); inst = []23 for bits in sizes:24 for i in range(per_size):25 E = random_prime_order_curve(bits, rng, j0=j0)26 d = rng.randrange(1, E.n); Q = E.mul(d, E.G)27 rec = {"bits": bits, "idx": i, "p": str(E.p), "a": str(E.a), "b": str(E.b), "n": str(E.n),28 "gx": str(E.G[0]), "gy": str(E.G[1]), "qx": str(Q[0]), "qy": str(Q[1]), "expected_d": str(d)}29 if j0:30 for beta in cube_roots_of_unity(E.p):31 try:32 lam = endomorphism_lambda(E, beta); rec["beta"] = str(beta); rec["lambda"] = str(lam); break33 except ValueError:34 continue35 assert "lambda" in rec36 inst.append(rec)37 return inst383940def base(rec):41 return {k: rec[k] for k in ("p", "a", "b", "n", "gx", "gy", "qx", "qy", "expected_d")}424344def dp_default(bits):45 return max(1, bits // 4 - 2)464748def study_scaling_generic(seed):49 sizes = [20, 24, 28, 32, 36, 40, 44, 48, 52, 56]50 inst = gen_instances(sizes, 60, seed, False); jobs = []51 for rec in inst:52 b = base(rec); lab = f"g{rec['bits']}b-{rec['idx']}"; bits = rec["bits"]53 if bits <= 48:54 jobs.append(dict(b, alg="bsgs", seed=seed, threads=1, label=lab + "/bsgs"))55 if bits <= 48:56 jobs.append(dict(b, alg="rho", cycle="floyd", r=20, seed=seed, threads=1, label=lab + "/rho_floyd"))57 jobs.append(dict(b, alg="rho", cycle="brent", r=20, seed=seed, threads=1, label=lab + "/rho_brent"))58 jobs.append(dict(b, alg="rho", cycle="dp", r=20, dp=dp_default(bits), seed=seed, threads=1, label=lab + "/rho_dp"))59 jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp_default(bits), seed=seed, threads=1, label=lab + "/rho_dp_r128"))60 jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp_default(bits), neg=True, seed=seed, threads=1, label=lab + "/rho_dp_neg"))61 jobs.append(dict(b, alg="kangaroo", dp=dp_default(bits), seed=seed, threads=1, label=lab + "/kangaroo"))62 return jobs, {"sizes": sizes, "per_size": 60}636465def study_scaling_j0(seed):66 sizes = [24, 28, 32, 36, 40, 44, 48, 52]67 inst = gen_instances(sizes, 60, seed + 1, True); jobs = []68 for rec in inst:69 b = base(rec); lab = f"j{rec['bits']}b-{rec['idx']}"; bits = rec["bits"]; dp = dp_default(bits)70 jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, seed=seed, threads=1, label=lab + "/rho_dp"))71 jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, neg=True, seed=seed, threads=1, label=lab + "/rho_dp_neg"))72 jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, aut3=True, beta=rec["beta"], seed=seed, threads=1, label=lab + "/rho_dp_aut3", **{"lambda": rec["lambda"]}))73 jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, neg=True, aut3=True, beta=rec["beta"], seed=seed, threads=1, label=lab + "/rho_dp_neg_aut3", **{"lambda": rec["lambda"]}))74 jobs.append(dict(b, alg="rho", cycle="dp", r=20, dp=dp, neg=True, aut3=True, beta=rec["beta"], seed=seed, threads=1, label=lab + "/rho_dp_neg_aut3_r20", **{"lambda": rec["lambda"]}))75 return jobs, {"sizes": sizes, "per_size": 60}767778def study_sweep_r(seed):79 inst = gen_instances([36, 40], 40, seed + 2, False); jobs = []80 for rec in inst:81 b = base(rec); lab = f"g{rec['bits']}b-{rec['idx']}"; dp = dp_default(rec["bits"])82 for r in (3, 4, 8, 16, 20, 32, 64, 128):83 for mixed in (0, 2, 4):84 kw = {"mixed": mixed} if mixed else {}85 jobs.append(dict(b, alg="rho", cycle="dp", r=r, dp=dp, seed=seed, threads=1, label=f"{lab}/r{r}_m{mixed}", **kw))86 # negative control: a bad walk — r = 2 (far from random), and Pollard's original 3-partition analogue r=3 is above87 jobs.append(dict(b, alg="rho", cycle="dp", r=2, dp=dp, seed=seed, threads=1, label=f"{lab}/r2_m0"))88 return jobs, {"sizes": [36, 40], "per_size": 40, "r": [2, 3, 4, 8, 16, 20, 32, 64, 128], "mixed": [0, 2, 4]}899091def study_parallel(seed):92 inst = gen_instances([56], 12, seed + 3, False); jobs = []93 for rec in inst:94 b = base(rec); lab = f"g56b-{rec['idx']}"95 for t in (1, 2, 4, 8, 16, 28):96 jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=12, neg=True, seed=seed, threads=t, label=f"{lab}/t{t}"))97 return jobs, {"sizes": [56], "per_size": 12, "threads": [1, 2, 4, 8, 16, 28]}9899100def study_tmto(seed):101 jobs = []102 for rec in gen_instances([36], 20, seed + 4, False):103 b = base(rec); lab = f"g36b-{rec['idx']}"104 for mem in (2**10, 2**12, 2**14, 2**16, 2**18, 2**20):105 jobs.append(dict(b, alg="bsgs", memory=str(mem), seed=seed, threads=1, label=f"{lab}/bsgs_m{mem}"))106 for rec in gen_instances([40], 20, seed + 5, False):107 b = base(rec); lab = f"g40b-{rec['idx']}"108 for dp in (2, 4, 6, 8, 10, 12, 14):109 jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, seed=seed, threads=1, label=f"{lab}/rho_dp{dp}"))110 return jobs, {"bsgs_bits": 36, "rho_bits": 40, "per_size": 20}111112113STUDIES = {"scaling_generic": study_scaling_generic, "scaling_j0": study_scaling_j0, "sweep_r": study_sweep_r,114 "parallel": study_parallel, "tmto": study_tmto}115116117def main():118 ap = argparse.ArgumentParser(); ap.add_argument("study", choices=list(STUDIES) + ["all"]); ap.add_argument("--seed", type=int, default=34)119 a = ap.parse_args(); OUT.mkdir(parents=True, exist_ok=True)120 for name in (STUDIES if a.study == "all" else [a.study]):121 jobs, params = STUDIES[name](a.seed)122 (OUT / f"{name}.jobs.json").write_text(json.dumps(jobs))123 (OUT / f"{name}.params.json").write_text(json.dumps(dict(params, seed=a.seed, study=name)))124 print(f"{name}: {len(jobs)} jobs → {OUT / (name + '.jobs.json')}")125126127if __name__ == "__main__":128 main()129