//! (b) Fixture instances generated by the Python reference (`scripts/gen_fixtures.py` → `src/wp34/ec.py` //! + PARI/GP): 60 prime-order curves of 20–32 bits (10 of them j = 0 per size) with known d. //! Every algorithm / option combination must recover d, and 1-thread runs must be deterministic. use rho_engine::{run, SolveSpec}; use serde::Deserialize; use std::path::PathBuf; #[derive(Deserialize, Clone)] struct Fx { label: String, j0: bool, p: String, a: String, b: String, n: String, gx: String, gy: String, qx: String, qy: String, d: String, #[serde(default)] beta: Option, #[serde(default)] lambda: Option, } #[derive(Deserialize)] struct Doc { instances: Vec, } fn fixture_path() -> PathBuf { let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.push("../../../tests/fixtures/ecdlp_instances.json"); p } fn load() -> Vec { let text = std::fs::read_to_string(fixture_path()).expect("fixture tests/fixtures/ecdlp_instances.json (run scripts/gen_fixtures.py)"); let doc: Doc = serde_json::from_str(&text).unwrap(); assert!(doc.instances.len() >= 50); doc.instances } fn spec(fx: &Fx, alg: &str, seed: u64) -> SolveSpec { SolveSpec { p: fx.p.clone(), a: fx.a.clone(), b: fx.b.clone(), n: fx.n.clone(), gx: fx.gx.clone(), gy: fx.gy.clone(), qx: fx.qx.clone(), qy: fx.qy.clone(), alg: alg.into(), seed, label: Some(fx.label.clone()), expected_d: Some(fx.d.clone()), r: 20, cycle: None, dp: None, threads: 1, walks: None, mixed: 0, neg: false, aut3: false, beta: fx.beta.clone(), lambda: fx.lambda.clone(), cycle_check: 32, max_walk_len: None, lo: None, hi: None, memory: None, chains: 64, } } fn check(s: &SolveSpec, what: &str) -> serde_json::Value { let out = run(s); assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true), "{what}: {out}"); assert_eq!(out.get("match").and_then(|v| v.as_bool()), Some(true), "{what}: {out}"); assert_eq!(out["d"].as_str(), s.expected_d.as_deref(), "{what}"); out } #[test] fn fixtures_every_algorithm_recovers_d() { let fxs = load(); for fx in &fxs { let seed = 7; check(&spec(fx, "bsgs", seed), &format!("{} bsgs", fx.label)); let mut s = spec(fx, "bsgs", seed); s.memory = Some("1000".into()); s.threads = 2; check(&s, &format!("{} bsgs --memory 1000 --threads 2", fx.label)); let mut s = spec(fx, "rho", seed); s.cycle = Some("floyd".into()); check(&s, &format!("{} rho floyd", fx.label)); s.cycle = Some("brent".into()); check(&s, &format!("{} rho brent", fx.label)); s.cycle = Some("dp".into()); check(&s, &format!("{} rho dp 1 thread", fx.label)); s.threads = 4; check(&s, &format!("{} rho dp 4 threads", fx.label)); let mut s = spec(fx, "rho", seed); s.neg = true; check(&s, &format!("{} rho neg", fx.label)); s.cycle = Some("brent".into()); check(&s, &format!("{} rho neg brent", fx.label)); let mut s = spec(fx, "rho", seed); s.mixed = 4; check(&s, &format!("{} rho mixed 4", fx.label)); if fx.j0 { let mut s = spec(fx, "rho", seed); s.aut3 = true; check(&s, &format!("{} rho aut3", fx.label)); s.neg = true; check(&s, &format!("{} rho neg aut3", fx.label)); s.cycle = Some("floyd".into()); check(&s, &format!("{} rho neg aut3 floyd", fx.label)); s.cycle = None; s.threads = 4; check(&s, &format!("{} rho neg aut3 4 threads", fx.label)); } let s = spec(fx, "kangaroo", seed); check(&s, &format!("{} kangaroo [0, n)", fx.label)); let mut s = spec(fx, "kangaroo", seed); s.threads = 4; check(&s, &format!("{} kangaroo 4 threads", fx.label)); // tight interval around d let d: u64 = fx.d.parse().unwrap(); let n: u64 = fx.n.parse().unwrap(); let mut s = spec(fx, "kangaroo", seed); s.lo = Some(d.saturating_sub(50_000).to_string()); s.hi = Some((d + 50_000).min(n - 1).to_string()); check(&s, &format!("{} kangaroo tight", fx.label)); } } #[test] fn one_thread_runs_are_deterministic() { let fxs = load(); for fx in fxs.iter().filter(|f| f.label.starts_with("28b") || f.label.starts_with("32b-j0")).take(6) { for (alg, neg, aut3, cycle) in [("rho", false, false, None), ("rho", true, false, None), ("rho", true, fx.j0, None), ("rho", false, false, Some("brent")), ("kangaroo", false, false, None)] { let mut s = spec(fx, alg, 99); s.neg = neg; s.aut3 = aut3; s.cycle = cycle.map(String::from); let a = check(&s, "det a"); let b = check(&s, "det b"); for key in ["iterations", "ops", "dp_count", "fruitless_cycles", "restarts"] { assert_eq!(a.get(key), b.get(key), "{} {alg} neg={neg} aut3={aut3} key={key}", fx.label); } } } } #[test] fn wrong_beta_or_lambda_aborts() { let fxs = load(); let fx = fxs.iter().find(|f| f.j0).unwrap(); let mut s = spec(fx, "rho", 1); s.aut3 = true; let beta: u64 = fx.beta.as_ref().unwrap().parse().unwrap(); s.beta = Some((beta + 1).to_string()); let out = run(&s); assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(false)); assert!(out.get("error").is_some()); // right beta but the other cube root of unity as lambda (λ² instead of λ) must also abort let mut s = spec(fx, "rho", 1); s.aut3 = true; let n: u64 = fx.n.parse().unwrap(); let lam: u128 = fx.lambda.as_ref().unwrap().parse().unwrap(); s.lambda = Some(((lam * lam) % n as u128).to_string()); let out = run(&s); assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(false), "{out}"); assert!(out["error"].as_str().unwrap().contains("lambda*G")); }