diff --git a/Cargo.lock b/Cargo.lock index d00c00db..0023750a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1509,12 +1509,14 @@ dependencies = [ "circuit-cairo-verifier", "circuit-common", "circuit-multiverifier", + "circuit-registry", "circuit-verifier", "circuits", "circuits-stark-verifier", "clap", "indexmap 2.14.0", "leaf-prover", + "serde_json", "stwo-cairo-common", "stwo-cairo-prover", "stwo-cairo-utils", @@ -1542,6 +1544,14 @@ dependencies = [ "stwo-constraint-framework", ] +[[package]] +name = "circuit-registry" +version = "1.2.2" +dependencies = [ + "circuit-common", + "serde", +] + [[package]] name = "circuit-serialize" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9cc0a81e..78e94d7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/vm_runner", "crates/privacy_prove", "crates/privacy_circuit_verify", + "crates/circuit_registry", "crates/circuit_params", ] resolver = "2" @@ -85,6 +86,7 @@ circuits = { git = "https://github.com/starkware-libs/stwo-circuits", rev = "7bb # local crates cairo-program-runner-lib = { path = "crates/cairo-program-runner-lib", version = "1.0.0" } +circuit-registry = { path = "crates/circuit_registry" } 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" } diff --git a/crates/circuit_params/Cargo.toml b/crates/circuit_params/Cargo.toml index b287aab1..31fe093a 100644 --- a/crates/circuit_params/Cargo.toml +++ b/crates/circuit_params/Cargo.toml @@ -5,10 +5,16 @@ edition.workspace = true license.workspace = true description = "Compute the leaf-prover verifier circuit's per-component sizes for a range of trace sizes" +[features] +# Gates the json CLI test, which builds and Merkle-commits a large preprocessed trace. +slow-tests = [] + [dependencies] clap.workspace = true indexmap = { version = "2.10.0", default-features = false, features = ["std"] } +serde_json.workspace = true +circuit-registry.workspace = true leaf-prover.workspace = true cairo-air.workspace = true diff --git a/crates/circuit_params/README.md b/crates/circuit_params/README.md new file mode 100644 index 00000000..2990831c --- /dev/null +++ b/crates/circuit_params/README.md @@ -0,0 +1,58 @@ +# circuit-params + +Computes the per-component sizes of the leaf-prover verifier circuit for a range of verified trace +sizes, using the CANONICAL preprocessed trace config. It reports two circuits: + +- the **leaf verifier** circuit, which verifies one Cairo proof (reported for every trace size), and +- the **multiverifier** circuit, which verifies two proofs of the leaf verifier circuit (reported + once, for the largest trace size). + +Entry point: `crates/circuit_params/src/main.rs` + +## Build & run + + cargo run -p circuit-params -- --help + +## Usage + +Required flags: + +- `--min_trace_log_size `: 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`. +- `--max_trace_log_size `: largest verified trace log size to measure (inclusive). + +Optional: + +- `--log_blowup_factor `: log blowup factor of the verified Cairo proof (1, 2, or 3, default 1). +- `--registry`: output a JSON circuit registry (see below). If omitted, prints the human-readable + report instead. +- `--output_path `: file to write the output to. Prints to stdout if omitted. + +Example: + + cargo run -p circuit-params -- \ + --min_trace_log_size 25 \ + --max_trace_log_size 25 \ + --registry \ + --output_path /abs/path/to/params.json + +## Output formats + +### Default (human-readable) + +One line per circuit and trace size, giving each AIR component's padded log size and its usage +percentage (how much of the padded power-of-two component is actually used). + +Can be used to choose circuit configurations and to find components whose size can be reduced. + +### `--registry` (JSON) + +A JSON circuit registry, with three top-level fields: + +- `circuit_configs`: a map from a config id (a string) to a config — its `log_blowup_factor` + and padded `component_log_sizes`. Circuits sharing a config produce proofs a common AIR can verify. +- `leaf_verifiers`: the leaf verifier circuits, each referencing its `config` (by id), its + `trace_log_size`, the verified proof's `log_blowup_factor`, and its `preprocessed_root`. +- `multiverifiers`: the multiverifier circuits, each referencing its own `config` (by id), the + `input_configs` (by id) of the circuits whose proofs it verifies, and its `preprocessed_root`. diff --git a/crates/circuit_params/src/main.rs b/crates/circuit_params/src/main.rs index f2578dc6..2682cb01 100644 --- a/crates/circuit_params/src/main.rs +++ b/crates/circuit_params/src/main.rs @@ -1,8 +1,17 @@ //! Computes per-component sizes for the CANONICAL preprocessed trace config, across a range of -//! verified trace sizes, and writes them as text. For each trace size it reports two circuits: -//! the leaf-prover verifier circuit (which verifies one Cairo proof), and the multiverifier circuit -//! (which verifies two proofs of that leaf verifier circuit). +//! verified trace sizes. +//! +//! By default, writes a human-readable report of two circuits per run: the leaf-prover verifier +//! circuit (which verifies one Cairo proof), reported for every trace size, and the multiverifier +//! circuit (which verifies two proofs of that leaf verifier circuit), reported once for the largest +//! trace size. Each line gives every component's padded log size and usage. +//! +//! With `--registry`, instead writes a JSON circuit registry: it computes +//! shared target component sizes (the elementwise max of the leaf and multiverifier circuits at the +//! largest trace size), then the preprocessed root of the leaf circuit for each trace size and of +//! the multiverifier circuit once (for the largest trace size), all padded to those targets. +use std::collections::BTreeMap; use std::path::PathBuf; use std::process::ExitCode; use std::sync::Arc; @@ -12,11 +21,14 @@ 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::N_RESERVED; -use circuit_common::finalize::{ComponentSizes, compute_padded_sizes, qm31_ops_n_rows}; +use circuit_common::finalize::{ + ComponentSizes, compute_padded_sizes, pad_to_targets, qm31_ops_n_rows, +}; use circuit_common::preprocessed::PreprocessedCircuit; use circuit_multiverifier::verify::{ MultiverifierInput, SharedConfig, build_multiverifier_circuit, }; +use circuit_registry::{CircuitConfig, CircuitRegistry, LeafVerifier, Multiverifier, RootHex}; use circuit_verifier::statement::{ INTERACTION_POW_BITS as CIRCUIT_INTERACTION_POW_BITS, all_circuit_components, circuit_component_log_sizes, @@ -31,24 +43,39 @@ 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_prover::stwo::core::pcs::PcsConfig; +use stwo_cairo_prover::stwo::core::poly::circle::CanonicCoset; +use stwo_cairo_prover::stwo::core::vcs_lifted::blake2_merkle::Blake2sM31MerkleChannel; +use stwo_cairo_prover::stwo::prover::CommitmentTreeProver; +use stwo_cairo_prover::stwo::prover::backend::simd::SimdBackend; +use stwo_cairo_prover::stwo::prover::mempool::BaseColumnPool; +use stwo_cairo_prover::stwo::prover::poly::circle::PolyOps; use stwo_cairo_utils::binary_utils::run_binary; +#[cfg(test)] +mod tests; + #[derive(Parser)] struct Args { /// Path to write the output to. If omitted, prints to stdout. - #[clap(long)] + #[clap(long = "output_path")] 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)] + #[clap(long = "min_trace_log_size")] min_trace_log_size: u32, /// Largest verified trace log size to measure (inclusive). - #[clap(long)] + #[clap(long = "max_trace_log_size")] 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, + /// Output a JSON circuit registry: a circuit-config map, the leaf verifiers (one per trace + /// size) and the multiverifier, each with its preprocessed root. All circuits are padded to + /// the shared target component sizes (the max of the leaf and multiverifier circuits at the + /// largest trace size). If omitted, prints human-readable per-component sizes. + #[clap(long = "registry")] + registry: bool, } /// The fraction (as a percentage) of the padded (power-of-two) component that is actually used. @@ -65,6 +92,36 @@ struct RawSizes { blake_g_gate: usize, } +/// The raw (non-padded) row counts of a circuit's AIR components, mirroring `compute_padded_sizes` +/// before its power-of-two rounding. +fn raw_sizes(context: &FinalizedContext) -> RawSizes { + let circuit = context.circuit(); + let qm31_ops = qm31_ops_n_rows(circuit); + 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(), + } +} + +/// Non-padded row counts and padded sizes of a circuit context's AIR components. +fn component_sizes(context: &FinalizedContext) -> (RawSizes, ComponentSizes) { + (raw_sizes(context), compute_padded_sizes(context)) +} + +/// The elementwise maximum of two components' padded sizes. +fn max_component_sizes(a: &ComponentSizes, b: &ComponentSizes) -> ComponentSizes { + ComponentSizes { + eq: a.eq.max(b.eq), + qm31_ops: a.qm31_ops.max(b.qm31_ops), + m31_to_u32: a.m31_to_u32.max(b.m31_to_u32), + triple_xor: a.triple_xor.max(b.triple_xor), + blake_g_gate: a.blake_g_gate.max(b.blake_g_gate), + } +} + /// 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 { @@ -160,24 +217,6 @@ fn build_multiverifier_context( build_multiverifier_circuit::(empty_input(), empty_input(), &shared_config) } -/// Non-padded row counts and padded sizes of a circuit context's AIR components. -fn component_sizes(context: &FinalizedContext) -> (RawSizes, ComponentSizes) { - let padded = compute_padded_sizes(context); - - // Non-padded row counts, mirroring `compute_padded_sizes` before its power-of-two rounding. - let circuit = context.circuit(); - let qm31_ops = qm31_ops_n_rows(circuit); - 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) -} - /// Builds the leaf verifier circuit for the given verified trace log size and returns its /// component sizes. fn leaf_component_sizes(trace_log_size: u32, log_blowup_factor: u32) -> (RawSizes, ComponentSizes) { @@ -186,11 +225,11 @@ fn leaf_component_sizes(trace_log_size: u32, log_blowup_factor: u32) -> (RawSize } /// Builds the multiverifier circuit that verifies two proofs of the leaf verifier circuit for the -/// given verified trace log size, and returns its component sizes. -fn multiverifier_component_sizes( +/// given verified trace log size. +fn build_multiverifier_context_for_trace( trace_log_size: u32, log_blowup_factor: u32, -) -> (RawSizes, ComponentSizes) { +) -> FinalizedContext { let mut leaf_context = build_leaf_verifier_context(trace_log_size, log_blowup_factor); // The multiverifier verifies proofs of the (preprocessed) leaf circuit, proven at the leaf @@ -198,9 +237,50 @@ fn multiverifier_component_sizes( let preprocessed_leaf = PreprocessedCircuit::preprocess_circuit(&mut leaf_context); let multiverifier_pcs_config = get_pcs_config(preprocessed_leaf.trace_log_size, log_blowup_factor); - let multiverifier_context = - build_multiverifier_context(&preprocessed_leaf, multiverifier_pcs_config); - component_sizes(&multiverifier_context) + build_multiverifier_context(&preprocessed_leaf, multiverifier_pcs_config) +} + +/// The multiverifier circuit's component sizes for the given verified trace log size. +fn multiverifier_component_sizes( + trace_log_size: u32, + log_blowup_factor: u32, +) -> (RawSizes, ComponentSizes) { + component_sizes(&build_multiverifier_context_for_trace(trace_log_size, log_blowup_factor)) +} + +/// Computes the Merkle root of a circuit's preprocessed trace, as eight little-endian Blake2s +/// words. +fn preprocessed_root( + preprocessed_circuit: &PreprocessedCircuit, + circuit_log_blowup_factor: u32, +) -> [u32; 8] { + let min_lifting_log_size = preprocessed_circuit.trace_log_size + circuit_log_blowup_factor; + let preprocessed_trace = preprocessed_circuit.preprocessed_trace.get_trace::(); + let twiddles = SimdBackend::precompute_twiddles( + CanonicCoset::new(min_lifting_log_size).circle_domain().half_coset, + ); + let preprocessed_trace_polys = SimdBackend::interpolate_columns(preprocessed_trace, &twiddles); + let preprocessed_tree = CommitmentTreeProver::::new( + preprocessed_trace_polys, + circuit_log_blowup_factor, + &twiddles, + true, + min_lifting_log_size, + &BaseColumnPool::::new(), + ); + let root_hash = preprocessed_tree.commitment.root(); + std::array::from_fn(|i| u32::from_le_bytes(root_hash.0[i * 4..i * 4 + 4].try_into().unwrap())) +} + +/// Pads `context` to the shared `target_sizes`, preprocesses it, and returns its preprocessed root. +fn padded_preprocessed_root( + mut context: FinalizedContext, + target_sizes: &ComponentSizes, + circuit_log_blowup_factor: u32, +) -> [u32; 8] { + pad_to_targets(&mut context, target_sizes.clone()); + let preprocessed_circuit = PreprocessedCircuit::preprocess_circuit(&mut context); + preprocessed_root(&preprocessed_circuit, circuit_log_blowup_factor) } fn main() -> ExitCode { @@ -210,25 +290,89 @@ fn main() -> ExitCode { fn run() -> Result<(), String> { let args = Args::parse(); - // The leaf verifier circuit's size grows with the verified trace size, so it's reported for - // every trace log size in the range, under a `leaf:` header. The multiverifier verifies proofs - // of the leaf circuit; we only report it for the largest leaf (`max_trace_log_size`), which - // bounds the multiverifier size across the range. - let leaf_lines: Vec = (args.min_trace_log_size..=args.max_trace_log_size) - .map(|trace_log_size| { - let (raw, padded) = leaf_component_sizes(trace_log_size, args.log_blowup_factor); - format!("{}: {}", trace_log_size, format_sizes(&raw, &padded)) - }) - .collect(); - let leaf_section = format!("leaf:\n{}", leaf_lines.join("\n")); - - // We report a single multiverifier line, as the multiverifier is about the same for all trace - // log sizes. - let (mv_raw, mv_padded) = - multiverifier_component_sizes(args.max_trace_log_size, args.log_blowup_factor); - let multiverifier_line = format!("multiverifier:\n{}", format_sizes(&mv_raw, &mv_padded,)); - - let output = format!("{leaf_section}\n\n{multiverifier_line}"); + let output = if args.registry { + // Currently a single log blowup factor is used across the system. + let circuit_log_blowup_factor = args.log_blowup_factor; + + // Target sizes: the elementwise max of the leaf (cairo verifier) and multiverifier + // component sizes at the largest trace size. Padding both circuits to this shared + // target lets a single multiverifier AIR verify executions of the cairo + // verifier and of itself (see `circuit_multiverifier`'s + // `test_padding_is_correct`). + let leaf_sizes = compute_padded_sizes(&build_leaf_verifier_context( + args.max_trace_log_size, + args.log_blowup_factor, + )); + let (_mv_raw, multiverifier_sizes) = + multiverifier_component_sizes(args.max_trace_log_size, args.log_blowup_factor); + let target_sizes = max_component_sizes(&leaf_sizes, &multiverifier_sizes); + + // All circuits are padded to `target_sizes` and proven with + // `circuit_log_blowup_factor`, so they share a single config. + const CONFIG_ID: &str = "default"; + let circuit_configs = BTreeMap::from([( + CONFIG_ID.to_string(), + CircuitConfig { + log_blowup_factor: circuit_log_blowup_factor, + component_log_sizes: (&target_sizes).into(), + }, + )]); + + let leaf_verifiers = (args.min_trace_log_size..=args.max_trace_log_size) + .map(|trace_log_size| { + let context = build_leaf_verifier_context(trace_log_size, args.log_blowup_factor); + LeafVerifier { + config: CONFIG_ID.to_string(), + trace_log_size, + log_blowup_factor: args.log_blowup_factor, + preprocessed_root: RootHex(padded_preprocessed_root( + context, + &target_sizes, + circuit_log_blowup_factor, + )), + } + }) + .collect::>(); + + // The multiverifier is essentially the same across trace sizes, so a single instance + // (for the largest trace size) is reported. It verifies two proofs of the leaf circuit, + // hence `input_configs = [CONFIG_ID, CONFIG_ID]`. + let multiverifiers = vec![Multiverifier { + config: CONFIG_ID.to_string(), + input_configs: [CONFIG_ID.to_string(), CONFIG_ID.to_string()], + preprocessed_root: RootHex(padded_preprocessed_root( + build_multiverifier_context_for_trace( + args.max_trace_log_size, + args.log_blowup_factor, + ), + &target_sizes, + circuit_log_blowup_factor, + )), + }]; + + let registry = CircuitRegistry { circuit_configs, leaf_verifiers, multiverifiers }; + serde_json::to_string_pretty(®istry).map_err(|err| err.to_string())? + } else { + // The leaf verifier circuit's size grows with the verified trace size, so it's reported + // for every trace log size in the range, under a `leaf:` header. The multiverifier + // verifies proofs of the leaf circuit; we only report it for the largest leaf + // (`max_trace_log_size`), which bounds the multiverifier size across the range. + let leaf_lines: Vec = (args.min_trace_log_size..=args.max_trace_log_size) + .map(|trace_log_size| { + let (raw, padded) = leaf_component_sizes(trace_log_size, args.log_blowup_factor); + format!("{}: {}", trace_log_size, format_sizes(&raw, &padded)) + }) + .collect(); + let leaf_section = format!("leaf:\n{}", leaf_lines.join("\n")); + + // We report a single multiverifier line, as the multiverifier is about the same for + // all trace log sizes. + let (mv_raw, mv_padded) = + multiverifier_component_sizes(args.max_trace_log_size, args.log_blowup_factor); + let multiverifier_line = format!("multiverifier:\n{}", format_sizes(&mv_raw, &mv_padded)); + + format!("{leaf_section}\n\n{multiverifier_line}") + }; match args.output_path { Some(path) => std::fs::write(&path, format!("{output}\n")) diff --git a/crates/circuit_params/src/tests.rs b/crates/circuit_params/src/tests.rs new file mode 100644 index 00000000..f4a5f673 --- /dev/null +++ b/crates/circuit_params/src/tests.rs @@ -0,0 +1,41 @@ +use std::collections::BTreeMap; + +use circuit_registry::{ + CircuitConfig, CircuitRegistry, LeafVerifier, LogSizes, Multiverifier, RootHex, +}; + +#[test] +fn json_output_round_trips() { + let registry = CircuitRegistry { + circuit_configs: BTreeMap::from([( + "default".to_string(), + CircuitConfig { + log_blowup_factor: 1, + component_log_sizes: LogSizes { + eq: 10, + qm31_ops: 11, + m31_to_u32: 12, + triple_xor: 13, + blake_g_gate: 14, + }, + }, + )]), + leaf_verifiers: vec![LeafVerifier { + config: "default".to_string(), + trace_log_size: 20, + log_blowup_factor: 1, + preprocessed_root: RootHex([0x0123_4567, 1, 2, 3, 4, 5, 6, 0xffff_ffff]), + }], + multiverifiers: vec![Multiverifier { + config: "default".to_string(), + input_configs: ["default".to_string(), "default".to_string()], + preprocessed_root: RootHex([7, 8, 9, 10, 11, 12, 13, 14]), + }], + }; + + let serialized = serde_json::to_string_pretty(®istry).unwrap(); + let deserialized: CircuitRegistry = serde_json::from_str(&serialized).unwrap(); + let reserialized = serde_json::to_string_pretty(&deserialized).unwrap(); + + assert!(serialized == reserialized); +} diff --git a/crates/circuit_params/tests/cli_test.rs b/crates/circuit_params/tests/cli_test.rs index c2a167f6..b1a045ee 100644 --- a/crates/circuit_params/tests/cli_test.rs +++ b/crates/circuit_params/tests/cli_test.rs @@ -1,13 +1,15 @@ 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() { +/// Runs the `circuit-params` binary for a single trace log size and returns its stdout, asserting +/// success. With `registry`, passes `--registry` (JSON output); otherwise emits the human-readable +/// report. +fn run(registry: bool) -> String { 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"); + let mut args = vec!["--min_trace_log_size", "25", "--max_trace_log_size", "25"]; + if registry { + args.push("--registry"); + } + let output = Command::new(binary).args(&args).output().expect("Cannot run circuit-params"); assert!( output.status.success(), @@ -16,7 +18,12 @@ fn run_circuit_params_binary() { String::from_utf8_lossy(&output.stderr) ); - let stdout = String::from_utf8(output.stdout).expect("stdout is not valid UTF-8"); + String::from_utf8(output.stdout).expect("stdout is not valid UTF-8") +} + +#[test] +fn run_circuit_params_binary_info() { + let stdout = run(false); assert!( stdout.contains("leaf:\n25: eq:(log:") && stdout.contains("multiverifier:\neq:(log:") @@ -24,3 +31,23 @@ fn run_circuit_params_binary() { "unexpected output: {stdout}" ); } + +// Slow: builds and Merkle-commits a ~2^24 preprocessed trace. Gated behind the `slow-tests` +// feature (run with `cargo test --features slow-tests`) so it runs under the coverage job but not +// the fast test job. +#[test] +#[cfg(feature = "slow-tests")] +fn run_circuit_params_binary_json() { + let stdout = run(true); + assert!( + stdout.contains("\"circuit_configs\":") + && stdout.contains("\"leaf_verifiers\":") + && stdout.contains("\"multiverifiers\":") + && stdout.contains("\"input_configs\":") + && stdout.contains("\"trace_log_size\": 25") + && stdout.contains("\"log_blowup_factor\":") + && stdout.contains("\"preprocessed_root\":") + && stdout.contains("\"0x"), + "unexpected output: {stdout}" + ); +} diff --git a/crates/circuit_registry/Cargo.toml b/crates/circuit_registry/Cargo.toml new file mode 100644 index 00000000..29d69886 --- /dev/null +++ b/crates/circuit_registry/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "circuit-registry" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "The circuit registry JSON schema: the verifier circuits the system supports and their preprocessed roots" + +[dependencies] +serde.workspace = true + +circuit-common.workspace = true diff --git a/crates/circuit_registry/src/lib.rs b/crates/circuit_registry/src/lib.rs new file mode 100644 index 00000000..7140b430 --- /dev/null +++ b/crates/circuit_registry/src/lib.rs @@ -0,0 +1,8 @@ +//! The circuit registry: the set of verifier circuits the system supports, identified by their +//! preprocessed roots. This crate defines the registry's JSON schema ([`CircuitRegistry`] and +//! friends), shared by the `circuit-params` tool that emits a registry and by the leaf prover that +//! reads its pad target from one. + +mod schema; + +pub use schema::{CircuitConfig, CircuitRegistry, LeafVerifier, LogSizes, Multiverifier, RootHex}; diff --git a/crates/circuit_registry/src/schema.rs b/crates/circuit_registry/src/schema.rs new file mode 100644 index 00000000..2576f7f5 --- /dev/null +++ b/crates/circuit_registry/src/schema.rs @@ -0,0 +1,96 @@ +//! The JSON schema for the circuit registry: a map of circuit configs, the leaf verifiers (one per +//! trace size), and the multiverifiers, each with its preprocessed root. + +use std::collections::BTreeMap; + +use circuit_common::finalize::ComponentSizes; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// The padded log sizes of the verifier circuit's AIR components. +#[derive(Serialize, Deserialize)] +pub struct LogSizes { + pub eq: u32, + pub qm31_ops: u32, + pub m31_to_u32: u32, + pub triple_xor: u32, + pub blake_g_gate: u32, +} + +impl From<&ComponentSizes> for LogSizes { + fn from(padded: &ComponentSizes) -> Self { + LogSizes { + eq: log_size(padded.eq), + qm31_ops: log_size(padded.qm31_ops), + m31_to_u32: log_size(padded.m31_to_u32), + triple_xor: log_size(padded.triple_xor), + blake_g_gate: log_size(padded.blake_g_gate), + } + } +} + +fn log_size(size: usize) -> u32 { + size.next_power_of_two().ilog2() +} + +/// A preprocessed-trace Merkle root: eight little-endian u32 words, serialized as an array of +/// `0x`-prefixed hex strings. +pub struct RootHex(pub [u32; 8]); + +impl Serialize for RootHex { + fn serialize(&self, serializer: S) -> Result { + self.0.map(|word| format!("{word:#010x}")).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for RootHex { + fn deserialize>(deserializer: D) -> Result { + let words: [String; 8] = Deserialize::deserialize(deserializer)?; + let mut root = [0u32; 8]; + for (out, word) in root.iter_mut().zip(words) { + let hex = word.strip_prefix("0x").unwrap_or(&word); + *out = u32::from_str_radix(hex, 16).map_err(serde::de::Error::custom)?; + } + Ok(RootHex(root)) + } +} + +/// A circuit configuration: the (circuit-prover) log blowup factor and padded component log sizes a +/// circuit is proven with. Circuits sharing a config produce proofs a common AIR can verify. +#[derive(Serialize, Deserialize)] +pub struct CircuitConfig { + pub log_blowup_factor: u32, + pub component_log_sizes: LogSizes, +} + +/// A leaf verifier circuit (verifying one Cairo proof of the given trace size and log blowup +/// factor), padded to its config's component sizes. +#[derive(Serialize, Deserialize)] +pub struct LeafVerifier { + /// Key into `CircuitRegistry::circuit_configs`. + pub config: String, + pub trace_log_size: u32, + /// Log blowup factor of the Cairo proof this leaf verifies. + pub log_blowup_factor: u32, + pub preprocessed_root: RootHex, +} + +/// The multiverifier circuit, padded to its config's component sizes. +#[derive(Serialize, Deserialize)] +pub struct Multiverifier { + /// Key into `CircuitRegistry::circuit_configs`: the multiverifier's own config. + pub config: String, + /// Configs of the two circuits whose proofs the multiverifier verifies. + pub input_configs: [String; 2], + pub preprocessed_root: RootHex, +} + +/// The json output: a map of circuit configs, the leaf verifiers (one per trace size), and the +/// multiverifiers. All circuits are padded to the shared target sizes and proven with the same +/// blowup, so they share a single config; the multiverifier verifies proofs of the leaf circuit and +/// is essentially the same across trace sizes, so a single multiverifier is reported. +#[derive(Serialize, Deserialize)] +pub struct CircuitRegistry { + pub circuit_configs: BTreeMap, + pub leaf_verifiers: Vec, + pub multiverifiers: Vec, +}