From 8e54217386bd56fad1822f4dcde0cc0fd376c4f9 Mon Sep 17 00:00:00 2001 From: Ilya Lesokhin Date: Mon, 20 Jul 2026 11:24:59 +0300 Subject: [PATCH] feat(circuit_params): add tool to report verifier circuit component sizes Add a `circuit-params` crate with a binary that builds the leaf-prover verifier circuit (CANONICAL preprocessed trace + its component set) for a range of verified trace sizes and prints each AIR component's non-padded row count, padded log size, and usage percentage. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 16 +++ Cargo.toml | 2 + crates/circuit_params/Cargo.toml | 20 ++++ crates/circuit_params/src/main.rs | 153 ++++++++++++++++++++++++ crates/circuit_params/tests/cli_test.rs | 24 ++++ crates/leaf_prover/src/prove_leaf.rs | 43 ++++--- 6 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 crates/circuit_params/Cargo.toml create mode 100644 crates/circuit_params/src/main.rs create mode 100644 crates/circuit_params/tests/cli_test.rs diff --git a/Cargo.lock b/Cargo.lock index 0c59403c..37fec203 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1486,6 +1486,22 @@ dependencies = [ "stwo-constraint-framework", ] +[[package]] +name = "circuit-params" +version = "1.2.2" +dependencies = [ + "cairo-air", + "circuit-cairo-verifier", + "circuit-common", + "circuits-stark-verifier", + "clap", + "indexmap 2.14.0", + "leaf-prover", + "stwo-cairo-common", + "stwo-cairo-prover", + "stwo-cairo-utils", +] + [[package]] name = "circuit-prover" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 19f1f509..328b24a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/vm_runner", "crates/privacy_prove", "crates/privacy_circuit_verify", + "crates/circuit_params", ] resolver = "2" @@ -81,6 +82,7 @@ circuit-serialize = { git = "https://github.com/starkware-libs/stwo-circuits", r # local crates cairo-program-runner-lib = { path = "crates/cairo-program-runner-lib", version = "1.0.0" } +leaf-prover = { path = "crates/leaf_prover" } leaf-proof-format = { path = "crates/leaf_proof_format" } privacy-circuit-verify = { path = "crates/privacy_circuit_verify", version = "1.0.0" } stwo-run-and-prove-common = { path = "crates/stwo_run_and_prove_common", version = "1.2.2" } diff --git a/crates/circuit_params/Cargo.toml b/crates/circuit_params/Cargo.toml new file mode 100644 index 00000000..715d479b --- /dev/null +++ b/crates/circuit_params/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "circuit-params" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Compute the leaf-prover verifier circuit's per-component sizes for a range of trace sizes" + +[dependencies] +clap.workspace = true +indexmap = { version = "2.10.0", default-features = false, features = ["std"] } + +leaf-prover.workspace = true + +cairo-air.workspace = true +circuit-cairo-verifier.workspace = true +circuits-stark-verifier.workspace = true +circuit-common.workspace = true +stwo-cairo-common.workspace = true +stwo-cairo-prover.workspace = true +stwo-cairo-utils.workspace = true diff --git a/crates/circuit_params/src/main.rs b/crates/circuit_params/src/main.rs new file mode 100644 index 00000000..85f838b8 --- /dev/null +++ b/crates/circuit_params/src/main.rs @@ -0,0 +1,153 @@ +//! Computes the leaf-prover verifier circuit's per-component sizes for the CANONICAL preprocessed +//! trace config, across a range of verified trace sizes, and writes them as text. + +use std::path::PathBuf; +use std::process::ExitCode; +use std::sync::Arc; + +use cairo_air::verifier::INTERACTION_POW_BITS; +use circuit_cairo_verifier::privacy::get_pcs_config; +use circuit_cairo_verifier::statement::MEMORY_VALUES_LIMBS; +use circuit_cairo_verifier::verify::{CairoVerifierConfig, build_cairo_verifier_circuit}; +use circuit_common::finalize::{ComponentSizes, compute_padded_sizes}; +use circuits_stark_verifier::proof::ProofConfig; +use clap::Parser; +use leaf_prover::consts::DISABLED_COMPONENTS_CANONICAL_PREPROCESSED; +use leaf_prover::prove_leaf::{LeafVerifierComponents, leaf_verifier_components}; +use stwo_cairo_common::preprocessed_columns::preprocessed_trace::PreProcessedTraceVariant; +use stwo_cairo_common::prover_types::cpu::M31; +use stwo_cairo_utils::binary_utils::run_binary; + +#[derive(Parser)] +struct Args { + /// Path to write the output to. If omitted, prints to stdout. + #[clap(long)] + output_path: Option, + /// Smallest verified trace log size to measure (inclusive). A canonical Cairo trace commits + /// its preprocessed sequence columns at `MAX_SEQUENCE_LOG_SIZE = 25`, so a real canonical + /// leaf proof has `log_trace_size >= 25`. + #[clap(long)] + min_trace_log_size: u32, + /// Largest verified trace log size to measure (inclusive). + #[clap(long)] + max_trace_log_size: u32, + /// Log blowup factor of the verified Cairo proof (1, 2, or 3), passed to `get_pcs_config`. + #[clap(long = "log_blowup_factor", default_value_t = 1)] + log_blowup_factor: u32, +} + +/// The fraction (as a percentage) of the padded (power-of-two) component that is actually used. +fn usage_percent(size: usize, padded_size: usize) -> f64 { + 100.0 * size as f64 / padded_size as f64 +} + +/// The raw (non-padded) row count of each AIR component. +struct RawSizes { + eq: usize, + qm31_ops: usize, + m31_to_u32: usize, + triple_xor: usize, + blake_g_gate: usize, +} + +/// One line with each component's padded log size and usage (fraction of the padded power-of-two +/// that the non-padded rows fill). +fn format_sizes(raw: &RawSizes, padded: &ComponentSizes) -> String { + let component = |name: &str, raw_size: usize, padded_size: usize| { + format!( + "{name}:(log: {}, usage = {:.0}%)", + padded_size.ilog2(), + usage_percent(raw_size, padded_size), + ) + }; + format!( + "{} {} {} {} {}", + component("eq", raw.eq, padded.eq), + component("qm31_ops", raw.qm31_ops, padded.qm31_ops), + component("m31_to_u32", raw.m31_to_u32, padded.m31_to_u32), + component("triple_xor", raw.triple_xor, padded.triple_xor), + component("blake_g_gate", raw.blake_g_gate, padded.blake_g_gate), + ) +} + +/// Builds the leaf-prover verifier circuit topology (with the CANONICAL preprocessed trace and its +/// component set) for a verified Cairo proof whose trace has the given log size, and returns the +/// non-padded row counts and padded sizes of its AIR components. +fn leaf_verifier_component_sizes( + trace_log_size: u32, + log_blowup_factor: u32, +) -> (RawSizes, ComponentSizes) { + let preprocessed_trace_variant = PreProcessedTraceVariant::Canonical; + + // The Cairo-proof PCS config the leaf prover uses (canonical preprocessed). + let pcs_config = get_pcs_config(trace_log_size, log_blowup_factor); + + let LeafVerifierComponents { components: cairo_components, enabled_bits } = + leaf_verifier_components(&DISABLED_COMPONENTS_CANONICAL_PREPROCESSED); + + let proof_config = ProofConfig::new( + &cairo_components, + preprocessed_trace_variant.n_columns(), + &pcs_config, + INTERACTION_POW_BITS, + ); + + // Program length and output count are held fixed for this measurement; only the trace size + // varies. The preprocessed root value is irrelevant for the [NoValue] topology. + let program: Arc<[[M31; MEMORY_VALUES_LIMBS]]> = + std::iter::repeat_n([M31::from(0u32); MEMORY_VALUES_LIMBS], 128).collect(); + + let verifier_config = CairoVerifierConfig { + proof_config, + enabled_bits, + program, + n_outputs: 1, + preprocessed_root: [0u32; 8].into(), + preprocessed_trace_variant, + }; + + let context = build_cairo_verifier_circuit(&verifier_config); + let padded = compute_padded_sizes(&context); + + // Non-padded row counts, mirroring `compute_padded_sizes` before its power-of-two rounding. + let circuit = context.circuit(); + // TODO(ilya): Use `qm31_ops_n_rows` instead of counting the operations manually. + let qm31_ops = circuit.add.len() + + circuit.sub.len() + + circuit.mul.len() + + circuit.pointwise_mul.len() + + circuit.permutation.iter().map(|p| p.inputs.len() + p.outputs.len()).sum::(); + let raw = RawSizes { + eq: circuit.eq.len(), + qm31_ops, + m31_to_u32: circuit.m31_to_u32.len(), + triple_xor: circuit.triple_xor.len(), + blake_g_gate: circuit.blake_g_gate.len(), + }; + + (raw, padded) +} + +fn main() -> ExitCode { + run_binary(run, "circuit_params") +} + +fn run() -> Result<(), String> { + let args = Args::parse(); + + let output = (args.min_trace_log_size..=args.max_trace_log_size) + .map(|trace_log_size| { + let (raw, padded) = + leaf_verifier_component_sizes(trace_log_size, args.log_blowup_factor); + format!("{}: {}", trace_log_size, format_sizes(&raw, &padded)) + }) + .collect::>() + .join("\n"); + + match args.output_path { + Some(path) => std::fs::write(&path, format!("{output}\n")) + .map_err(|err| format!("Cannot write output to {}: {err}", path.display()))?, + None => println!("{output}"), + } + Ok(()) +} diff --git a/crates/circuit_params/tests/cli_test.rs b/crates/circuit_params/tests/cli_test.rs new file mode 100644 index 00000000..6a79edfb --- /dev/null +++ b/crates/circuit_params/tests/cli_test.rs @@ -0,0 +1,24 @@ +use std::process::Command; + +/// Runs the `circuit-params` binary for a single trace log size and checks it prints one info line. +#[test] +fn run_circuit_params_binary() { + let binary = env!("CARGO_BIN_EXE_circuit-params"); + let output = Command::new(binary) + .args(["--min-trace-log-size", "25", "--max-trace-log-size", "25"]) + .output() + .expect("Cannot run circuit-params"); + + assert!( + output.status.success(), + "binary exited with status {}: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8(output.stdout).expect("stdout is not valid UTF-8"); + assert!( + stdout.contains("25: eq:(log:") && stdout.contains("blake_g_gate:(log:"), + "unexpected output: {stdout}" + ); +} diff --git a/crates/leaf_prover/src/prove_leaf.rs b/crates/leaf_prover/src/prove_leaf.rs index b05790f7..45e6691b 100644 --- a/crates/leaf_prover/src/prove_leaf.rs +++ b/crates/leaf_prover/src/prove_leaf.rs @@ -91,13 +91,6 @@ pub fn prove_leaf( let preprocessed_root = proof.extended_stark_proof.proof.commitments[PREPROCESSED_TRACE_IDX]; - // Create the component list and enabled bits for the circuit that verifies the Cairo proof. - // The set of components is constant (all possible components for the given preprocessed trace) - // to keep the verifier circuit stable. The trace is expected to contain all the components - // in this set. - let mut cairo_components: IndexMap<&'static str, Box>> = IndexMap::default(); - let mut enabled_bits = vec![]; - let disabled_components: &[&str] = match cairo_prover_parameters.preprocessed_trace { PreProcessedTraceVariant::Canonical => &DISABLED_COMPONENTS_CANONICAL_PREPROCESSED, PreProcessedTraceVariant::CanonicalSmall => &DISABLED_COMPONENTS_SMALL_PREPROCESSED, @@ -106,14 +99,8 @@ pub fn prove_leaf( cairo_prover_parameters.preprocessed_trace ), }; - for (name, component) in all_components::() { - if disabled_components.contains(&name) { - enabled_bits.push(false); - } else { - cairo_components.insert(name, component); - enabled_bits.push(true); - } - } + let LeafVerifierComponents { components: cairo_components, enabled_bits } = + leaf_verifier_components(disabled_components); // Set min_lifting_log_size from the cairo proof. let mut pcs_config = cairo_prover_parameters.pcs_config; @@ -204,6 +191,32 @@ pub fn prove_leaf( SerializedLeafProof { circuit_preprocessed_root, proof: proof_bytes } } +/// The components of the circuit that verifies the Cairo proof. +pub struct LeafVerifierComponents { + /// Map from component name to the circuit evaluator that verifies it. + pub components: IndexMap<&'static str, Box>>, + /// One bit per possible component: `true` if the component is enabled (present). + pub enabled_bits: Vec, +} + +/// Creates the component list and enabled bits for the circuit that verifies the Cairo proof. +/// The set of components is constant (all possible components for the given preprocessed trace, +/// minus `disabled_components`) to keep the verifier circuit stable. The trace is expected to +/// contain all the components in this set. +pub fn leaf_verifier_components(disabled_components: &[&str]) -> LeafVerifierComponents { + let mut components: IndexMap<&'static str, Box>> = IndexMap::default(); + let mut enabled_bits = vec![]; + for (name, component) in all_components::() { + if disabled_components.contains(&name) { + enabled_bits.push(false); + } else { + components.insert(name, component); + enabled_bits.push(true); + } + } + LeafVerifierComponents { components, enabled_bits } +} + fn program_felts(program: &Program) -> Vec<[M31; MEMORY_VALUES_LIMBS]> { let mut program_felts = vec![]; for value in program.iter_data() {