openvm_static_verifier/
keygen.rs

1use halo2_base::{
2    gates::circuit::CircuitBuilderStage,
3    halo2_proofs::plonk::{keygen_pk, keygen_vk},
4};
5use openvm_stark_sdk::{
6    config::baby_bear_bn254_poseidon2::BabyBearBn254Poseidon2Config as RootConfig,
7    openvm_stark_backend::proof::Proof,
8};
9#[cfg(feature = "evm-prove")]
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    circuit::StaticVerifierCircuit,
14    config::StaticVerifierShape,
15    prover::{Halo2Params, Halo2ProvingMetadata, Halo2ProvingPinning, StaticVerifierProof},
16    tracegen::graph_executor::GraphProgram,
17};
18
19impl StaticVerifierCircuit {
20    /// Run keygen to produce a [`Halo2ProvingPinning`].
21    ///
22    /// The `representative_proof` is used as a witness for keygen; any valid proof for this static
23    /// circuit shape will do.
24    pub fn keygen(
25        &self,
26        params: &Halo2Params,
27        shape: &StaticVerifierShape,
28        representative_proof: &Proof<RootConfig>,
29    ) -> Halo2ProvingPinning {
30        let mut builder = Self::builder(CircuitBuilderStage::Keygen, shape);
31        self.populate(&mut builder, representative_proof);
32
33        let config_params = builder.calculate_params(Some(shape.minimum_rows));
34
35        let vk = keygen_vk(params, &builder).expect("keygen_vk should succeed");
36        let pk = keygen_pk(params, vk, &builder).expect("keygen_pk should succeed");
37        let break_points = builder.break_points();
38
39        Halo2ProvingPinning {
40            pk,
41            metadata: Halo2ProvingMetadata {
42                config_params,
43                break_points,
44                num_pvs: builder
45                    .assigned_instances
46                    .iter()
47                    .map(|instances| instances.len())
48                    .collect(),
49            },
50        }
51    }
52}
53
54/// High-level proving key that owns a [`StaticVerifierCircuit`], [`Halo2ProvingPinning`], and
55/// [`StaticVerifierShape`].
56#[derive(Clone)]
57pub struct StaticVerifierProvingKey {
58    pub circuit: StaticVerifierCircuit,
59    pub pinning: Halo2ProvingPinning,
60    pub shape: StaticVerifierShape,
61    pub graph_program: GraphProgram,
62}
63
64impl StaticVerifierProvingKey {
65    /// Run keygen and return a proving key that can be reused for multiple proofs.
66    pub fn keygen(
67        params: &Halo2Params,
68        shape: StaticVerifierShape,
69        circuit: StaticVerifierCircuit,
70        representative_proof: &Proof<RootConfig>,
71    ) -> Self {
72        let pinning = circuit.keygen(params, &shape, representative_proof);
73        let graph_program = tracing::info_span!("build_graph_program")
74            .in_scope(|| GraphProgram::new(&circuit, shape.lookup_bits, representative_proof));
75        Self {
76            circuit,
77            pinning,
78            shape,
79            graph_program,
80        }
81    }
82
83    /// Generate a proof using the stored pinning and shape.
84    pub fn prove(&self, params: &Halo2Params, proof: &Proof<RootConfig>) -> StaticVerifierProof {
85        self.circuit
86            .prove(params, &self.pinning, &self.shape, proof)
87    }
88
89    /// Verify a proof against this proving key's verifying key.
90    pub fn verify(&self, params: &Halo2Params, proof: &StaticVerifierProof) -> bool {
91        StaticVerifierCircuit::verify(params, self.pinning.pk.get_vk(), proof)
92    }
93}
94
95// --- EVM support (feature-gated) ---
96
97#[cfg(feature = "evm-prove")]
98use halo2_base::{
99    gates::circuit::builder::BaseCircuitBuilder,
100    halo2_proofs::{halo2curves::bn256::Fr, plonk::AdviceColumns},
101};
102#[cfg(feature = "evm-prove")]
103use snark_verifier_sdk::{
104    evm::{gen_evm_proof_shplonk, gen_evm_verifier_sol_code},
105    SHPLONK,
106};
107
108/// EVM-compatible proof consisting of instances and raw proof bytes.
109#[cfg(feature = "evm-prove")]
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct RawEvmProof {
112    pub instances: Vec<Fr>,
113    pub proof: Vec<u8>,
114}
115
116#[cfg(feature = "evm-prove")]
117impl StaticVerifierProvingKey {
118    /// Generate a Solidity verifier contract for this circuit.
119    pub fn generate_fallback_evm_verifier(&self, params: &Halo2Params) -> String {
120        gen_evm_verifier_sol_code::<BaseCircuitBuilder<Fr>, SHPLONK>(
121            params,
122            self.pinning.pk.get_vk(),
123            self.pinning.metadata.num_pvs.clone(),
124        )
125    }
126
127    /// Produce a [`Snark`](snark_verifier_sdk::Snark) for consumption by the wrapper circuit.
128    ///
129    /// `state` is the caller-owned witness scratch (see
130    /// [`GraphExecutorState`](crate::tracegen::graph_executor::GraphExecutorState));
131    /// reusing it across proofs avoids reallocating the tape and flag buffers.
132    ///
133    /// Unlike [`prove_for_evm_unwrapped`](Self::prove_for_evm_unwrapped), this
134    /// returns a `Snark` (not a raw EVM proof), which should be fed into
135    /// [`Halo2WrapperProvingKey::prove_for_evm`](crate::wrapper::Halo2WrapperProvingKey::prove_for_evm).
136    pub fn prove_wrapped(
137        &self,
138        params: &Halo2Params,
139        proof: &Proof<RootConfig>,
140        state: &mut crate::tracegen::graph_executor::GraphExecutorState,
141    ) -> snark_verifier_sdk::Snark {
142        // Cores − 2 leaves one core for the release-walk callback and one for
143        // the rest of the runtime (GPU driver threads, tokio, etc.).
144        static GRAPH_EXE_THREADS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
145        let num_threads = *GRAPH_EXE_THREADS.get_or_init(|| {
146            std::env::var("GRAPH_EXE_THREADS")
147                .ok()
148                .and_then(|s| s.parse().ok())
149                .filter(|&t: &usize| t > 0)
150                .unwrap_or_else(|| {
151                    std::thread::available_parallelism()
152                        .map(|n| n.get().saturating_sub(2).max(1))
153                        .unwrap_or(1)
154                })
155        });
156
157        let (advice, instances) = self.generate_witness(proof, num_threads, state);
158
159        snark_verifier_sdk::halo2::gen_snark_from_base(params, &self.pinning.pk, advice, instances)
160    }
161
162    /// Runs the graph-executor witness pipeline: binds the stored
163    /// [`GraphProgram`] to `state`, streams the advice/lookup deltas through a
164    /// [`FusedColumnBuilder`](crate::tracegen::graph_executor::FusedColumnBuilder) onto advice
165    /// columns, and returns the [`AdviceColumns<Fr>`] + instance columns ready for
166    /// [`gen_snark_from_base`](snark_verifier_sdk::halo2::gen_snark_from_base).
167    pub fn generate_witness(
168        &self,
169        proof: &Proof<RootConfig>,
170        num_threads: usize,
171        state: &mut crate::tracegen::graph_executor::GraphExecutorState,
172    ) -> (AdviceColumns<Fr>, Vec<Vec<Fr>>) {
173        use halo2_base::{
174            gates::circuit::MaybeRangeConfig,
175            halo2_proofs::plonk::{Circuit, ConstraintSystem},
176        };
177        use tracing::info_span;
178
179        use crate::{
180            stages::full_pipeline::load_proof_wire,
181            tracegen::graph_executor::{FusedColumnBuilder, GraphExecutor},
182        };
183
184        // Pre-derive the physical column layout that the fused closure will fill.
185        let num_advice_columns = self.pinning.pk.get_vk().cs().num_advice_columns();
186        let n = 1usize << self.pinning.metadata.config_params.k;
187        let mut cs = ConstraintSystem::<Fr>::default();
188        let config = <BaseCircuitBuilder<Fr> as Circuit<Fr>>::configure_with_params(
189            &mut cs,
190            self.pinning.metadata.config_params.clone(),
191        );
192        let MaybeRangeConfig::WithRange(range_config) = &config.base else {
193            panic!("static verifier requires lookup advice columns");
194        };
195        let lookup_col_indices: Vec<usize> = range_config.lookup_advice[0]
196            .iter()
197            .map(|c| c.index())
198            .collect();
199        assert!(
200            !lookup_col_indices.is_empty(),
201            "range lookups require lookup advice columns"
202        );
203        let max_lookup_rows = range_config.gate.max_rows;
204        let break_points = self.pinning.metadata.break_points[0].clone();
205        assert!(
206            self.graph_program
207                .lookup_cells()
208                .div_ceil(lookup_col_indices.len())
209                <= max_lookup_rows,
210            "range lookups would be assigned to unusable rows"
211        );
212
213        let mut builder =
214            FusedColumnBuilder::new(n, num_advice_columns, break_points, lookup_col_indices);
215
216        let mut executor = GraphExecutor::new(&self.graph_program, state);
217        executor.state.reset();
218        info_span!("populate_inputs").in_scope(|| {
219            load_proof_wire(&mut executor, proof, &self.circuit.log_heights_per_air);
220        });
221        info_span!("graph_witness_gen", num_threads).in_scope(|| {
222            executor.run(
223                num_threads,
224                |advice_offset, advice_delta, lookup_offset, lookup_delta| {
225                    builder.append(advice_offset, advice_delta, lookup_offset, lookup_delta)
226                },
227            );
228        });
229        let pvs = info_span!("collect_pvs").in_scope(|| {
230            let advice = executor.advice();
231            self.graph_program
232                .pv_offsets()
233                .iter()
234                .map(|&offset| advice[offset])
235                .collect()
236        });
237
238        let mut instances = vec![Vec::new(); self.shape.instance_columns];
239        instances[0] = pvs;
240
241        let advice = builder.take_columns();
242        (advice, instances)
243    }
244
245    /// Generate a dummy snark for wrapper keygen.
246    pub fn generate_dummy_snark(
247        &self,
248        reader: &impl crate::wrapper::Halo2ParamsReader,
249    ) -> snark_verifier_sdk::Snark {
250        let k = self.pinning.metadata.config_params.k;
251        let params = reader.read_params(k);
252        snark_verifier_sdk::halo2::gen_dummy_snark_from_vk::<SHPLONK>(
253            &params,
254            self.pinning.pk.get_vk(),
255            self.pinning.metadata.num_pvs.clone(),
256            None,
257        )
258    }
259
260    /// Generate an EVM-compatible proof directly (one-step, no wrapper circuit).
261    pub fn prove_for_evm_unwrapped(
262        &self,
263        params: &Halo2Params,
264        proof: &Proof<RootConfig>,
265    ) -> RawEvmProof {
266        self.shape.assert_onchain_verifier_supported();
267
268        let mut builder = BaseCircuitBuilder::prover(
269            self.pinning.metadata.config_params.clone(),
270            self.pinning.metadata.break_points.clone(),
271        )
272        .use_instance_columns(self.shape.instance_columns);
273
274        let public_inputs = self.circuit.populate(&mut builder, proof);
275        let instances_vec = public_inputs.to_vec();
276
277        let snark = gen_evm_proof_shplonk(
278            params,
279            &self.pinning.pk,
280            builder,
281            vec![instances_vec.clone()],
282        );
283
284        RawEvmProof {
285            instances: instances_vec,
286            proof: snark,
287        }
288    }
289}
290
291/// Verify an EVM proof using a deployed verifier contract.
292///
293/// Returns the gas used on success, or an error message on failure.
294#[cfg(feature = "evm-verify")]
295pub fn evm_verify(deployment_code: &[u8], proof: &RawEvmProof) -> Result<u64, String> {
296    snark_verifier_sdk::evm::evm_verify(
297        deployment_code.to_vec(),
298        vec![proof.instances.clone()],
299        proof.proof.clone(),
300    )
301    .map_err(|e| format!("EVM verification failed: {e}"))
302}