//! rho_engine — native ECDLP experimentation engine for WP34-UQO (synthetic instances, p < 2^62). //! //! Library entry point: [`run`] takes a [`SolveSpec`] (the CLI flags or one element of a batch file) //! and returns the JSON result object documented in README.md. pub mod bsgs; pub mod curve; pub mod field; pub mod instance; pub mod kangaroo; pub mod rho; pub mod stats; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::time::Instant; /// One ECDLP job. Big integers are decimal strings so that the JSON stays exact. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SolveSpec { pub p: String, pub a: String, pub b: String, pub n: String, pub gx: String, pub gy: String, pub qx: String, pub qy: String, pub alg: String, #[serde(default = "default_seed")] pub seed: u64, #[serde(default)] pub label: Option, #[serde(default)] pub expected_d: Option, // rho #[serde(default = "default_r")] pub r: usize, #[serde(default)] pub cycle: Option, #[serde(default)] pub dp: Option, #[serde(default = "default_one")] pub threads: usize, #[serde(default)] pub walks: Option, #[serde(default)] pub mixed: usize, #[serde(default)] pub neg: bool, #[serde(default)] pub aut3: bool, #[serde(default)] pub beta: Option, #[serde(default)] pub lambda: Option, #[serde(default = "default_cycle_check")] pub cycle_check: u64, #[serde(default)] pub max_walk_len: Option, // kangaroo #[serde(default)] pub lo: Option, #[serde(default)] pub hi: Option, // bsgs #[serde(default)] pub memory: Option, #[serde(default = "default_chains")] pub chains: usize, } fn default_seed() -> u64 { 1 } fn default_r() -> usize { 20 } fn default_one() -> usize { 1 } fn default_cycle_check() -> u64 { 32 } fn default_chains() -> usize { 64 } fn parse_u64(name: &str, s: &str) -> Result { s.trim().parse::().map_err(|e| format!("{name}: cannot parse '{s}' as u64 ({e})")) } /// Run one job and return the result JSON object (never panics on a bad spec: returns {"error": …}). pub fn run(spec: &SolveSpec) -> Value { match run_inner(spec) { Ok(v) => v, Err(e) => json!({ "error": e, "ok": false, "label": spec.label, "params": serde_json::to_value(spec).unwrap_or(Value::Null) }), } } fn run_inner(spec: &SolveSpec) -> Result { let t0 = Instant::now(); let cpu0 = stats::process_cpu_s(); let p = parse_u64("p", &spec.p)?; let a = parse_u64("a", &spec.a)?; let b = parse_u64("b", &spec.b)?; let n = parse_u64("n", &spec.n)?; let gx = parse_u64("gx", &spec.gx)?; let gy = parse_u64("gy", &spec.gy)?; let qx = parse_u64("qx", &spec.qx)?; let qy = parse_u64("qy", &spec.qy)?; if p >= (1u64 << 62) { return Err("p must be < 2^62".into()); } if n >= (1u64 << 62) { return Err("n must be < 2^62".into()); } let mut inst = instance::Instance::new(p, a, b, n, gx, gy, qx, qy)?; if spec.aut3 { let beta = parse_u64("beta", spec.beta.as_deref().ok_or("--aut3 requires --beta")?)?; let lambda = parse_u64("lambda", spec.lambda.as_deref().ok_or("--aut3 requires --lambda")?)?; inst = inst.with_aut3(beta, lambda)?; } let bits = inst.bits(); let expected_d = spec.expected_d.as_deref().map(|s| parse_u64("expected_d", s)).transpose()?; let (d, ok, mut out) = match spec.alg.as_str() { "bsgs" => { let memory = spec.memory.as_deref().map(|s| parse_u64("memory", s)).transpose()?; let cfg = bsgs::BsgsConfig { memory, threads: spec.threads.max(1), chains: spec.chains.max(1) }; let res = bsgs::solve_bsgs(&inst, &cfg); let ok = res.d.map(|d| inst.verify(d, &mut curve::Counters::default())).unwrap_or(false); let out = json!({ "ops": res.ops, "iterations": res.iterations, "dp_count": 0, "dp_bytes": 0, "fruitless_cycles": 0, "threads": res.threads, "cpu_s": res.cpu_s, "table_entries": res.table_entries, "table_bytes": res.table_bytes, "baby_steps": res.baby_steps.to_string(), "giant_steps_planned": res.giant_steps_planned.to_string(), "build_s": res.build_s, "params": { "alg": "bsgs", "memory": res.baby_steps.to_string(), "threads": res.threads, "chains": cfg.chains, "seed": spec.seed }, }); (res.d, ok, out) } "rho" => { let mode = match spec.cycle.as_deref().unwrap_or("dp") { "floyd" => rho::RhoMode::Floyd, "brent" => rho::RhoMode::Brent, "dp" => rho::RhoMode::Dp, other => return Err(format!("unknown --cycle '{other}' (floyd|brent|dp)")), }; let dp_bits = spec.dp.unwrap_or_else(|| rho::RhoConfig::default_dp_bits(bits)); if spec.r < 2 { return Err("--r must be ≥ 2".into()); } if spec.mixed == 1 { return Err("--mixed k must be ≥ 2 (every k-th partition doubles)".into()); } let cfg = rho::RhoConfig { r: spec.r, mixed: spec.mixed, neg: spec.neg, aut3: spec.aut3, cycle_check: spec.cycle_check.max(2), dp_bits, walks: spec.walks.unwrap_or(128).max(1), threads: spec.threads.max(1), mode, seed: spec.seed, max_walk_len: spec.max_walk_len.unwrap_or((64u64 << dp_bits.min(40)).max(1 << 16)), }; let res = rho::solve_rho(&inst, &cfg); let ok = res.d.map(|d| inst.verify(d, &mut curve::Counters::default())).unwrap_or(false); let class = (if cfg.neg { 2 } else { 1 }) * (if cfg.aut3 { 3 } else { 1 }); let out = json!({ "ops": res.stats.ops, "iterations": res.stats.iterations, "dp_count": res.dp_count, "dp_bytes": res.dp_bytes, "fruitless_cycles": res.stats.fruitless_cycles, "fruitless_by_length": res.stats.fruitless_by_length.iter().enumerate().filter(|(_, c)| **c > 0).map(|(l, c)| json!({"length": l, "count": c})).collect::>(), "lookahead_rejections": res.stats.lookahead_rejections, "doubling_fallbacks": res.stats.doubling_fallbacks, "abandoned_walks": res.stats.abandoned_walks, "restarts": res.stats.restarts, "useless_collisions": res.stats.useless_collisions, "threads": res.threads, "cpu_s": res.stats.cpu_s, "table_entries": res.table_entries, "expected_iterations": rho::expected_iterations(n, class), "class_size": class, "params": { "alg": "rho", "mode": cfg.mode, "r": cfg.r, "mixed": cfg.mixed, "neg": cfg.neg, "aut3": cfg.aut3, "dp": cfg.dp_bits, "walks": cfg.walks, "threads": cfg.threads, "cycle_check": cfg.cycle_check, "max_walk_len": cfg.max_walk_len, "seed": cfg.seed, "beta": spec.beta, "lambda": spec.lambda }, }); (res.d, ok, out) } "kangaroo" => { let lo = spec.lo.as_deref().map(|s| parse_u64("lo", s)).transpose()?.unwrap_or(0); let hi = spec.hi.as_deref().map(|s| parse_u64("hi", s)).transpose()?.unwrap_or(n - 1); if hi >= n || lo > hi { return Err("need 0 ≤ lo ≤ hi < n".into()); } let cfg = kangaroo::KangarooConfig { lo, hi, threads: spec.threads.max(1), walks: spec.walks.unwrap_or(0), dp_bits: spec.dp.unwrap_or(0), seed: spec.seed }; let res = kangaroo::solve_kangaroo(&inst, &cfg); let ok = res.d.map(|d| inst.verify(d, &mut curve::Counters::default())).unwrap_or(false); let out = json!({ "ops": res.stats.ops, "iterations": res.stats.iterations, "dp_count": res.dp_count, "dp_bytes": res.dp_bytes, "fruitless_cycles": 0, "herd_collisions": res.stats.useless_collisions, "abandoned_walks": res.stats.abandoned_walks, "restarts": res.stats.restarts, "threads": res.threads, "cpu_s": res.stats.cpu_s, "table_entries": res.table_entries, "expected_iterations": 2.0 * (res.interval_width as f64).sqrt(), "params": { "alg": "kangaroo", "lo": lo.to_string(), "hi": hi.to_string(), "interval_width": res.interval_width.to_string(), "threads": res.threads, "kangaroos_per_thread": res.kangaroos_per_thread, "dp": res.dp_bits, "jump_count": res.jump_count, "mean_jump": res.mean_jump.to_string(), "seed": spec.seed }, }); (res.d, ok, out) } other => return Err(format!("unknown algorithm '{other}' (bsgs|rho|kangaroo)")), }; let obj = out.as_object_mut().unwrap(); let wall = t0.elapsed().as_secs_f64(); obj.insert("d".into(), d.map(|v| Value::String(v.to_string())).unwrap_or(Value::Null)); obj.insert("ok".into(), Value::Bool(ok)); obj.insert("wall_s".into(), json!(wall)); obj.insert("process_cpu_s".into(), json!(stats::process_cpu_s() - cpu0)); obj.insert("peak_rss_bytes".into(), json!(stats::peak_rss_bytes())); obj.insert("bits".into(), json!(bits)); obj.insert("seed".into(), json!(spec.seed)); obj.insert("label".into(), spec.label.clone().map(Value::String).unwrap_or(Value::Null)); obj.insert("engine".into(), json!(format!("rho_engine {}", env!("CARGO_PKG_VERSION")))); let ops = obj.get("ops").cloned().unwrap_or(Value::Null); let group_ops = ops.get("add").and_then(|v| v.as_u64()).unwrap_or(0) + ops.get("dbl").and_then(|v| v.as_u64()).unwrap_or(0); let cpu = obj.get("cpu_s").and_then(|v| v.as_f64()).unwrap_or(0.0); obj.insert("group_ops".into(), json!(group_ops)); obj.insert("group_ops_per_cpu_s".into(), json!(if cpu > 0.0 { group_ops as f64 / cpu } else { 0.0 })); obj.insert("group_ops_per_wall_s".into(), json!(if wall > 0.0 { group_ops as f64 / wall } else { 0.0 })); if let Some(e) = expected_d { obj.insert("expected_d".into(), Value::String(e.to_string())); obj.insert("match".into(), Value::Bool(d == Some(e % n))); } let instance = json!({ "p": spec.p, "a": spec.a, "b": spec.b, "n": spec.n, "gx": spec.gx, "gy": spec.gy, "qx": spec.qx, "qy": spec.qy }); obj.get_mut("params").and_then(|v| v.as_object_mut()).map(|m| m.insert("instance".into(), instance)); Ok(out) }