1//! CLI: `rho_engine solve …` (one JSON object on stdout) and `rho_engine batch …` (JSON array).23use clap::{Args, Parser, Subcommand};4use rho_engine::SolveSpec;5use std::io::Write;67#[derive(Parser)]8#[command(name = "rho_engine", version, about = "Native ECDLP engine (BSGS / Pollard rho / kangaroo) for synthetic prime-order curves, p < 2^62")]9struct Cli {10 #[command(subcommand)]11 cmd: Cmd,12}1314#[derive(Subcommand)]15enum Cmd {16 /// Solve one instance Q = d·G and print a JSON result object17 Solve(SolveArgs),18 /// Run every job of a JSON array file sequentially and write a JSON array of results19 Batch {20 /// input file: JSON array of job objects (same keys as the `solve` flags, decimal strings)21 #[arg(long)]22 file: String,23 /// output file (JSON array); "-" for stdout24 #[arg(long, default_value = "-")]25 out: String,26 /// print progress lines on stderr27 #[arg(long)]28 verbose: bool,29 },30}3132#[derive(Args)]33struct SolveArgs {34 #[arg(long)]35 p: String,36 #[arg(long)]37 a: String,38 #[arg(long)]39 b: String,40 /// prime order of G41 #[arg(long)]42 n: String,43 #[arg(long)]44 gx: String,45 #[arg(long)]46 gy: String,47 #[arg(long)]48 qx: String,49 #[arg(long)]50 qy: String,51 /// bsgs | rho | kangaroo52 #[arg(long)]53 alg: String,54 #[arg(long, default_value_t = 1)]55 seed: u64,56 /// print JSON (always on; kept for compatibility)57 #[arg(long)]58 json: bool,59 /// optional label copied to the output60 #[arg(long)]61 label: Option<String>,62 /// optional known answer (adds "match")63 #[arg(long)]64 expected_d: Option<String>,65 /// rho: number of partitions of the r-adding walk66 #[arg(long, default_value_t = 20)]67 r: usize,68 /// rho: floyd | brent (single walk) | dp (van Oorschot–Wiener, default)69 #[arg(long)]70 cycle: Option<String>,71 /// rho/kangaroo: distinguished-point bits (default ⌊bits/4⌋ − 2, ≥ 1)72 #[arg(long)]73 dp: Option<u32>,74 #[arg(long, default_value_t = 1)]75 threads: usize,76 /// rho: walks per thread (default 128); kangaroo: kangaroos per thread (default automatic)77 #[arg(long)]78 walks: Option<usize>,79 /// rho: Teske mixed walk — every k-th partition is a doubling partition (0 = pure adding walk)80 #[arg(long, default_value_t = 0)]81 mixed: usize,82 /// rho: negation map (classes {P, −P})83 #[arg(long)]84 neg: bool,85 /// rho: order-3 automorphism classes (a = 0 curves; requires --beta and --lambda)86 #[arg(long)]87 aut3: bool,88 #[arg(long)]89 beta: Option<String>,90 #[arg(long)]91 lambda: Option<String>,92 /// rho: remember one point every K steps for fruitless-cycle detection93 #[arg(long, default_value_t = 32)]94 cycle_check: u64,95 /// rho (dp): abandon a walk after this many steps without a distinguished point96 #[arg(long)]97 max_walk_len: Option<u64>,98 /// kangaroo: interval lower bound (default 0)99 #[arg(long)]100 lo: Option<String>,101 /// kangaroo: interval upper bound (default n − 1)102 #[arg(long)]103 hi: Option<String>,104 /// bsgs: number of baby steps m (default ⌈√n⌉)105 #[arg(long)]106 memory: Option<String>,107 /// bsgs: chains per thread for batch inversion108 #[arg(long, default_value_t = 64)]109 chains: usize,110}111112impl From<SolveArgs> for SolveSpec {113 fn from(a: SolveArgs) -> SolveSpec {114 SolveSpec {115 p: a.p,116 a: a.a,117 b: a.b,118 n: a.n,119 gx: a.gx,120 gy: a.gy,121 qx: a.qx,122 qy: a.qy,123 alg: a.alg,124 seed: a.seed,125 label: a.label,126 expected_d: a.expected_d,127 r: a.r,128 cycle: a.cycle,129 dp: a.dp,130 threads: a.threads,131 walks: a.walks,132 mixed: a.mixed,133 neg: a.neg,134 aut3: a.aut3,135 beta: a.beta,136 lambda: a.lambda,137 cycle_check: a.cycle_check,138 max_walk_len: a.max_walk_len,139 lo: a.lo,140 hi: a.hi,141 memory: a.memory,142 chains: a.chains,143 }144 }145}146147fn main() {148 let cli = Cli::parse();149 match cli.cmd {150 Cmd::Solve(args) => {151 let spec: SolveSpec = args.into();152 let out = rho_engine::run(&spec);153 println!("{}", serde_json::to_string(&out).unwrap());154 if out.get("ok").and_then(|v| v.as_bool()) != Some(true) {155 std::process::exit(2);156 }157 }158 Cmd::Batch { file, out, verbose } => {159 let text = std::fs::read_to_string(&file).unwrap_or_else(|e| {160 eprintln!("cannot read {file}: {e}");161 std::process::exit(1)162 });163 let specs: Vec<SolveSpec> = serde_json::from_str(&text).unwrap_or_else(|e| {164 eprintln!("cannot parse {file}: {e}");165 std::process::exit(1)166 });167 let mut results = Vec::with_capacity(specs.len());168 let mut failures = 0usize;169 for (i, spec) in specs.iter().enumerate() {170 let res = rho_engine::run(spec);171 let ok = res.get("ok").and_then(|v| v.as_bool()) == Some(true);172 if !ok {173 failures += 1;174 }175 if verbose {176 eprintln!(177 "[{}/{}] {} alg={} ok={} wall={:.3}s group_ops={}",178 i + 1,179 specs.len(),180 spec.label.as_deref().unwrap_or("-"),181 spec.alg,182 ok,183 res.get("wall_s").and_then(|v| v.as_f64()).unwrap_or(0.0),184 res.get("group_ops").and_then(|v| v.as_u64()).unwrap_or(0)185 );186 }187 results.push(res);188 }189 let text = serde_json::to_string_pretty(&results).unwrap();190 if out == "-" {191 println!("{text}");192 } else {193 let mut f = std::fs::File::create(&out).unwrap_or_else(|e| {194 eprintln!("cannot write {out}: {e}");195 std::process::exit(1)196 });197 f.write_all(text.as_bytes()).unwrap();198 f.write_all(b"\n").unwrap();199 }200 if failures > 0 {201 eprintln!("{failures} job(s) failed");202 std::process::exit(2);203 }204 }205 }206}207