Skip to main content

binius_examples/
cli.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3use std::{fs, path::Path};
4
5use anyhow::Result;
6use binius_core::constraint_system::{ConstraintSystem, Proof, ValueVec, ValuesData, ValuesRef};
7use binius_frontend::{CircuitBuilder, CircuitStat};
8use binius_hash::{Blake3HashSuite, StdHashSuite};
9use binius_hash_prover::ParallelHashSuite;
10use binius_utils::serialization::{DeserializeBytes, SerializeBytes};
11use clap::{Arg, ArgMatches, Args, Command, FromArgMatches};
12use digest::Output;
13
14use crate::{
15	ExampleCircuit, HashSuiteType, check_proof, check_proof_zk, create_proof, create_proof_zk,
16	prove_verify, setup, setup_verifier, setup_zk, setup_zk_verifier,
17};
18
19/// Write raw bytes to the given path, creating the parent directory if it is missing.
20///
21/// Naming an output under a directory that does not exist yet is the common case here.
22/// So the directory is created rather than reported as an error.
23fn write_file(path: &str, bytes: &[u8]) -> Result<()> {
24	// A bare filename has an empty parent, which needs no directory created.
25	if let Some(parent) = Path::new(path).parent()
26		&& !parent.as_os_str().is_empty()
27	{
28		fs::create_dir_all(parent).map_err(|e| {
29			anyhow::anyhow!("Failed to create directory '{}': {}", parent.display(), e)
30		})?;
31	}
32	fs::write(path, bytes)
33		.map_err(|e| anyhow::anyhow!("Failed to write serialized data to '{}': {}", path, e))
34}
35
36/// Serialize a value implementing `SerializeBytes` and write it to the given path.
37fn write_serialized<T: SerializeBytes>(value: &T, path: &str) -> Result<()> {
38	let mut buf: Vec<u8> = Vec::new();
39	value.serialize(&mut buf)?;
40	write_file(path, &buf)
41}
42
43/// Deserialize a value implementing `DeserializeBytes` from the given path.
44fn read_deserialized<T: DeserializeBytes>(path: &str) -> Result<T> {
45	let buf =
46		fs::read(path).map_err(|e| anyhow::anyhow!("Failed to read file '{}': {}", path, e))?;
47	T::deserialize(buf.as_slice())
48		.map_err(|e| anyhow::anyhow!("Failed to deserialize data from '{}': {}", path, e))
49}
50
51/// Log the proof size and, if `output` is `Some`, serialize and write the proof to that path.
52fn maybe_write_proof(proof_bytes: &[u8], output: Option<&str>) -> Result<()> {
53	tracing::info!("Proof size: {} KiB", proof_bytes.len() / 1024);
54	if let Some(path) = output {
55		use binius_verifier::config::{ChallengerWithName, StdChallenger};
56		let proof = Proof::owned(proof_bytes.to_vec(), StdChallenger::NAME.to_string());
57		write_serialized(&proof, path)?;
58		tracing::info!("Proof written to '{}'", path);
59	}
60	Ok(())
61}
62
63/// Prove and verify with the given `HashSuite`, branching on `zk`.
64fn prove_with_hash_suite<H>(
65	cs: ConstraintSystem,
66	log_inv_rate: usize,
67	zk: bool,
68	message: Option<&[u8]>,
69	witness: &ValueVec,
70	output: Option<&str>,
71) -> Result<()>
72where
73	H: ParallelHashSuite + Clone,
74	Output<H::LeafHash>: SerializeBytes + DeserializeBytes,
75{
76	if zk {
77		let (verifier, prover) = setup_zk::<H>(cs, log_inv_rate)?;
78		let proof_bytes = create_proof_zk(&prover, witness, message)?;
79		maybe_write_proof(&proof_bytes, output)?;
80		check_proof_zk(&verifier, witness, proof_bytes, message)?;
81	} else {
82		let (verifier, prover) = setup::<H>(cs, log_inv_rate, None)?;
83		let proof_bytes = create_proof(&prover, witness)?;
84		maybe_write_proof(&proof_bytes, output)?;
85		check_proof(&verifier, witness, proof_bytes)?;
86	}
87	Ok(())
88}
89
90/// Verify a proof with the given `HashSuite`, branching on `zk`.
91fn verify_with_hash_suite<H>(
92	cs: ConstraintSystem,
93	log_inv_rate: usize,
94	zk: bool,
95	message: Option<&[u8]>,
96	witness: &ValueVec,
97	proof_bytes: Vec<u8>,
98) -> Result<()>
99where
100	H: ParallelHashSuite + Clone,
101	Output<H::LeafHash>: SerializeBytes + DeserializeBytes,
102{
103	if zk {
104		let verifier = setup_zk_verifier::<H>(cs, log_inv_rate)?;
105		check_proof_zk(&verifier, witness, proof_bytes, message)?;
106	} else {
107		let verifier = setup_verifier::<H>(cs, log_inv_rate)?;
108		check_proof(&verifier, witness, proof_bytes)?;
109	}
110	Ok(())
111}
112
113/// Prove and verify, with the Merkle hash suite chosen at run time.
114///
115/// This is deliberately not generic. A non-generic function is codegen'd once, in this library,
116/// so every example binary links one copy of the proving stack instead of instantiating its own.
117fn prove_dispatch(
118	cs: ConstraintSystem,
119	log_inv_rate: usize,
120	hash_suite: HashSuiteType,
121	zk: bool,
122	message: Option<&[u8]>,
123	witness: &ValueVec,
124	output: Option<&str>,
125) -> Result<()> {
126	match hash_suite {
127		HashSuiteType::Sha256 => {
128			tracing::info!("Using SHA-256 hash suite for Merkle tree");
129			prove_with_hash_suite::<StdHashSuite>(cs, log_inv_rate, zk, message, witness, output)
130		}
131		HashSuiteType::Blake3 => {
132			tracing::info!("Using Blake3 hash suite for Merkle tree");
133			prove_with_hash_suite::<Blake3HashSuite>(cs, log_inv_rate, zk, message, witness, output)
134		}
135	}
136}
137
138/// Verify a proof, with the Merkle hash suite chosen at run time.
139///
140/// Non-generic for the same reason as [`prove_dispatch`].
141fn verify_dispatch(
142	cs: ConstraintSystem,
143	log_inv_rate: usize,
144	hash_suite: HashSuiteType,
145	zk: bool,
146	message: Option<&[u8]>,
147	witness: &ValueVec,
148	proof_bytes: Vec<u8>,
149) -> Result<()> {
150	match hash_suite {
151		HashSuiteType::Sha256 => {
152			tracing::info!("Using SHA-256 hash suite for Merkle tree");
153			verify_with_hash_suite::<StdHashSuite>(
154				cs,
155				log_inv_rate,
156				zk,
157				message,
158				witness,
159				proof_bytes,
160			)
161		}
162		HashSuiteType::Blake3 => {
163			tracing::info!("Using Blake3 hash suite for Merkle tree");
164			verify_with_hash_suite::<Blake3HashSuite>(
165				cs,
166				log_inv_rate,
167				zk,
168				message,
169				witness,
170				proof_bytes,
171			)
172		}
173	}
174}
175
176/// Write whichever of the proving-job artifacts were given a path.
177///
178/// Non-generic for the same reason as [`prove_dispatch`].
179fn save_artifacts(
180	cs: &ConstraintSystem,
181	witness: &ValueVec,
182	cs_path: Option<&str>,
183	pub_witness_path: Option<&str>,
184	non_pub_data_path: Option<&str>,
185	key_collection_path: Option<&str>,
186) -> Result<()> {
187	if let Some(path) = cs_path {
188		write_serialized(cs, path)?;
189		tracing::info!("Constraint system saved to '{}'", path);
190	}
191
192	if let Some(path) = pub_witness_path {
193		// Only the inout values: the constants ride along in the constraint system.
194		write_serialized(&ValuesRef::new(witness.inout()), path)?;
195		tracing::info!("Inout witness saved to '{}'", path);
196	}
197
198	if let Some(path) = non_pub_data_path {
199		write_serialized(&ValuesRef::new(witness.non_public()), path)?;
200		tracing::info!("Non-public witness saved to '{}'", path);
201	}
202
203	if let Some(path) = key_collection_path {
204		let key_collection_scope = tracing::info_span!("Building key collection").entered();
205		let key_collection = binius_prover::protocols::shift::KeyCollection::build(
206			cs,
207			binius_core::constraint_system::InoutSegment::Public,
208		);
209		drop(key_collection_scope);
210		write_serialized(&key_collection, path)?;
211		tracing::info!("Key collection saved to '{}'", path);
212	}
213
214	Ok(())
215}
216
217/// Prove and verify a constraint system and witness read from files.
218///
219/// Nothing here depends on the example circuit, so it lives outside the generic
220/// [`Cli`] impl and is codegen'd once, in this library.
221fn run_load_prove(matches: &ArgMatches) -> Result<()> {
222	// Extract file paths and parameters
223	let cs_path = matches
224		.get_one::<String>("cs_path")
225		.expect("cs_path is required");
226	let pub_witness_path = matches
227		.get_one::<String>("pub_witness_path")
228		.expect("pub_witness_path is required");
229	let non_pub_data_path = matches
230		.get_one::<String>("non_pub_data_path")
231		.expect("non_pub_data_path is required");
232	let key_collection_path = matches.get_one::<String>("key_collection_path").cloned();
233	let log_inv_rate = *matches
234		.get_one::<u32>("log_inv_rate")
235		.expect("has default value");
236	let hash_suite = *matches
237		.get_one::<HashSuiteType>("hash_suite")
238		.expect("has default value");
239
240	// Load constraint system
241	let cs_load_scope = tracing::info_span!("Loading constraint system").entered();
242	let cs: ConstraintSystem = read_deserialized(cs_path)?;
243	tracing::info!("Constraint system loaded from '{}'", cs_path);
244	drop(cs_load_scope);
245
246	// Load pre-built KeyCollection if path provided
247	let maybe_key_collection = key_collection_path
248		.map(|kc_path| -> Result<_> {
249			let kc_load_scope = tracing::info_span!("Loading key collection").entered();
250			let key_collection: binius_prover::KeyCollection = read_deserialized(&kc_path)?;
251			tracing::info!("Key collection loaded from '{kc_path}'");
252			drop(kc_load_scope);
253			Ok(key_collection)
254		})
255		.transpose()?;
256
257	// Load witness data
258	let witness_load_scope = tracing::info_span!("Loading witness data").entered();
259	let inout: ValuesData = read_deserialized(pub_witness_path)?;
260	tracing::info!("Public inout values loaded from '{}'", pub_witness_path);
261
262	let non_pub_data: ValuesData = read_deserialized(non_pub_data_path)?;
263	tracing::info!("Non-public data loaded from '{}'", non_pub_data_path);
264
265	// Reconstruct the full witness from its two segments
266	let witness = cs.value_vec_from_data(&inout, &non_pub_data);
267	drop(witness_load_scope);
268
269	match hash_suite {
270		HashSuiteType::Sha256 => {
271			tracing::info!("Using SHA-256 hash suite for Merkle tree");
272			let (verifier, prover) =
273				setup::<StdHashSuite>(cs, log_inv_rate as usize, maybe_key_collection)?;
274			prove_verify(&verifier, &prover, &witness)?;
275		}
276		HashSuiteType::Blake3 => {
277			tracing::info!("Using Blake3 hash suite for Merkle tree");
278			let (verifier, prover) =
279				setup::<Blake3HashSuite>(cs, log_inv_rate as usize, maybe_key_collection)?;
280			prove_verify(&verifier, &prover, &witness)?;
281		}
282	};
283
284	Ok(())
285}
286
287/// The example-circuit CLI: one subcommand per circuit.
288///
289/// Each circuit is registered with [`Cli::circuit`], which gives it a subcommand carrying the
290/// full circuit CLI (`prove`, `stat`, `check-snapshot`, …) built from its `Params` and
291/// `Instance` types.
292///
293/// # Example
294///
295/// ```rust,ignore
296/// fn main() -> Result<()> {
297///     Cli::new("binius-examples")
298///         .circuit::<MyExample>("my_circuit", "Description of my circuit")
299///         .run()
300/// }
301/// ```
302pub struct Cli {
303	command: Command,
304	runners: Vec<(&'static str, Runner)>,
305}
306
307/// Runs one circuit from the `ArgMatches` of its subcommand and that subcommand's name.
308type Runner = fn(&ArgMatches, &str) -> Result<()>;
309
310impl Cli {
311	/// Create a CLI with no circuits registered yet.
312	pub fn new(name: &'static str) -> Self {
313		Self {
314			command: Command::new(name)
315				.subcommand_required(true)
316				.arg_required_else_help(true),
317			runners: Vec::new(),
318		}
319	}
320
321	/// Register one circuit as a subcommand named `name`.
322	pub fn circuit<E: ExampleCircuit>(mut self, name: &'static str, about: &'static str) -> Self {
323		self.command = self
324			.command
325			.subcommand(build_command::<E>(name).about(about));
326		self.runners.push((name, run::<E>));
327		self
328	}
329
330	/// Parse the command line and run the selected circuit.
331	pub fn run(self) -> Result<()> {
332		let matches = self.command.get_matches();
333		let (name, sub_matches) = matches.subcommand().expect("subcommand is required");
334		let (_, run) = self
335			.runners
336			.iter()
337			.find(|(registered, _)| *registered == name)
338			.expect("clap only accepts a registered subcommand");
339		run(sub_matches, name)
340	}
341}
342
343/// Build the subcommand tree for one circuit.
344///
345/// The circuit's `Params` and `Instance` flags are also lifted onto the subcommand itself, so
346/// `<circuit> --flag` keeps meaning `<circuit> prove --flag`.
347fn build_command<E: ExampleCircuit>(name: &'static str) -> Command {
348	let command = Command::new(name)
349		.subcommand_required(false)
350		.arg_required_else_help(false);
351
352	// Build subcommands
353	let prove_cmd = build_prove_subcommand::<E>();
354	let stat_cmd = build_stat_subcommand::<E>();
355	let composition_cmd = build_composition_subcommand::<E>();
356	let check_snapshot_cmd = build_check_snapshot_subcommand::<E>();
357	let bless_snapshot_cmd = build_bless_snapshot_subcommand::<E>();
358	let save_cmd = build_save_subcommand::<E>();
359	let load_prove_cmd = build_load_prove_subcommand();
360	let verify_cmd = build_verify_subcommand::<E>();
361
362	let command = command
363		.subcommand(prove_cmd)
364		.subcommand(stat_cmd)
365		.subcommand(composition_cmd)
366		.subcommand(check_snapshot_cmd)
367		.subcommand(bless_snapshot_cmd)
368		.subcommand(save_cmd)
369		.subcommand(load_prove_cmd)
370		.subcommand(verify_cmd);
371
372	// Add top-level args for default prove behavior (when no subcommand specified)
373	let command = command
374		.arg(
375			Arg::new("log_inv_rate")
376				.short('l')
377				.long("log-inv-rate")
378				.value_name("RATE")
379				.help("Log of the inverse rate for the proof system")
380				.default_value("1")
381				.value_parser(clap::value_parser!(u32).range(1..)),
382		)
383		.arg(
384			Arg::new("hash_suite")
385				.short('c')
386				.long("hash-suite")
387				.alias("compression")
388				.value_name("SUITE")
389				.help("Merkle hash suite to use (leaf hash + inner-node compression)")
390				.value_parser(clap::value_parser!(HashSuiteType))
391				.default_value("sha256"),
392		)
393		.arg(
394			Arg::new("zk")
395				.long("zk")
396				.help("Use the zero-knowledge proving config")
397				.action(clap::ArgAction::SetTrue),
398		)
399		.arg(
400			Arg::new("sign_message")
401				.long("sign-message")
402				.value_name("MESSAGE")
403				.requires("zk")
404				.help(
405					"Produce a zero-knowledge signature of knowledge over this message \
406					 instead of a plain proof of knowledge (requires --zk)",
407				),
408		)
409		.arg(
410			Arg::new("output")
411				.short('o')
412				.long("output")
413				.value_name("PATH")
414				.help("Write the serialized proof to this file"),
415		);
416
417	// Augment with Params arguments at top level for default behavior
418	let command = E::Params::augment_args(command);
419	E::Instance::augment_args(command)
420}
421
422fn build_prove_subcommand<E: ExampleCircuit>() -> Command {
423	let mut cmd = Command::new("prove")
424		.about("Generate and verify a proof")
425		.arg(
426			Arg::new("log_inv_rate")
427				.short('l')
428				.long("log-inv-rate")
429				.value_name("RATE")
430				.help("Log of the inverse rate for the proof system")
431				.default_value("1")
432				.value_parser(clap::value_parser!(u32).range(1..)),
433		)
434		.arg(
435			Arg::new("hash_suite")
436				.short('c')
437				.long("hash-suite")
438				.alias("compression")
439				.value_name("SUITE")
440				.help("Merkle hash suite to use (leaf hash + inner-node compression)")
441				.value_parser(clap::value_parser!(HashSuiteType))
442				.default_value("sha256"),
443		)
444		.arg(
445			Arg::new("zk")
446				.long("zk")
447				.help("Use the zero-knowledge proving config")
448				.action(clap::ArgAction::SetTrue),
449		)
450		.arg(
451			Arg::new("sign_message")
452				.long("sign-message")
453				.value_name("MESSAGE")
454				.requires("zk")
455				.help(
456					"Produce a zero-knowledge signature of knowledge over this message \
457					 instead of a plain proof of knowledge (requires --zk)",
458				),
459		)
460		.arg(
461			Arg::new("output")
462				.short('o')
463				.long("output")
464				.value_name("PATH")
465				.help("Write the serialized proof to this file"),
466		);
467	cmd = E::Params::augment_args(cmd);
468	cmd = E::Instance::augment_args(cmd);
469	cmd
470}
471
472fn build_stat_subcommand<E: ExampleCircuit>() -> Command {
473	let cmd = Command::new("stat").about("Display circuit statistics");
474	E::Params::augment_args(cmd)
475}
476
477fn build_composition_subcommand<E: ExampleCircuit>() -> Command {
478	let cmd = Command::new("composition").about("Output circuit composition in JSON format");
479	E::Params::augment_args(cmd)
480}
481
482fn build_check_snapshot_subcommand<E: ExampleCircuit>() -> Command {
483	let cmd = Command::new("check-snapshot").about("Verify circuit statistics against a snapshot");
484	E::Params::augment_args(cmd)
485}
486
487fn build_bless_snapshot_subcommand<E: ExampleCircuit>() -> Command {
488	let cmd = Command::new("bless-snapshot").about("Update the snapshot with current statistics");
489	E::Params::augment_args(cmd)
490}
491
492fn build_save_subcommand<E: ExampleCircuit>() -> Command {
493	let mut cmd = Command::new("save").about(
494		"Save constraint system, public inout values, non-public data, and key collection to files if paths are provided",
495	);
496	cmd = cmd
497		.arg(
498			Arg::new("cs_path")
499				.long("cs-path")
500				.value_name("PATH")
501				.help("Output path for the constraint system binary"),
502		)
503		.arg(
504			Arg::new("pub_witness_path")
505				.long("pub-witness-path")
506				.value_name("PATH")
507				.help("Output path for the public inout values binary"),
508		)
509		.arg(
510			Arg::new("non_pub_data_path")
511				.long("non-pub-data-path")
512				.value_name("PATH")
513				.help("Output path for the non-public data (witness + internal) binary"),
514		)
515		.arg(
516			Arg::new("key_collection_path")
517				.long("key-collection-path")
518				.value_name("PATH")
519				.help("Output path for the key collection binary (for fast prover setup)"),
520		);
521	cmd = E::Params::augment_args(cmd);
522	cmd = E::Instance::augment_args(cmd);
523	cmd
524}
525
526fn build_load_prove_subcommand() -> Command {
527	Command::new("load-prove")
528		.about("Load constraint system, witness data, and optionally key collection from files and generate/verify proof")
529		.arg(
530			Arg::new("cs_path")
531				.long("cs-path")
532				.value_name("PATH")
533				.help("Input path for the constraint system binary")
534				.required(true),
535		)
536		.arg(
537			Arg::new("pub_witness_path")
538				.long("pub-witness-path")
539				.value_name("PATH")
540				.help("Input path for the public inout values binary")
541				.required(true),
542		)
543		.arg(
544			Arg::new("non_pub_data_path")
545				.long("non-pub-data-path")
546				.value_name("PATH")
547				.help("Input path for the non-public data (witness + internal) binary")
548				.required(true),
549		)
550		.arg(
551			Arg::new("key_collection_path")
552				.long("key-collection-path")
553				.value_name("PATH")
554				.help("Input path for the key collection binary (optional, for fast prover setup)"),
555		)
556		.arg(
557			Arg::new("log_inv_rate")
558				.short('l')
559				.long("log-inv-rate")
560				.value_name("RATE")
561				.help("Log of the inverse rate for the proof system")
562				.default_value("1")
563				.value_parser(clap::value_parser!(u32).range(1..)),
564		)
565		.arg(
566			Arg::new("hash_suite")
567				.short('c')
568				.long("hash-suite")
569				.alias("compression")
570				.value_name("SUITE")
571				.help("Merkle hash suite to use (leaf hash + inner-node compression)")
572				.value_parser(clap::value_parser!(HashSuiteType))
573				.default_value("sha256"),
574		)
575}
576
577fn build_verify_subcommand<E: ExampleCircuit>() -> Command {
578	let mut cmd = Command::new("verify")
579		.about("Verify a proof read from a file")
580		.arg(
581			Arg::new("proof_file")
582				.value_name("PROOF_FILE")
583				.help("Path to the proof file to verify")
584				.required(true),
585		)
586		.arg(
587			Arg::new("log_inv_rate")
588				.short('l')
589				.long("log-inv-rate")
590				.value_name("RATE")
591				.help("Log of the inverse rate for the proof system")
592				.default_value("1")
593				.value_parser(clap::value_parser!(u32).range(1..)),
594		)
595		.arg(
596			Arg::new("hash_suite")
597				.short('c')
598				.long("hash-suite")
599				.alias("compression")
600				.value_name("SUITE")
601				.help("Merkle hash suite to use (leaf hash + inner-node compression)")
602				.value_parser(clap::value_parser!(HashSuiteType))
603				.default_value("sha256"),
604		)
605		.arg(
606			Arg::new("zk")
607				.long("zk")
608				.help("Use the zero-knowledge verifier config")
609				.action(clap::ArgAction::SetTrue),
610		)
611		.arg(
612			Arg::new("sign_message")
613				.long("sign-message")
614				.value_name("MESSAGE")
615				.requires("zk")
616				.help(
617					"Verify a zero-knowledge signature of knowledge over this message \
618					 (requires --zk)",
619				),
620		);
621	cmd = E::Params::augment_args(cmd);
622	cmd = E::Instance::augment_args(cmd);
623	cmd
624}
625/// Run one circuit from the `ArgMatches` of its subcommand.
626#[allow(unused_variables)]
627fn run<E: ExampleCircuit>(matches: &ArgMatches, circuit_name: &str) -> Result<()> {
628	// Honour `RAYON_NUM_THREADS=1` by pinning the pool to this thread, which keeps profiles
629	// free of worker frames. This must run before anything else touches the pool, because the
630	// first use builds it and it can only be built once — `current_num_threads` below is one
631	// such use. The outcome is reported once tracing is up.
632	let thread_pool_result = binius_utils::rayon::config::adjust_thread_pool();
633
634	// Initialize tracing once at the beginning for all commands. In perfetto mode the
635	// returned guard must be held for the duration of the program to flush the trace.
636	#[cfg(feature = "perfetto")]
637	let _tracing_guard = {
638		// Detect threading information
639		let thread_count = binius_utils::rayon::current_num_threads();
640		let thread_mode = if thread_count == 1 { "st" } else { "mt" };
641
642		let mut builder = tracing_profile::TraceFilenameBuilder::for_benchmark(circuit_name)
643			.output_dir("perfetto_traces")
644			.timestamp() // Add timestamp for uniqueness
645			.git_info() // Include git status
646			.platform() // Include OS info
647			.thread_mode(thread_mode);
648
649		// Try to extract params from the appropriate matches for richer context
650		// This will succeed for most commands (prove, stat, save, etc.)
651		// and fail gracefully for commands without params (like load-prove)
652		let matches_for_params = matches.subcommand().map(|(_, sub)| sub).unwrap_or(matches);
653
654		if let Ok(params) = E::Params::from_arg_matches(matches_for_params)
655			&& let Some(param_summary) = E::param_summary(&params)
656		{
657			builder = builder.add("params", param_summary);
658		}
659
660		tracing_profile::init_tracing_with_builder(builder)?
661	};
662	#[cfg(not(feature = "perfetto"))]
663	crate::init_tracing();
664
665	// A failure costs only the cleaner call stacks, so it is not fatal.
666	if let Err(err) = thread_pool_result {
667		tracing::warn!("could not pin the rayon thread pool to one thread: {err}");
668	}
669
670	// Check if a subcommand was used
671	match matches.subcommand() {
672		Some(("prove", sub_matches)) => run_prove::<E>(sub_matches),
673		Some(("stat", sub_matches)) => run_stat::<E>(sub_matches),
674		Some(("composition", sub_matches)) => run_composition::<E>(sub_matches),
675		Some(("check-snapshot", sub_matches)) => run_check_snapshot::<E>(sub_matches, circuit_name),
676		Some(("bless-snapshot", sub_matches)) => run_bless_snapshot::<E>(sub_matches, circuit_name),
677		Some(("save", sub_matches)) => run_save::<E>(sub_matches),
678		Some(("load-prove", sub_matches)) => run_load_prove(sub_matches),
679		Some(("verify", sub_matches)) => run_verify::<E>(sub_matches),
680		Some((cmd, _)) => anyhow::bail!("Unknown subcommand: {}", cmd),
681		None => {
682			// No subcommand - default to prove behavior for backward compatibility
683			run_prove::<E>(matches)
684		}
685	}
686}
687
688fn run_prove<E: ExampleCircuit>(matches: &ArgMatches) -> Result<()> {
689	// Extract common arguments
690	let log_inv_rate = *matches
691		.get_one::<u32>("log_inv_rate")
692		.expect("has default value");
693	let hash_suite = *matches
694		.get_one::<HashSuiteType>("hash_suite")
695		.expect("has default value");
696	let zk = matches.get_flag("zk");
697	let sign_message = matches.get_one::<String>("sign_message").cloned();
698	let output = matches.get_one::<String>("output").cloned();
699	tracing::info!("Parsed hash suite: {hash_suite:?}");
700	if zk {
701		tracing::info!("Using zero-knowledge proving config");
702	}
703	if sign_message.is_some() {
704		tracing::info!("Producing a signature of knowledge over the provided message");
705	}
706	let message = sign_message.as_deref().map(str::as_bytes);
707
708	// Parse Params and Instance from matches
709	let params = E::Params::from_arg_matches(matches)?;
710	let instance = E::Instance::from_arg_matches(matches)?;
711
712	// Build the circuit
713	let build_scope = tracing::info_span!("Building circuit").entered();
714	let mut builder = CircuitBuilder::new();
715	let example = E::build(params, &mut builder)?;
716	let circuit = builder.build();
717	drop(build_scope);
718
719	// Set up prover and verifier
720	let cs = circuit.constraint_system().clone();
721
722	// Population of the input to the witness and then evaluating the circuit.
723	let witness_population = tracing::info_span!(
724		"Generating witness",
725		operation = "witness_generation",
726		perfetto_category = "operation"
727	)
728	.entered();
729	let mut filler = circuit.new_witness_filler();
730	tracing::info_span!("Input population")
731		.in_scope(|| example.populate_witness(instance, &mut filler))?;
732	tracing::info_span!("Circuit evaluation")
733		.in_scope(|| circuit.populate_wire_witness(&mut filler))?;
734	let witness = filler.into_value_vec();
735	drop(witness_population);
736
737	prove_dispatch(cs, log_inv_rate as usize, hash_suite, zk, message, &witness, output.as_deref())
738}
739
740fn run_stat<E: ExampleCircuit>(matches: &ArgMatches) -> Result<()> {
741	// Parse Params from matches
742	let params = E::Params::from_arg_matches(matches)?;
743
744	// Build the circuit
745	let mut builder = CircuitBuilder::new();
746	let _example = E::build(params, &mut builder)?;
747	let circuit = builder.build();
748
749	// Print statistics
750	let stat = CircuitStat::collect(&circuit);
751	print!("{}", stat);
752
753	Ok(())
754}
755
756fn run_composition<E: ExampleCircuit>(matches: &ArgMatches) -> Result<()> {
757	// Parse Params from matches
758	let params = E::Params::from_arg_matches(matches)?;
759
760	// Build the circuit
761	let mut builder = CircuitBuilder::new();
762	let _example = E::build(params, &mut builder)?;
763	let circuit = builder.build();
764
765	// Print composition
766	let dump = circuit.simple_json_dump();
767	println!("{}", dump);
768
769	Ok(())
770}
771
772fn run_check_snapshot<E: ExampleCircuit>(matches: &ArgMatches, circuit_name: &str) -> Result<()> {
773	// Parse Params from matches
774	let params = E::Params::from_arg_matches(matches)?;
775
776	// Build the circuit
777	let mut builder = CircuitBuilder::new();
778	let _example = E::build(params, &mut builder)?;
779	let circuit = builder.build();
780
781	// Check snapshot
782	crate::snapshot::check_snapshot(circuit_name, &circuit)?;
783
784	Ok(())
785}
786
787fn run_bless_snapshot<E: ExampleCircuit>(matches: &ArgMatches, circuit_name: &str) -> Result<()> {
788	// Parse Params from matches
789	let params = E::Params::from_arg_matches(matches)?;
790
791	// Build the circuit
792	let mut builder = CircuitBuilder::new();
793	let _example = E::build(params, &mut builder)?;
794	let circuit = builder.build();
795
796	// Bless snapshot
797	crate::snapshot::bless_snapshot(circuit_name, &circuit)?;
798
799	Ok(())
800}
801
802fn run_save<E: ExampleCircuit>(matches: &ArgMatches) -> Result<()> {
803	// Extract optional output paths
804	let cs_path = matches.get_one::<String>("cs_path").cloned();
805	let pub_witness_path = matches.get_one::<String>("pub_witness_path").cloned();
806	let non_pub_data_path = matches.get_one::<String>("non_pub_data_path").cloned();
807	let key_collection_path = matches.get_one::<String>("key_collection_path").cloned();
808
809	// If nothing to save, exit early
810	if cs_path.is_none()
811		&& pub_witness_path.is_none()
812		&& non_pub_data_path.is_none()
813		&& key_collection_path.is_none()
814	{
815		tracing::info!("No output paths provided; nothing to save");
816		return Ok(());
817	}
818
819	// Parse Params and Instance
820	let params = E::Params::from_arg_matches(matches)?;
821	let instance = E::Instance::from_arg_matches(matches)?;
822
823	// Build circuit
824	let mut builder = CircuitBuilder::new();
825	let example = E::build(params, &mut builder)?;
826	let circuit = builder.build();
827
828	// Generate witness
829	let mut filler = circuit.new_witness_filler();
830	example.populate_witness(instance, &mut filler)?;
831	circuit.populate_wire_witness(&mut filler)?;
832	let witness: ValueVec = filler.into_value_vec();
833
834	save_artifacts(
835		circuit.constraint_system(),
836		&witness,
837		cs_path.as_deref(),
838		pub_witness_path.as_deref(),
839		non_pub_data_path.as_deref(),
840		key_collection_path.as_deref(),
841	)
842}
843
844fn run_verify<E: ExampleCircuit>(matches: &ArgMatches) -> Result<()> {
845	let proof_file = matches
846		.get_one::<String>("proof_file")
847		.expect("proof_file is required");
848	let log_inv_rate = *matches
849		.get_one::<u32>("log_inv_rate")
850		.expect("has default value");
851	let hash_suite = *matches
852		.get_one::<HashSuiteType>("hash_suite")
853		.expect("has default value");
854	let zk = matches.get_flag("zk");
855	let sign_message = matches.get_one::<String>("sign_message").cloned();
856	let message = sign_message.as_deref().map(str::as_bytes);
857
858	// Read proof from file
859	let proof: Proof<'static> = read_deserialized(proof_file)?;
860	let (proof_bytes, _) = proof.into_owned();
861
862	// Parse Params and Instance from matches
863	let params = E::Params::from_arg_matches(matches)?;
864	let instance = E::Instance::from_arg_matches(matches)?;
865
866	// Build the circuit
867	let build_scope = tracing::info_span!("Building circuit").entered();
868	let mut builder = CircuitBuilder::new();
869	let example = E::build(params, &mut builder)?;
870	let circuit = builder.build();
871	drop(build_scope);
872
873	// Set up verifier
874	let cs = circuit.constraint_system().clone();
875
876	// Populate witness (needed to supply public inputs for verification)
877	let mut filler = circuit.new_witness_filler();
878	example.populate_witness(instance, &mut filler)?;
879	circuit.populate_wire_witness(&mut filler)?;
880	let witness = filler.into_value_vec();
881
882	verify_dispatch(cs, log_inv_rate as usize, hash_suite, zk, message, &witness, proof_bytes)?;
883
884	tracing::info!("Proof verified successfully.");
885	Ok(())
886}