//! CLI: `rho_engine solve …` (one JSON object on stdout) and `rho_engine batch …` (JSON array). use clap::{Args, Parser, Subcommand}; use rho_engine::SolveSpec; use std::io::Write; #[derive(Parser)] #[command(name = "rho_engine", version, about = "Native ECDLP engine (BSGS / Pollard rho / kangaroo) for synthetic prime-order curves, p < 2^62")] struct Cli { #[command(subcommand)] cmd: Cmd, } #[derive(Subcommand)] enum Cmd { /// Solve one instance Q = d·G and print a JSON result object Solve(SolveArgs), /// Run every job of a JSON array file sequentially and write a JSON array of results Batch { /// input file: JSON array of job objects (same keys as the `solve` flags, decimal strings) #[arg(long)] file: String, /// output file (JSON array); "-" for stdout #[arg(long, default_value = "-")] out: String, /// print progress lines on stderr #[arg(long)] verbose: bool, }, } #[derive(Args)] struct SolveArgs { #[arg(long)] p: String, #[arg(long)] a: String, #[arg(long)] b: String, /// prime order of G #[arg(long)] n: String, #[arg(long)] gx: String, #[arg(long)] gy: String, #[arg(long)] qx: String, #[arg(long)] qy: String, /// bsgs | rho | kangaroo #[arg(long)] alg: String, #[arg(long, default_value_t = 1)] seed: u64, /// print JSON (always on; kept for compatibility) #[arg(long)] json: bool, /// optional label copied to the output #[arg(long)] label: Option, /// optional known answer (adds "match") #[arg(long)] expected_d: Option, /// rho: number of partitions of the r-adding walk #[arg(long, default_value_t = 20)] r: usize, /// rho: floyd | brent (single walk) | dp (van Oorschot–Wiener, default) #[arg(long)] cycle: Option, /// rho/kangaroo: distinguished-point bits (default ⌊bits/4⌋ − 2, ≥ 1) #[arg(long)] dp: Option, #[arg(long, default_value_t = 1)] threads: usize, /// rho: walks per thread (default 128); kangaroo: kangaroos per thread (default automatic) #[arg(long)] walks: Option, /// rho: Teske mixed walk — every k-th partition is a doubling partition (0 = pure adding walk) #[arg(long, default_value_t = 0)] mixed: usize, /// rho: negation map (classes {P, −P}) #[arg(long)] neg: bool, /// rho: order-3 automorphism classes (a = 0 curves; requires --beta and --lambda) #[arg(long)] aut3: bool, #[arg(long)] beta: Option, #[arg(long)] lambda: Option, /// rho: remember one point every K steps for fruitless-cycle detection #[arg(long, default_value_t = 32)] cycle_check: u64, /// rho (dp): abandon a walk after this many steps without a distinguished point #[arg(long)] max_walk_len: Option, /// kangaroo: interval lower bound (default 0) #[arg(long)] lo: Option, /// kangaroo: interval upper bound (default n − 1) #[arg(long)] hi: Option, /// bsgs: number of baby steps m (default ⌈√n⌉) #[arg(long)] memory: Option, /// bsgs: chains per thread for batch inversion #[arg(long, default_value_t = 64)] chains: usize, } impl From for SolveSpec { fn from(a: SolveArgs) -> SolveSpec { SolveSpec { p: a.p, a: a.a, b: a.b, n: a.n, gx: a.gx, gy: a.gy, qx: a.qx, qy: a.qy, alg: a.alg, seed: a.seed, label: a.label, expected_d: a.expected_d, r: a.r, cycle: a.cycle, dp: a.dp, threads: a.threads, walks: a.walks, mixed: a.mixed, neg: a.neg, aut3: a.aut3, beta: a.beta, lambda: a.lambda, cycle_check: a.cycle_check, max_walk_len: a.max_walk_len, lo: a.lo, hi: a.hi, memory: a.memory, chains: a.chains, } } } fn main() { let cli = Cli::parse(); match cli.cmd { Cmd::Solve(args) => { let spec: SolveSpec = args.into(); let out = rho_engine::run(&spec); println!("{}", serde_json::to_string(&out).unwrap()); if out.get("ok").and_then(|v| v.as_bool()) != Some(true) { std::process::exit(2); } } Cmd::Batch { file, out, verbose } => { let text = std::fs::read_to_string(&file).unwrap_or_else(|e| { eprintln!("cannot read {file}: {e}"); std::process::exit(1) }); let specs: Vec = serde_json::from_str(&text).unwrap_or_else(|e| { eprintln!("cannot parse {file}: {e}"); std::process::exit(1) }); let mut results = Vec::with_capacity(specs.len()); let mut failures = 0usize; for (i, spec) in specs.iter().enumerate() { let res = rho_engine::run(spec); let ok = res.get("ok").and_then(|v| v.as_bool()) == Some(true); if !ok { failures += 1; } if verbose { eprintln!( "[{}/{}] {} alg={} ok={} wall={:.3}s group_ops={}", i + 1, specs.len(), spec.label.as_deref().unwrap_or("-"), spec.alg, ok, res.get("wall_s").and_then(|v| v.as_f64()).unwrap_or(0.0), res.get("group_ops").and_then(|v| v.as_u64()).unwrap_or(0) ); } results.push(res); } let text = serde_json::to_string_pretty(&results).unwrap(); if out == "-" { println!("{text}"); } else { let mut f = std::fs::File::create(&out).unwrap_or_else(|e| { eprintln!("cannot write {out}: {e}"); std::process::exit(1) }); f.write_all(text.as_bytes()).unwrap(); f.write_all(b"\n").unwrap(); } if failures > 0 { eprintln!("{failures} job(s) failed"); std::process::exit(2); } } } }