#![allow(clippy::all, unused_imports)]
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use ligerito::transcript::{FiatShamir, MerlinTranscript};
use ligerito::{
config_info_for_log_size, prove_with_transcript, prover_config_for_log_size,
verifier_config_for_log_size, verify_with_transcript, FinalizedLigeritoProof, VerifierConfig,
MAX_LOG_SIZE, MIN_LOG_SIZE,
};
use ligerito_binary_fields::{BinaryElem128, BinaryElem32};
use std::fs::File;
use std::io::{self, Read, Write};
use std::time::Instant;
#[derive(Parser)]
#[command(name = "ligerito")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "Ligerito polynomial commitment scheme CLI", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Prove {
#[arg(short, long, conflicts_with = "config")]
size: Option<u32>,
#[arg(short, long, conflicts_with = "size")]
config: Option<String>,
#[arg(short, long, default_value = "bincode")]
format: String,
#[arg(short, long, default_value = "sha256")]
transcript: String,
#[arg(value_name = "FILE")]
input: Option<String>,
},
Verify {
#[arg(short, long, conflicts_with = "config")]
size: Option<u32>,
#[arg(short, long, conflicts_with = "size")]
config: Option<String>,
#[arg(short, long, default_value = "bincode")]
format: String,
#[arg(short, long, default_value = "sha256")]
transcript: String,
#[arg(short, long)]
verbose: bool,
#[arg(value_name = "FILE")]
input: Option<String>,
},
Config {
#[arg(short, long)]
size: Option<u32>,
#[arg(long)]
generate: bool,
#[arg(long, default_value = "json")]
output_format: String,
},
Generate {
#[arg(short, long, default_value = "20")]
size: u32,
#[arg(short, long, default_value = "random")]
pattern: String,
#[arg(short, long)]
output: Option<String>,
},
Bench {
#[arg(short, long, default_value = "20")]
size: u32,
#[arg(short, long, default_value = "3")]
iterations: usize,
#[arg(short, long)]
verify: bool,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Prove {
size,
config,
format,
transcript,
input,
} => {
prove_command(size, config, &format, &transcript, input)?;
}
Commands::Verify {
size,
config,
format,
transcript,
verbose,
input,
} => {
verify_command(size, config, &format, &transcript, verbose, input)?;
}
Commands::Config {
size,
generate,
output_format,
} => {
config_command(size, generate, &output_format)?;
}
Commands::Generate {
size,
pattern,
output,
} => {
generate_command(size, &pattern, output)?;
}
Commands::Bench {
size,
iterations,
verify,
} => {
bench_command(size, iterations, verify)?;
}
}
Ok(())
}
fn prove_command(
size: Option<u32>,
config_path: Option<String>,
format: &str,
transcript_type: &str,
input: Option<String>,
) -> Result<()> {
if config_path.is_some() {
anyhow::bail!("Custom config loading not yet implemented. Use --size for now.");
}
let mut buffer = Vec::new();
match input {
Some(path) => {
let mut file =
File::open(&path).context(format!("Failed to open input file: {}", path))?;
file.read_to_end(&mut buffer)
.context("Failed to read polynomial from file")?;
}
None => {
io::stdin()
.read_to_end(&mut buffer)
.context("Failed to read polynomial from stdin")?;
}
}
let elem_size = std::mem::size_of::<BinaryElem32>();
let num_elements = buffer.len() / elem_size;
let size = match size {
Some(s) => {
if !(MIN_LOG_SIZE..=MAX_LOG_SIZE).contains(&s) {
anyhow::bail!(
"Size must be between {} and {}, got {}",
MIN_LOG_SIZE,
MAX_LOG_SIZE,
s
);
}
let expected_len = 1usize << s;
if num_elements != expected_len {
anyhow::bail!(
"Expected {} bytes ({} elements), got {} bytes ({} elements)",
expected_len * elem_size,
expected_len,
buffer.len(),
num_elements
);
}
s
}
None => {
if num_elements == 0 || !num_elements.is_power_of_two() {
anyhow::bail!(
"Input must be a power-of-2 number of elements. Got {} bytes ({} elements)",
buffer.len(),
num_elements
);
}
let detected_size = num_elements.trailing_zeros();
if !(MIN_LOG_SIZE..=MAX_LOG_SIZE).contains(&detected_size) {
anyhow::bail!(
"Auto-detected size 2^{} is out of range ({}-{})",
detected_size,
MIN_LOG_SIZE,
MAX_LOG_SIZE
);
}
eprintln!("auto-detected size: 2^{}", detected_size);
detected_size
}
};
let poly: Vec<BinaryElem32> = buffer
.chunks_exact(4)
.map(|chunk| {
let val = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
BinaryElem32::from(val)
})
.collect();
let mut build_info = vec![];
#[cfg(debug_assertions)]
build_info.push("DEBUG");
#[cfg(not(debug_assertions))]
build_info.push("release");
#[cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq"))]
build_info.push("SIMD");
#[cfg(not(all(target_arch = "x86_64", target_feature = "pclmulqdq")))]
build_info.push("no-SIMD");
eprintln!(
"prove: 2^{} GF(2^32), {} [{}]",
size,
transcript_type,
build_info.join(" ")
);
let config = prover_config_for_log_size::<BinaryElem32, BinaryElem128>(size);
let start = Instant::now();
let proof = match transcript_type {
"sha256" => {
let transcript = FiatShamir::new_sha256(0);
prove_with_transcript(&config, &poly, transcript)
}
"merlin" => {
#[cfg(feature = "transcript-merlin")]
{
let transcript = MerlinTranscript::new(b"ligerito-v1");
prove_with_transcript(&config, &poly, transcript)
}
#[cfg(not(feature = "transcript-merlin"))]
{
anyhow::bail!(
"Merlin transcript not available. Rebuild with --features transcript-merlin"
)
}
}
_ => anyhow::bail!(
"Unknown transcript backend: {}. Use sha256 or merlin",
transcript_type
),
}
.context("Proving failed")?;
let elapsed = start.elapsed();
let encoded = bincode::serialize(&proof).context("Failed to serialize proof")?;
let prove_ms = elapsed.as_secs_f64() * 1000.0;
let throughput = (poly.len() as f64) / elapsed.as_secs_f64();
eprintln!(
"result: {:.2}ms, {:.2e} elem/s, {} bytes",
prove_ms,
throughput,
encoded.len()
);
match format {
"bincode" => {
io::stdout()
.write_all(&encoded)
.context("Failed to write proof to stdout")?;
}
"hex" => {
let hex_str = hex::encode(&encoded);
println!("{}", hex_str);
}
_ => anyhow::bail!("Unknown format: {}. Use 'bincode' or 'hex'", format),
}
Ok(())
}
fn verify_command(
size: Option<u32>,
config_path: Option<String>,
format: &str,
transcript_type: &str,
verbose: bool,
input: Option<String>,
) -> Result<()> {
if config_path.is_some() {
anyhow::bail!("Custom config loading not yet implemented. Use --size for now.");
}
let mut buffer = Vec::new();
match input {
Some(path) => {
let mut file =
File::open(&path).context(format!("Failed to open input file: {}", path))?;
file.read_to_end(&mut buffer)
.context("Failed to read proof from file")?;
}
None => {
io::stdin()
.read_to_end(&mut buffer)
.context("Failed to read proof from stdin")?;
}
}
let proof_bytes = match format {
"bincode" => buffer,
"hex" => {
let hex_str = String::from_utf8(buffer).context("Invalid UTF-8 in hex input")?;
hex::decode(hex_str.trim()).context("Failed to decode hex")?
}
_ => anyhow::bail!("Unknown format: {}. Use 'bincode' or 'hex'", format),
};
let proof: FinalizedLigeritoProof<BinaryElem32, BinaryElem128> =
bincode::deserialize(&proof_bytes).context("Failed to deserialize proof")?;
let size = match size {
Some(s) => {
if !(MIN_LOG_SIZE..=MAX_LOG_SIZE).contains(&s) {
anyhow::bail!(
"Size must be between {} and {}, got {}",
MIN_LOG_SIZE,
MAX_LOG_SIZE,
s
);
}
s
}
None => {
let yr_len = proof.final_ligero_proof.yr.len();
if yr_len == 0 || !yr_len.is_power_of_two() {
anyhow::bail!("Cannot auto-detect size from proof. Please specify --size");
}
anyhow::bail!("Auto-detect from proof not yet implemented. Please specify --size");
}
};
eprintln!(
"verify: 2^{} GF(2^32), {} bytes, {} transcript",
size,
proof_bytes.len(),
transcript_type
);
if verbose {
eprintln!(" proof structure: {} bytes", proof.size_of());
}
let config = verifier_config_for_log_size(size);
let start = Instant::now();
let valid = match transcript_type {
"sha256" => {
let transcript = FiatShamir::new_sha256(0);
verify_with_transcript(&config, &proof, transcript)
}
"merlin" => {
#[cfg(feature = "transcript-merlin")]
{
let transcript = MerlinTranscript::new(b"ligerito-v1");
verify_with_transcript(&config, &proof, transcript)
}
#[cfg(not(feature = "transcript-merlin"))]
{
anyhow::bail!(
"Merlin transcript not available. Rebuild with --features transcript-merlin"
)
}
}
_ => anyhow::bail!(
"Unknown transcript backend: {}. Use sha256 or merlin",
transcript_type
),
}
.context("Verification failed")?;
let elapsed = start.elapsed();
let verify_ms = elapsed.as_secs_f64() * 1000.0;
if valid {
eprintln!("result: VALID {:.2}ms", verify_ms);
println!("VALID");
Ok(())
} else {
eprintln!("result: INVALID {:.2}ms", verify_ms);
println!("INVALID");
std::process::exit(1);
}
}
fn config_command(size: Option<u32>, generate: bool, _output_format: &str) -> Result<()> {
if generate {
anyhow::bail!(
"Config generation not yet implemented. This will allow creating custom config files."
);
}
match size {
Some(s) => {
if !(MIN_LOG_SIZE..=MAX_LOG_SIZE).contains(&s) {
anyhow::bail!("Size must be between {} and {}", MIN_LOG_SIZE, MAX_LOG_SIZE);
}
println!("Ligerito Configuration for 2^{}", s);
println!("====================================");
let config = verifier_config_for_log_size(s);
print_config_info(&config, s);
}
None => {
ligerito::autosizer::print_config_summary();
}
}
Ok(())
}
fn print_config_info(config: &VerifierConfig, log_size: u32) {
let info = config_info_for_log_size(log_size);
println!(
"Polynomial size: 2^{} = {} elements",
log_size, info.poly_size
);
println!("Recursive steps: {}", config.recursive_steps);
println!("Initial k: {}", config.initial_k);
println!("Recursive ks: {:?}", config.ks);
println!("Log dimensions: {:?}", config.log_dims);
let poly_size_bytes = info.poly_size * 4; println!("\nEstimated sizes:");
println!(
" Polynomial: {} bytes ({:.2} MB)",
poly_size_bytes,
poly_size_bytes as f64 / 1_048_576.0
);
println!(" Proof: ~{} KB", info.estimated_proof_bytes / 1024);
}
fn generate_command(size: u32, pattern: &str, output: Option<String>) -> Result<()> {
if !(MIN_LOG_SIZE..=MAX_LOG_SIZE).contains(&size) {
anyhow::bail!("Size must be between {} and {}", MIN_LOG_SIZE, MAX_LOG_SIZE);
}
let len = 1usize << size;
eprintln!(
"Generating 2^{} = {} elements with pattern '{}'",
size, len, pattern
);
let poly: Vec<BinaryElem32> = match pattern {
"random" => {
use rand::Rng;
let mut rng = rand::thread_rng();
(0..len)
.map(|_| BinaryElem32::from(rng.gen::<u32>()))
.collect()
}
"zeros" => vec![BinaryElem32::from(0); len],
"ones" => vec![BinaryElem32::from(1); len],
"sequential" => (0..len).map(|i| BinaryElem32::from(i as u32)).collect(),
_ => anyhow::bail!(
"Unknown pattern '{}'. Use: random, zeros, ones, sequential",
pattern
),
};
let bytes = bytemuck::cast_slice(&poly).to_vec();
match output {
Some(path) => {
let mut file =
File::create(&path).context(format!("Failed to create output file: {}", path))?;
file.write_all(&bytes)
.context("Failed to write polynomial data")?;
eprintln!("Wrote {} bytes to {}", bytes.len(), path);
}
None => {
io::stdout()
.write_all(&bytes)
.context("Failed to write polynomial data to stdout")?;
}
}
Ok(())
}
fn bench_command(size: u32, iterations: usize, do_verify: bool) -> Result<()> {
use ligerito::prove_sha256;
if !(MIN_LOG_SIZE..=MAX_LOG_SIZE).contains(&size) {
anyhow::bail!("Size must be between {} and {}", MIN_LOG_SIZE, MAX_LOG_SIZE);
}
eprintln!("Benchmarking 2^{} ({} elements)", size, 1usize << size);
eprintln!("Iterations: {}", iterations);
eprintln!("Threads: {}", rayon::current_num_threads());
let poly: Vec<BinaryElem32> = (0..(1usize << size))
.map(|i| BinaryElem32::from(i as u32))
.collect();
let prover_config = prover_config_for_log_size::<BinaryElem32, BinaryElem128>(size);
let verifier_config = verifier_config_for_log_size(size);
eprintln!("Warming up...");
let warmup_proof = prove_sha256(&prover_config, &poly).context("Warmup failed")?;
eprintln!("Running prove benchmark...");
let mut prove_times = Vec::new();
let mut proof = warmup_proof;
for i in 0..iterations {
let start = Instant::now();
proof = prove_sha256(&prover_config, &poly).context("Proving failed")?;
let elapsed = start.elapsed();
prove_times.push(elapsed);
eprintln!(" Run {}: {:.2}ms", i + 1, elapsed.as_secs_f64() * 1000.0);
}
let avg_prove =
prove_times.iter().map(|d| d.as_millis()).sum::<u128>() / prove_times.len() as u128;
let min_prove = prove_times.iter().map(|d| d.as_millis()).min().unwrap();
let max_prove = prove_times.iter().map(|d| d.as_millis()).max().unwrap();
eprintln!("\nProve results:");
eprintln!(" Average: {}ms", avg_prove);
eprintln!(" Min: {}ms", min_prove);
eprintln!(" Max: {}ms", max_prove);
eprintln!(" Proof size: {} bytes", proof.size_of());
if do_verify {
eprintln!("\nRunning verify benchmark...");
let mut verify_times = Vec::new();
for i in 0..iterations {
let start = Instant::now();
let valid =
ligerito::verify_sha256(&verifier_config, &proof).context("Verification failed")?;
let elapsed = start.elapsed();
verify_times.push(elapsed);
eprintln!(
" Run {}: {:.2}ms ({})",
i + 1,
elapsed.as_secs_f64() * 1000.0,
if valid { "VALID" } else { "INVALID" }
);
}
let avg_verify =
verify_times.iter().map(|d| d.as_millis()).sum::<u128>() / verify_times.len() as u128;
let min_verify = verify_times.iter().map(|d| d.as_millis()).min().unwrap();
eprintln!("\nVerify results:");
eprintln!(" Average: {}ms", avg_verify);
eprintln!(" Min: {}ms", min_verify);
}
Ok(())
}