This crate provides example circuits for the Binius zero-knowledge proof system. These examples serve multiple purposes:
- Testing: Verify that the Binius framework works correctly with real-world circuits
- Profiling: Benchmark and optimize the performance of proof generation and verification
- Learning: Demonstrate best practices and patterns for building circuits with Binius
- sha256: SHA-256 hash function implementation demonstrating efficient binary field arithmetic
- zklogin: Zero-knowledge authentication circuit for JWT verification
Each example is a standalone binary that can be run with customizable parameters to test different configurations and input sizes.
This guide shows how to create new circuit examples using the standardized ExampleCircuit trait and the simplified CLI API.
Here's a minimal template for a new circuit example:
use anyhow::{ensure, Result};
use binius_examples::{Cli, ExampleCircuit};
use binius_frontend::compiler::{circuit::WitnessFiller, CircuitBuilder, Wire};
use clap::Args;
// The main example struct that holds circuit components
struct MyCircuitExample {
params: Params,
// Store any gadgets or wire references needed for witness population
// e.g., gadget: MyGadget,
}
// Circuit parameters that affect structure (compile-time configuration)
#[derive(Args, Debug)]
struct Params {
/// Maximum size for the circuit
#[arg(long, default_value_t = 1024)]
max_size: usize,
/// Whether to use optimized mode
#[arg(long, default_value_t = false)]
optimized: bool,
}
// Instance data for witness population (runtime values)
#[derive(Args, Debug)]
struct Instance {
/// Input value (if not provided, random data is generated)
#[arg(long)]
input: Option<String>,
/// Size of the input
#[arg(long)]
size: Option<usize>,
}
impl ExampleCircuit for MyCircuitExample {
type Params = Params;
type Instance = Instance;
fn build(params: Params, builder: &mut CircuitBuilder) -> Result<Self> {
// Build your circuit here
// 1. Add witnesses
// 2. Add constants
// 3. Create gadgets
// 4. Add constraints
// Example:
// let input_wire = builder.add_witness();
// let output_wire = builder.add_inout();
// let gadget = MyGadget::new(builder, params.max_size, input_wire, output_wire);
Ok(Self {
params,
// gadget,
})
}
fn populate_witness(&self, instance: Instance, w: &mut WitnessFiller) -> Result<()> {
// Process instance data and populate witness values
// Example with random or user-provided input:
let input_data = if let Some(input) = instance.input {
// Process user-provided input
input.as_bytes().to_vec()
} else {
// Generate random data
let size = instance.size.unwrap_or(self.params.max_size);
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
let mut data = vec![0u8; size];
rand::RngCore::fill_bytes(&mut rng, &mut data);
data
};
// Validate instance data against circuit parameters
ensure!(
input_data.len() <= self.params.max_size,
"Input size ({}) exceeds maximum ({})",
input_data.len(),
self.params.max_size
);
// Populate witness values
// self.gadget.populate_input(w, &input_data);
// self.gadget.populate_output(w, &output);
Ok(())
}
}
fn main() -> Result<()> {
let _tracing_guard = tracing_profile::init_tracing()?;
// Create and run the CLI - this is all you need!
Cli::<MyCircuitExample>::new("my_circuit")
.about("Description of what your circuit does")
.run()
}The new API requires only three things from developers:
- Implement
ExampleCircuit- Define your circuit logic - Define
ParamsandInstancestructs - Use#[derive(Args)]for automatic CLI parsing - Call
Cli::new().run()- The library handles everything else
No more manual CLI struct definitions or boilerplate code!
-
Params: Circuit structure configuration (compile-time)
- Maximum sizes, bounds, modes
- Affects how the circuit is built
- Examples:
max_len,exact_len,use_optimization
-
Instance: Witness data (runtime)
- Actual input values for a specific proof
- Should be validated against params
- Examples:
message,input_value,seed
In the build method:
- Create witnesses using
builder.add_witness() - Create constants using
builder.add_constant_64() - Create input/output wires using
builder.add_inout() - Instantiate gadgets with the builder
- Store references needed for witness population
In the populate_witness method:
- Process instance data (parse, validate, generate if needed)
- Validate against circuit parameters
- Populate all witness values using the stored references
- Use deterministic randomness (
StdRng::seed_from_u64(0)) for reproducibility
- Use
ensure!for validation with clear error messages - Return
Result<()>from all trait methods - Validate instance data against params before populating witnesses
The Cli builder provides additional customization options:
Cli::<MyExample>::new("my_circuit")
.about("Short description") // Shown in help
.long_about("Detailed description") // Shown with --help
.version("1.0.0") // Version info
.author("Your Name") // Author info
.run()let data = if let Some(user_input) = instance.input {
user_input.as_bytes().to_vec()
} else {
let mut rng = StdRng::seed_from_u64(0);
let mut data = vec![0u8; size];
rng.fill_bytes(&mut data);
data
};let len_wire = if params.exact_len {
builder.add_constant_64(params.max_len as u64)
} else {
builder.add_witness()
};use sha2::{Digest, Sha256};
let hash: [u8; 32] = Sha256::digest(&data).into();Use clap's derive attributes to customize CLI arguments:
#[derive(Args, Debug)]
struct Params {
/// Help text for the argument
#[arg(long, short = 'n', default_value_t = 100)]
number: usize,
/// Optional argument
#[arg(long)]
optional_value: Option<String>,
/// Boolean flag
#[arg(long, short)]
verbose: bool,
/// Value with custom parser
#[arg(long, value_parser = clap::value_parser!(u32).range(1..100))]
percentage: u32,
}For mutually exclusive options:
#[derive(Args, Debug)]
#[group(multiple = false)]
struct Instance {
#[arg(long)]
from_file: Option<String>,
#[arg(long)]
from_stdin: bool,
}Build and run your example:
# Build
cargo build --release --example my_circuit
# Run with default parameters
cargo run --release --example my_circuit
# Run with custom parameters
cargo run --release --example my_circuit -- --max-size 2048 --input "test data"
# Show help
cargo run --release --example my_circuit -- --help
# Run with increased verbosity
RUST_LOG=info cargo run --release --example my_circuitAll example binaries share a common CLI with these subcommands:
- prove (default): build the circuit, generate witness, create and verify a proof
- stat: print circuit statistics
- composition: output circuit composition as JSON
- check-snapshot: compare current stats with the stored snapshot
- bless-snapshot: update the stored snapshot with current stats
- save: save artifacts to files (only those explicitly requested)
Use the save subcommand to write selected artifacts to disk. Nothing is written unless a corresponding path is provided.
Flags:
- --cs-path PATH: write the constraint system binary
- --pub-witness-path PATH: write the public witness values binary
- --non-pub-data-path PATH: write the non-public witness values binary
Examples:
# Save only the constraint system
cargo run --release --example my_circuit -- save --cs-path out/cs.bin
# Save public values and non-public values
cargo run --release --example my_circuit -- save \
--pub-witness-path out/public.bin \
--non-pub-data-path out/non_public.bin
# Save all three
cargo run --release --example my_circuit -- save \
--cs-path out/cs.bin \
--pub-witness-path out/public.bin \
--non-pub-data-path out/non_public.binNotes:
- Public and non-public outputs are serialized using the versioned ValuesData format from core.
- Parent directories are created automatically if they don’t exist.
Add your example to crates/examples/Cargo.toml:
[[example]]
name = "my_circuit"
path = "examples/my_circuit.rs"Look at these examples for reference:
sha256.rs- Shows parameter/instance separation, random data generationzklogin.rs- Shows complex witness population with external data generation
The prover example binary reads a constraint system and witnesses from disk and produces a serialized proof. This is useful for cross-host proof generation pipelines.
Arguments:
--cs-path PATH: path to the constraint system binary--pub-witness-path PATH: path to the public values binary (ValuesData)--non-pub-data-path PATH: path to the non-public values binary (ValuesData)--proof-path PATH: path to write the proof binary-l, --log-inv-rate N: log of the inverse rate (default: 1)
Usage:
# 1) Generate artifacts from an example circuit (e.g., sha256)
cargo run --release --example sha256 -- save \
--cs-path out/sha256/cs.bin \
--pub-witness-path out/sha256/public.bin \
--non-pub-data-path out/sha256/non_public.bin
# 2) Produce a proof from those files
cargo run --release --example prover -- \
--cs-path out/sha256/cs.bin \
--pub-witness-path out/sha256/public.bin \
--non-pub-data-path out/sha256/non_public.bin \
--proof-path out/sha256/proof.bin \
--log-inv-rate 1- Keep it simple: The main function should just create the CLI and run it
- Use descriptive help text: Document what each parameter does
- Validate early: Check instance compatibility with params in
populate_witness - Use deterministic randomness: Always seed with a fixed value for reproducibility
- Store what you need: Keep references to gadgets/wires in your struct for witness population