From 52e86fa4f446e86d39b74c63fdb704001914b4fe Mon Sep 17 00:00:00 2001 From: Ilya Lesokhin Date: Wed, 22 Jul 2026 16:10:11 +0300 Subject: [PATCH] feat(leaf_prover): pad and validate the leaf circuit against the registry Pad the leaf verifier circuit to a target read from a circuit registry rather than a hardcoded constant, and validate the result against the registry. `prove_leaf` takes a `&CircuitRegistry`: it looks up the leaf verifier for the verified Cairo proof's (trace_log_size, log_blowup_factor), pads the verifier circuit to that entry's config component sizes, and after proving asserts the circuit's preprocessed root equals the entry's root. A leaf not listed in the registry (or whose root differs) is rejected. The `leaf-prover` binary gains a required `--circuit-registry-json` argument. Adds `CircuitRegistry::leaf_verifier` / `config_pad_target`, the `LogSizes -> ComponentSizes` conversion and `RootHex::to_le_bytes` to `circuit-registry`, plus a fast fixture test; the slow e2e `cli_test` passes a registry. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 1 + crates/circuit_registry/src/schema.rs | 42 +++++++++++++++++++ crates/leaf_prover/Cargo.toml | 1 + crates/leaf_prover/src/main.rs | 23 +++++++++- crates/leaf_prover/src/prove_leaf.rs | 30 ++++++++++++- crates/leaf_prover/tests/cli_test.rs | 2 + crates/leaf_prover/tests/data/registry.json | 32 ++++++++++++++ .../tests/registry_fixture_test.rs | 38 +++++++++++++++++ 8 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 crates/leaf_prover/tests/data/registry.json create mode 100644 crates/leaf_prover/tests/registry_fixture_test.rs diff --git a/Cargo.lock b/Cargo.lock index 0023750a..ed701deb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2814,6 +2814,7 @@ dependencies = [ "circuit-cairo-verifier", "circuit-common", "circuit-prover", + "circuit-registry", "circuit-serialize", "circuits-stark-verifier", "clap", diff --git a/crates/circuit_registry/src/schema.rs b/crates/circuit_registry/src/schema.rs index 2576f7f5..2cad905e 100644 --- a/crates/circuit_registry/src/schema.rs +++ b/crates/circuit_registry/src/schema.rs @@ -28,6 +28,18 @@ impl From<&ComponentSizes> for LogSizes { } } +impl From<&LogSizes> for ComponentSizes { + fn from(log: &LogSizes) -> Self { + ComponentSizes { + eq: 1 << log.eq, + qm31_ops: 1 << log.qm31_ops, + m31_to_u32: 1 << log.m31_to_u32, + triple_xor: 1 << log.triple_xor, + blake_g_gate: 1 << log.blake_g_gate, + } + } +} + fn log_size(size: usize) -> u32 { size.next_power_of_two().ilog2() } @@ -54,6 +66,17 @@ impl<'de> Deserialize<'de> for RootHex { } } +impl RootHex { + /// The root as 32 little-endian bytes, matching a proof's `circuit_preprocessed_root`. + pub fn to_le_bytes(&self) -> [u8; 32] { + let mut bytes = [0u8; 32]; + for (word, chunk) in self.0.iter().zip(bytes.chunks_exact_mut(4)) { + chunk.copy_from_slice(&word.to_le_bytes()); + } + bytes + } +} + /// 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)] @@ -94,3 +117,22 @@ pub struct CircuitRegistry { pub leaf_verifiers: Vec, pub multiverifiers: Vec, } + +impl CircuitRegistry { + /// The supported leaf verifier for a Cairo proof with the given trace log size and log blowup + /// factor, or `None` if the registry doesn't list it. + pub fn leaf_verifier( + &self, + trace_log_size: u32, + log_blowup_factor: u32, + ) -> Option<&LeafVerifier> { + self.leaf_verifiers.iter().find(|leaf| { + leaf.trace_log_size == trace_log_size && leaf.log_blowup_factor == log_blowup_factor + }) + } + + /// The component-size pad target of the given config, or `None` if the config is unknown. + pub fn config_pad_target(&self, config: &str) -> Option { + self.circuit_configs.get(config).map(|config| (&config.component_log_sizes).into()) + } +} diff --git a/crates/leaf_prover/Cargo.toml b/crates/leaf_prover/Cargo.toml index 9e945457..b8460e49 100644 --- a/crates/leaf_prover/Cargo.toml +++ b/crates/leaf_prover/Cargo.toml @@ -18,6 +18,7 @@ sonic-rs.workspace = true tracing.workspace = true indexmap = { version = "2.10.0", default-features = false, features = ["std"] } +circuit-registry.workspace = true leaf-proof-format.workspace = true circuit-cairo-verifier.workspace = true diff --git a/crates/leaf_prover/src/main.rs b/crates/leaf_prover/src/main.rs index 38ee7e97..27f311d5 100644 --- a/crates/leaf_prover/src/main.rs +++ b/crates/leaf_prover/src/main.rs @@ -14,6 +14,7 @@ use std::path::PathBuf; use std::process::ExitCode; use cairo_program_runner_lib::utils::{get_program, get_program_input_from_path}; +use circuit_registry::CircuitRegistry; use clap::Parser; use leaf_prover::prove_leaf::prove_leaf; use stwo_cairo_utils::binary_utils::run_binary; @@ -28,6 +29,12 @@ struct Args { cairo_prover_params_json: PathBuf, #[clap(long, help = "JSON file containing the circuit prover parameters.")] circuit_prover_params_json: PathBuf, + #[clap( + long, + help = "JSON file containing the circuit registry; the leaf circuit is padded to its \ + component-size target." + )] + circuit_registry_json: PathBuf, #[clap(long, help = "Path to write the output file")] output_path: PathBuf, } @@ -65,8 +72,20 @@ fn run() -> Result<(), String> { }); let circuit_prover_pcs_config = sonic_rs::from_str(&circuit_prover_pcs_config).unwrap(); - let output = - prove_leaf(&program, program_input, cairo_prover_parameters, circuit_prover_pcs_config); + let registry = read_to_string(&args.circuit_registry_json).unwrap_or_else(|err| { + panic!("Cannot get circuit registry from {}: {err}", args.circuit_registry_json.display()) + }); + let registry: CircuitRegistry = serde_json::from_str(®istry).unwrap_or_else(|err| { + panic!("Cannot parse circuit registry from {}: {err}", args.circuit_registry_json.display()) + }); + + let output = prove_leaf( + &program, + program_input, + cairo_prover_parameters, + circuit_prover_pcs_config, + ®istry, + ); fs::write(&args.output_path, serde_json::to_string_pretty(&output).unwrap()).unwrap_or_else( |err| panic!("Cannot write output to {}: {err}", args.output_path.display()), diff --git a/crates/leaf_prover/src/prove_leaf.rs b/crates/leaf_prover/src/prove_leaf.rs index 34e263ce..6747a046 100644 --- a/crates/leaf_prover/src/prove_leaf.rs +++ b/crates/leaf_prover/src/prove_leaf.rs @@ -11,10 +11,12 @@ use circuit_cairo_verifier::verify::{ CairoVerifierConfig, build_and_fill_cairo_verifier_circuit, prepare_cairo_proof_for_circuit_verifier, }; +use circuit_common::finalize::pad_to_targets; use circuit_common::preprocessed::PreprocessedCircuit; use circuit_prover::prover::{ BaseColumnPool, prepare_circuit_proof_for_circuit_verifier, prove_circuit_assignment, }; +use circuit_registry::CircuitRegistry; use circuit_serialize::serialize::CircuitSerialize; use circuits_stark_verifier::constraint_eval::CircuitEval; use circuits_stark_verifier::proof::ProofConfig; @@ -40,6 +42,7 @@ pub fn prove_leaf( program_input: Option, cairo_prover_parameters: ProverParameters, circuit_prover_pcs_config: PcsConfig, + registry: &CircuitRegistry, ) -> SerializedLeafProof { assert!( cairo_prover_parameters.include_all_preprocessed_columns, @@ -119,6 +122,23 @@ pub fn prove_leaf( let mut pcs_config = cairo_prover_parameters.pcs_config; pcs_config.min_lifting_log_size = proof.extended_stark_proof.proof.config.min_lifting_log_size; + // Look up this leaf in the registry by the verified Cairo proof's trace log size and blowup. + // `get_pcs_config` sets `min_lifting_log_size = trace_log_size + log_blowup_factor`, so we + // invert it here. The matched entry gives the component-size target to pad to and the + // preprocessed root the resulting circuit must produce. + let log_blowup_factor = pcs_config.fri_config.log_blowup_factor; + let trace_log_size = pcs_config.min_lifting_log_size - log_blowup_factor; + let leaf_verifier = + registry.leaf_verifier(trace_log_size, log_blowup_factor).unwrap_or_else(|| { + panic!( + "No leaf verifier in the registry for trace_log_size={trace_log_size}, \ + log_blowup_factor={log_blowup_factor}" + ) + }); + let pad_target = registry.config_pad_target(&leaf_verifier.config).unwrap_or_else(|| { + panic!("Registry has no config {:?} for the matched leaf verifier", leaf_verifier.config) + }); + let proof_config = ProofConfig::new( &cairo_components, cairo_prover_parameters.preprocessed_trace.n_columns(), @@ -158,7 +178,7 @@ pub fn prove_leaf( output_hash, ); - // TODO: Pad to multiverifier size. + pad_to_targets(&mut context, pad_target); info!( "Verifier config: @@ -193,6 +213,14 @@ pub fn prove_leaf( circuit_proof.stark_proof.proof.commitments[PREPROCESSED_TRACE_IDX].0; info!("Circuit preprocessed root: {:?}", circuit_preprocessed_root); + // The circuit we built and proved must be the one the registry describes for this leaf. + let expected_root = leaf_verifier.preprocessed_root.to_le_bytes(); + assert!( + circuit_preprocessed_root == expected_root, + "Circuit preprocessed root {circuit_preprocessed_root:?} does not match the registry's \ + leaf verifier root {expected_root:?}" + ); + // Convert the proof to our output format. let (proof_qm31s, _public_data) = prepare_circuit_proof_for_circuit_verifier(circuit_proof); diff --git a/crates/leaf_prover/tests/cli_test.rs b/crates/leaf_prover/tests/cli_test.rs index 0654d581..52711cea 100644 --- a/crates/leaf_prover/tests/cli_test.rs +++ b/crates/leaf_prover/tests/cli_test.rs @@ -24,6 +24,8 @@ fn run_leaf_prover_binary() { .arg(test_data_dir.join("cairo_prover_params_canonical_small.json")) .arg("--circuit-prover-params-json") .arg(test_data_dir.join("circuit_prover_params_canonical_small.json")) + .arg("--circuit-registry-json") + .arg(test_data_dir.join("registry.json")) .arg("--output-path") .arg(&output_path) .status() diff --git a/crates/leaf_prover/tests/data/registry.json b/crates/leaf_prover/tests/data/registry.json new file mode 100644 index 00000000..7be0b4d0 --- /dev/null +++ b/crates/leaf_prover/tests/data/registry.json @@ -0,0 +1,32 @@ +{ + "circuit_configs": { + "default": { + "log_blowup_factor": 1, + "component_log_sizes": { + "eq": 20, + "qm31_ops": 23, + "m31_to_u32": 20, + "triple_xor": 19, + "blake_g_gate": 23 + } + } + }, + "leaf_verifiers": [ + { + "config": "default", + "trace_log_size": 20, + "log_blowup_factor": 1, + "preprocessed_root": [ + "0x6de807ae", + "0x0bf50621", + "0x05cbd9f4", + "0x87b08e05", + "0x12453193", + "0x89e82e30", + "0x25a779de", + "0x7da05a04" + ] + } + ], + "multiverifiers": [] +} diff --git a/crates/leaf_prover/tests/registry_fixture_test.rs b/crates/leaf_prover/tests/registry_fixture_test.rs new file mode 100644 index 00000000..3a88bc97 --- /dev/null +++ b/crates/leaf_prover/tests/registry_fixture_test.rs @@ -0,0 +1,38 @@ +//! Guards the `cli_test` registry fixture: it must list the leaf the slow end-to-end test proves, +//! resolve to the expected pad target, and carry the same preprocessed root as the committed +//! expected output. Fast (no proving), so it runs on every test job unlike `cli_test`. + +use std::fs; +use std::path::PathBuf; + +use circuit_registry::CircuitRegistry; +use leaf_proof_format::SerializedLeafProof; + +#[test] +fn registry_fixture_matches_expected_leaf() { + let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data"); + + let registry: CircuitRegistry = + serde_json::from_str(&fs::read_to_string(data_dir.join("registry.json")).unwrap()) + .expect("Cannot parse registry fixture"); + + // The fixture lists exactly the one leaf the e2e test proves, found by its identity. + assert_eq!(registry.leaf_verifiers.len(), 1); + let leaf = ®istry.leaf_verifiers[0]; + assert!(registry.leaf_verifier(leaf.trace_log_size, leaf.log_blowup_factor).is_some()); + + // Its config resolves to the pad target the leaf prover expects. + let pad = registry.config_pad_target(&leaf.config).expect("leaf verifier config missing"); + assert_eq!(pad.eq, 1 << 20); + assert_eq!(pad.qm31_ops, 1 << 23); + assert_eq!(pad.m31_to_u32, 1 << 20); + assert_eq!(pad.triple_xor, 1 << 19); + assert_eq!(pad.blake_g_gate, 1 << 23); + + // Its preprocessed root equals the root in the committed expected e2e output, so a matching + // leaf passes the prover's root check. + let expected: SerializedLeafProof = + serde_json::from_str(&fs::read_to_string(data_dir.join("expected_output.json")).unwrap()) + .expect("Cannot parse expected output"); + assert_eq!(leaf.preprocessed_root.to_le_bytes(), expected.circuit_preprocessed_root); +}