1//! (b) Fixture instances generated by the Python reference (`scripts/gen_fixtures.py` → `src/wp34/ec.py`2//! + PARI/GP): 60 prime-order curves of 20–32 bits (10 of them j = 0 per size) with known d.3//! Every algorithm / option combination must recover d, and 1-thread runs must be deterministic.45use rho_engine::{run, SolveSpec};6use serde::Deserialize;7use std::path::PathBuf;89#[derive(Deserialize, Clone)]10struct Fx {11 label: String,12 j0: bool,13 p: String,14 a: String,15 b: String,16 n: String,17 gx: String,18 gy: String,19 qx: String,20 qy: String,21 d: String,22 #[serde(default)]23 beta: Option<String>,24 #[serde(default)]25 lambda: Option<String>,26}2728#[derive(Deserialize)]29struct Doc {30 instances: Vec<Fx>,31}3233fn fixture_path() -> PathBuf {34 let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));35 p.push("../../../tests/fixtures/ecdlp_instances.json");36 p37}3839fn load() -> Vec<Fx> {40 let text = std::fs::read_to_string(fixture_path()).expect("fixture tests/fixtures/ecdlp_instances.json (run scripts/gen_fixtures.py)");41 let doc: Doc = serde_json::from_str(&text).unwrap();42 assert!(doc.instances.len() >= 50);43 doc.instances44}4546fn spec(fx: &Fx, alg: &str, seed: u64) -> SolveSpec {47 SolveSpec {48 p: fx.p.clone(),49 a: fx.a.clone(),50 b: fx.b.clone(),51 n: fx.n.clone(),52 gx: fx.gx.clone(),53 gy: fx.gy.clone(),54 qx: fx.qx.clone(),55 qy: fx.qy.clone(),56 alg: alg.into(),57 seed,58 label: Some(fx.label.clone()),59 expected_d: Some(fx.d.clone()),60 r: 20,61 cycle: None,62 dp: None,63 threads: 1,64 walks: None,65 mixed: 0,66 neg: false,67 aut3: false,68 beta: fx.beta.clone(),69 lambda: fx.lambda.clone(),70 cycle_check: 32,71 max_walk_len: None,72 lo: None,73 hi: None,74 memory: None,75 chains: 64,76 }77}7879fn check(s: &SolveSpec, what: &str) -> serde_json::Value {80 let out = run(s);81 assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true), "{what}: {out}");82 assert_eq!(out.get("match").and_then(|v| v.as_bool()), Some(true), "{what}: {out}");83 assert_eq!(out["d"].as_str(), s.expected_d.as_deref(), "{what}");84 out85}8687#[test]88fn fixtures_every_algorithm_recovers_d() {89 let fxs = load();90 for fx in &fxs {91 let seed = 7;92 check(&spec(fx, "bsgs", seed), &format!("{} bsgs", fx.label));93 let mut s = spec(fx, "bsgs", seed);94 s.memory = Some("1000".into());95 s.threads = 2;96 check(&s, &format!("{} bsgs --memory 1000 --threads 2", fx.label));9798 let mut s = spec(fx, "rho", seed);99 s.cycle = Some("floyd".into());100 check(&s, &format!("{} rho floyd", fx.label));101 s.cycle = Some("brent".into());102 check(&s, &format!("{} rho brent", fx.label));103 s.cycle = Some("dp".into());104 check(&s, &format!("{} rho dp 1 thread", fx.label));105 s.threads = 4;106 check(&s, &format!("{} rho dp 4 threads", fx.label));107 let mut s = spec(fx, "rho", seed);108 s.neg = true;109 check(&s, &format!("{} rho neg", fx.label));110 s.cycle = Some("brent".into());111 check(&s, &format!("{} rho neg brent", fx.label));112 let mut s = spec(fx, "rho", seed);113 s.mixed = 4;114 check(&s, &format!("{} rho mixed 4", fx.label));115 if fx.j0 {116 let mut s = spec(fx, "rho", seed);117 s.aut3 = true;118 check(&s, &format!("{} rho aut3", fx.label));119 s.neg = true;120 check(&s, &format!("{} rho neg aut3", fx.label));121 s.cycle = Some("floyd".into());122 check(&s, &format!("{} rho neg aut3 floyd", fx.label));123 s.cycle = None;124 s.threads = 4;125 check(&s, &format!("{} rho neg aut3 4 threads", fx.label));126 }127 let s = spec(fx, "kangaroo", seed);128 check(&s, &format!("{} kangaroo [0, n)", fx.label));129 let mut s = spec(fx, "kangaroo", seed);130 s.threads = 4;131 check(&s, &format!("{} kangaroo 4 threads", fx.label));132 // tight interval around d133 let d: u64 = fx.d.parse().unwrap();134 let n: u64 = fx.n.parse().unwrap();135 let mut s = spec(fx, "kangaroo", seed);136 s.lo = Some(d.saturating_sub(50_000).to_string());137 s.hi = Some((d + 50_000).min(n - 1).to_string());138 check(&s, &format!("{} kangaroo tight", fx.label));139 }140}141142#[test]143fn one_thread_runs_are_deterministic() {144 let fxs = load();145 for fx in fxs.iter().filter(|f| f.label.starts_with("28b") || f.label.starts_with("32b-j0")).take(6) {146 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)] {147 let mut s = spec(fx, alg, 99);148 s.neg = neg;149 s.aut3 = aut3;150 s.cycle = cycle.map(String::from);151 let a = check(&s, "det a");152 let b = check(&s, "det b");153 for key in ["iterations", "ops", "dp_count", "fruitless_cycles", "restarts"] {154 assert_eq!(a.get(key), b.get(key), "{} {alg} neg={neg} aut3={aut3} key={key}", fx.label);155 }156 }157 }158}159160#[test]161fn wrong_beta_or_lambda_aborts() {162 let fxs = load();163 let fx = fxs.iter().find(|f| f.j0).unwrap();164 let mut s = spec(fx, "rho", 1);165 s.aut3 = true;166 let beta: u64 = fx.beta.as_ref().unwrap().parse().unwrap();167 s.beta = Some((beta + 1).to_string());168 let out = run(&s);169 assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(false));170 assert!(out.get("error").is_some());171 // right beta but the other cube root of unity as lambda (λ² instead of λ) must also abort172 let mut s = spec(fx, "rho", 1);173 s.aut3 = true;174 let n: u64 = fx.n.parse().unwrap();175 let lam: u128 = fx.lambda.as_ref().unwrap().parse().unwrap();176 s.lambda = Some(((lam * lam) % n as u128).to_string());177 let out = run(&s);178 assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(false), "{out}");179 assert!(out["error"].as_str().unwrap().contains("lambda*G"));180}181