openvm_sdk/
lib.rs

1use std::{fs::read, marker::PhantomData, path::Path, sync::Arc};
2
3#[cfg(feature = "evm-verify")]
4use alloy_sol_types::sol;
5use commit::commit_app_exe;
6use config::{AggregationTreeConfig, AppConfig};
7use eyre::Result;
8use keygen::{AppProvingKey, AppVerifyingKey};
9use openvm_build::{
10    build_guest_package, find_unique_executable, get_package, GuestOptions, TargetFilter,
11};
12use openvm_circuit::{
13    arch::{
14        hasher::poseidon2::vm_poseidon2_hasher, instructions::exe::VmExe, verify_segments,
15        ContinuationVmProof, ExecutionError, VerifiedExecutionPayload, VmConfig, VmExecutor,
16        VmVerificationError,
17    },
18    system::{
19        memory::{tree::public_values::extract_public_values, CHUNK},
20        program::trace::VmCommittedExe,
21    },
22};
23use openvm_continuations::verifier::root::types::RootVmVerifierInput;
24pub use openvm_continuations::{
25    static_verifier::{DefaultStaticVerifierPvHandler, StaticVerifierPvHandler},
26    RootSC, C, F, SC,
27};
28use openvm_native_recursion::halo2::utils::Halo2ParamsReader;
29use openvm_stark_backend::proof::Proof;
30use openvm_stark_sdk::{
31    config::{baby_bear_poseidon2::BabyBearPoseidon2Engine, FriParameters},
32    engine::StarkFriEngine,
33    openvm_stark_backend::{verifier::VerificationError, Chip},
34};
35use openvm_transpiler::{
36    elf::Elf,
37    openvm_platform::memory::MEM_SIZE,
38    transpiler::{Transpiler, TranspilerError},
39    FromElf,
40};
41#[cfg(feature = "evm-verify")]
42use snark_verifier_sdk::{evm::gen_evm_verifier_sol_code, halo2::aggregation::AggregationCircuit};
43
44use crate::{
45    config::AggConfig,
46    keygen::{AggProvingKey, AggStarkProvingKey},
47    prover::{AppProver, StarkProver},
48};
49#[cfg(feature = "evm-prove")]
50use crate::{prover::EvmHalo2Prover, types::EvmProof};
51
52pub mod codec;
53pub mod commit;
54pub mod config;
55pub mod keygen;
56pub mod prover;
57
58mod stdin;
59pub use stdin::*;
60
61pub mod fs;
62pub mod types;
63
64pub type NonRootCommittedExe = VmCommittedExe<SC>;
65
66pub const EVM_HALO2_VERIFIER_INTERFACE: &str =
67    include_str!("../contracts/src/IOpenVmHalo2Verifier.sol");
68pub const EVM_HALO2_VERIFIER_TEMPLATE: &str =
69    include_str!("../contracts/template/OpenVmHalo2Verifier.sol");
70
71#[cfg(feature = "evm-verify")]
72sol! {
73    IOpenVmHalo2Verifier,
74    concat!(env!("CARGO_MANIFEST_DIR"), "/contracts/abi/IOpenVmHalo2Verifier.json"),
75}
76
77/// The payload of a verified guest VM execution with user public values extracted and
78/// verified.
79pub struct VerifiedContinuationVmPayload {
80    /// The Merklelized hash of:
81    /// - Program code commitment (commitment of the cached trace)
82    /// - Merkle root of the initial memory
83    /// - Starting program counter (`pc_start`)
84    ///
85    /// The Merklelization uses Poseidon2 as a cryptographic hash function (for the leaves)
86    /// and a cryptographic compression function (for internal nodes).
87    pub exe_commit: [F; CHUNK],
88    pub user_public_values: Vec<F>,
89}
90
91pub struct GenericSdk<E: StarkFriEngine<SC>> {
92    agg_tree_config: AggregationTreeConfig,
93    _phantom: PhantomData<E>,
94}
95
96impl<E: StarkFriEngine<SC>> Default for GenericSdk<E> {
97    fn default() -> Self {
98        Self {
99            agg_tree_config: AggregationTreeConfig::default(),
100            _phantom: PhantomData,
101        }
102    }
103}
104
105pub type Sdk = GenericSdk<BabyBearPoseidon2Engine>;
106
107impl<E: StarkFriEngine<SC>> GenericSdk<E> {
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    pub fn agg_tree_config(&self) -> &AggregationTreeConfig {
113        &self.agg_tree_config
114    }
115
116    pub fn set_agg_tree_config(&mut self, agg_tree_config: AggregationTreeConfig) {
117        self.agg_tree_config = agg_tree_config;
118    }
119
120    pub fn build<P: AsRef<Path>>(
121        &self,
122        guest_opts: GuestOptions,
123        pkg_dir: P,
124        target_filter: &Option<TargetFilter>,
125    ) -> Result<Elf> {
126        let pkg = get_package(pkg_dir.as_ref());
127        let target_dir = match build_guest_package(&pkg, &guest_opts, None, target_filter) {
128            Ok(target_dir) => target_dir,
129            Err(Some(code)) => {
130                return Err(eyre::eyre!("Failed to build guest: code = {}", code));
131            }
132            Err(None) => {
133                return Err(eyre::eyre!(
134                    "Failed to build guest (OPENVM_SKIP_BUILD is set)"
135                ));
136            }
137        };
138
139        let elf_path = find_unique_executable(pkg_dir, target_dir, target_filter)?;
140        let data = read(&elf_path)?;
141        Elf::decode(&data, MEM_SIZE as u32)
142    }
143
144    pub fn transpile(
145        &self,
146        elf: Elf,
147        transpiler: Transpiler<F>,
148    ) -> Result<VmExe<F>, TranspilerError> {
149        VmExe::from_elf(elf, transpiler)
150    }
151
152    pub fn execute<VC: VmConfig<F>>(
153        &self,
154        exe: VmExe<F>,
155        vm_config: VC,
156        inputs: StdIn,
157    ) -> Result<Vec<F>, ExecutionError>
158    where
159        VC::Executor: Chip<SC>,
160        VC::Periphery: Chip<SC>,
161    {
162        let vm = VmExecutor::new(vm_config);
163        let final_memory = vm.execute(exe, inputs)?;
164        let public_values = extract_public_values(
165            &vm.config.system().memory_config.memory_dimensions(),
166            vm.config.system().num_public_values,
167            final_memory.as_ref().unwrap(),
168        );
169        Ok(public_values)
170    }
171
172    pub fn commit_app_exe(
173        &self,
174        app_fri_params: FriParameters,
175        exe: VmExe<F>,
176    ) -> Result<Arc<NonRootCommittedExe>> {
177        let committed_exe = commit_app_exe(app_fri_params, exe);
178        Ok(committed_exe)
179    }
180
181    pub fn app_keygen<VC: VmConfig<F>>(&self, config: AppConfig<VC>) -> Result<AppProvingKey<VC>>
182    where
183        VC::Executor: Chip<SC>,
184        VC::Periphery: Chip<SC>,
185    {
186        let app_pk = AppProvingKey::keygen(config);
187        Ok(app_pk)
188    }
189
190    pub fn generate_app_proof<VC: VmConfig<F>>(
191        &self,
192        app_pk: Arc<AppProvingKey<VC>>,
193        app_committed_exe: Arc<NonRootCommittedExe>,
194        inputs: StdIn,
195    ) -> Result<ContinuationVmProof<SC>>
196    where
197        VC::Executor: Chip<SC>,
198        VC::Periphery: Chip<SC>,
199    {
200        let app_prover = AppProver::<VC, E>::new(app_pk.app_vm_pk.clone(), app_committed_exe);
201        let proof = app_prover.generate_app_proof(inputs);
202        Ok(proof)
203    }
204
205    /// Verifies the [ContinuationVmProof], which is a collection of STARK proofs as well as
206    /// additional Merkle proof for user public values.
207    ///
208    /// This function verifies the STARK proofs and additional conditions to ensure that the
209    /// `proof` is a valid proof of guest VM execution that terminates successfully (exit code 0)
210    /// _with respect to_ a commitment to some VM executable.
211    /// It is the responsibility of the caller to check that the commitment matches the expected
212    /// VM executable.
213    pub fn verify_app_proof(
214        &self,
215        app_vk: &AppVerifyingKey,
216        proof: &ContinuationVmProof<SC>,
217    ) -> Result<VerifiedContinuationVmPayload, VmVerificationError> {
218        let engine = E::new(app_vk.fri_params);
219        let VerifiedExecutionPayload {
220            exe_commit,
221            final_memory_root,
222        } = verify_segments(&engine, &app_vk.app_vm_vk, &proof.per_segment)?;
223
224        let hasher = vm_poseidon2_hasher();
225        proof
226            .user_public_values
227            .verify(&hasher, app_vk.memory_dimensions, final_memory_root)?;
228
229        Ok(VerifiedContinuationVmPayload {
230            exe_commit,
231            user_public_values: proof.user_public_values.public_values.clone(),
232        })
233    }
234
235    pub fn verify_app_proof_without_continuations(
236        &self,
237        app_vk: &AppVerifyingKey,
238        proof: &Proof<SC>,
239    ) -> Result<(), VerificationError> {
240        let e = E::new(app_vk.fri_params);
241        e.verify(&app_vk.app_vm_vk, proof)
242    }
243
244    pub fn agg_keygen(
245        &self,
246        config: AggConfig,
247        reader: &impl Halo2ParamsReader,
248        pv_handler: &impl StaticVerifierPvHandler,
249    ) -> Result<AggProvingKey> {
250        let agg_pk = AggProvingKey::keygen(config, reader, pv_handler);
251        Ok(agg_pk)
252    }
253
254    pub fn generate_root_verifier_input<VC: VmConfig<F>>(
255        &self,
256        app_pk: Arc<AppProvingKey<VC>>,
257        app_exe: Arc<NonRootCommittedExe>,
258        agg_stark_pk: AggStarkProvingKey,
259        inputs: StdIn,
260    ) -> Result<RootVmVerifierInput<SC>>
261    where
262        VC::Executor: Chip<SC>,
263        VC::Periphery: Chip<SC>,
264    {
265        let stark_prover =
266            StarkProver::<VC, E>::new(app_pk, app_exe, agg_stark_pk, self.agg_tree_config);
267        let proof = stark_prover.generate_root_verifier_input(inputs);
268        Ok(proof)
269    }
270
271    #[cfg(feature = "evm-prove")]
272    pub fn generate_evm_proof<VC: VmConfig<F>>(
273        &self,
274        reader: &impl Halo2ParamsReader,
275        app_pk: Arc<AppProvingKey<VC>>,
276        app_exe: Arc<NonRootCommittedExe>,
277        agg_pk: AggProvingKey,
278        inputs: StdIn,
279    ) -> Result<EvmProof>
280    where
281        VC::Executor: Chip<SC>,
282        VC::Periphery: Chip<SC>,
283    {
284        let e2e_prover =
285            EvmHalo2Prover::<VC, E>::new(reader, app_pk, app_exe, agg_pk, self.agg_tree_config);
286        let proof = e2e_prover.generate_proof_for_evm(inputs);
287        Ok(proof)
288    }
289
290    #[cfg(feature = "evm-verify")]
291    pub fn generate_halo2_verifier_solidity(
292        &self,
293        reader: &impl Halo2ParamsReader,
294        agg_pk: &AggProvingKey,
295    ) -> Result<types::EvmHalo2Verifier> {
296        use std::{
297            fs::{create_dir_all, write},
298            io::Write,
299            process::{Command, Stdio},
300        };
301
302        use eyre::Context;
303        use forge_fmt::{
304            format, parse, FormatterConfig, IntTypes, MultilineFuncHeaderStyle, NumberUnderscore,
305            QuoteStyle, SingleLineBlockStyle,
306        };
307        use openvm_native_recursion::halo2::wrapper::EvmVerifierByteCode;
308        use serde_json::{json, Value};
309        use snark_verifier::halo2_base::halo2_proofs::poly::commitment::Params;
310        use snark_verifier_sdk::SHPLONK;
311        use tempfile::tempdir;
312        use types::EvmHalo2Verifier;
313
314        use crate::fs::{
315            EVM_HALO2_VERIFIER_BASE_NAME, EVM_HALO2_VERIFIER_INTERFACE_NAME,
316            EVM_HALO2_VERIFIER_PARENT_NAME,
317        };
318
319        let params = reader.read_params(agg_pk.halo2_pk.wrapper.pinning.metadata.config_params.k);
320        let pinning = &agg_pk.halo2_pk.wrapper.pinning;
321
322        assert_eq!(
323            pinning.metadata.config_params.k as u32,
324            params.k(),
325            "Provided params don't match circuit config"
326        );
327
328        let halo2_verifier_code = gen_evm_verifier_sol_code::<AggregationCircuit, SHPLONK>(
329            &params,
330            pinning.pk.get_vk(),
331            pinning.metadata.num_pvs.clone(),
332        );
333
334        let wrapper_pvs = agg_pk.halo2_pk.wrapper.pinning.metadata.num_pvs.clone();
335        let pvs_length = match wrapper_pvs.first() {
336            // We subtract 14 to exclude the KZG accumulator and the app exe
337            // and vm commits.
338            Some(v) => v
339                .checked_sub(14)
340                .expect("Unexpected number of static verifier wrapper public values"),
341            _ => panic!("Unexpected amount of instance columns in the static verifier wrapper"),
342        };
343
344        assert!(
345            pvs_length <= 8192,
346            "OpenVM Halo2 verifier contract does not support more than 8192 public values"
347        );
348
349        let openvm_version = env!("CARGO_PKG_VERSION");
350
351        // Fill out the public values length and OpenVM version in the template
352        let openvm_verifier_code = EVM_HALO2_VERIFIER_TEMPLATE
353            .replace("{PUBLIC_VALUES_LENGTH}", &pvs_length.to_string())
354            .replace("{OPENVM_VERSION}", openvm_version);
355
356        let formatter_config = FormatterConfig {
357            line_length: 120,
358            tab_width: 4,
359            bracket_spacing: true,
360            int_types: IntTypes::Long,
361            multiline_func_header: MultilineFuncHeaderStyle::AttributesFirst,
362            quote_style: QuoteStyle::Double,
363            number_underscore: NumberUnderscore::Thousands,
364            single_line_statement_blocks: SingleLineBlockStyle::Preserve,
365            override_spacing: false,
366            wrap_comments: false,
367            ignore: vec![],
368            contract_new_lines: false,
369        };
370
371        let parsed_interface =
372            parse(EVM_HALO2_VERIFIER_INTERFACE).expect("Failed to parse interface");
373        let parsed_halo2_verifier_code =
374            parse(&halo2_verifier_code).expect("Failed to parse halo2 verifier code");
375        let parsed_openvm_verifier_code =
376            parse(&openvm_verifier_code).expect("Failed to parse openvm verifier code");
377
378        let mut formatted_interface = String::new();
379        let mut formatted_halo2_verifier_code = String::new();
380        let mut formatted_openvm_verifier_code = String::new();
381
382        format(
383            &mut formatted_interface,
384            parsed_interface,
385            formatter_config.clone(),
386        )
387        .expect("Failed to format interface");
388        format(
389            &mut formatted_halo2_verifier_code,
390            parsed_halo2_verifier_code,
391            formatter_config.clone(),
392        )
393        .expect("Failed to format halo2 verifier code");
394        format(
395            &mut formatted_openvm_verifier_code,
396            parsed_openvm_verifier_code,
397            formatter_config,
398        )
399        .expect("Failed to format openvm verifier code");
400
401        // Create temp dir
402        let temp_dir = tempdir().wrap_err("Failed to create temp dir")?;
403        let temp_path = temp_dir.path();
404        let root_path = Path::new("src").join(format!("v{}", openvm_version));
405
406        // Make interfaces dir
407        let interfaces_path = root_path.join("interfaces");
408
409        // This will also create the dir for root_path, so no need to explicitly
410        // create it
411        create_dir_all(temp_path.join(&interfaces_path))?;
412
413        let interface_file_path = interfaces_path.join(EVM_HALO2_VERIFIER_INTERFACE_NAME);
414        let parent_file_path = root_path.join(EVM_HALO2_VERIFIER_PARENT_NAME);
415        let base_file_path = root_path.join(EVM_HALO2_VERIFIER_BASE_NAME);
416
417        // Write the files to the temp dir. This is only for compilation
418        // purposes.
419        write(temp_path.join(&interface_file_path), &formatted_interface)?;
420        write(
421            temp_path.join(&parent_file_path),
422            &formatted_halo2_verifier_code,
423        )?;
424        write(
425            temp_path.join(&base_file_path),
426            &formatted_openvm_verifier_code,
427        )?;
428
429        // Run solc from the temp dir
430        let solc_input = json!({
431            "language": "Solidity",
432            "sources": {
433                interface_file_path.to_str().unwrap(): {
434                    "content": formatted_interface
435                },
436                parent_file_path.to_str().unwrap(): {
437                    "content": formatted_halo2_verifier_code
438                },
439                base_file_path.to_str().unwrap(): {
440                    "content": formatted_openvm_verifier_code
441                }
442            },
443            "settings": {
444                "remappings": ["forge-std/=lib/forge-std/src/"],
445                "optimizer": {
446                    "enabled": true,
447                    "runs": 100000,
448                    "details": {
449                        "constantOptimizer": false,
450                        "yul": false
451                    }
452                },
453                "evmVersion": "paris",
454                "viaIR": false,
455                "outputSelection": {
456                    "*": {
457                        "*": ["metadata", "evm.bytecode.object"]
458                    }
459                }
460            }
461        });
462
463        let mut child = Command::new("solc")
464            .current_dir(temp_path)
465            .arg("--standard-json")
466            .stdin(Stdio::piped())
467            .stdout(Stdio::piped())
468            .stderr(Stdio::piped())
469            .spawn()
470            .expect("Failed to spawn solc");
471
472        child
473            .stdin
474            .as_mut()
475            .expect("Failed to open stdin")
476            .write_all(solc_input.to_string().as_bytes())
477            .expect("Failed to write to stdin");
478
479        let output = child.wait_with_output().expect("Failed to read output");
480
481        if !output.status.success() {
482            eyre::bail!(
483                "solc exited with status {}: {}",
484                output.status,
485                String::from_utf8_lossy(&output.stderr)
486            );
487        }
488
489        let parsed: Value = serde_json::from_slice(&output.stdout)?;
490
491        let bytecode = parsed
492            .get("contracts")
493            .expect("No 'contracts' field found")
494            .get(format!("src/v{}/OpenVmHalo2Verifier.sol", openvm_version))
495            .unwrap_or_else(|| {
496                panic!(
497                    "No 'src/v{}/OpenVmHalo2Verifier.sol' field found",
498                    openvm_version
499                )
500            })
501            .get("OpenVmHalo2Verifier")
502            .expect("No 'OpenVmHalo2Verifier' field found")
503            .get("evm")
504            .expect("No 'evm' field found")
505            .get("bytecode")
506            .expect("No 'bytecode' field found")
507            .get("object")
508            .expect("No 'object' field found")
509            .as_str()
510            .expect("No 'object' field found");
511
512        let bytecode = hex::decode(bytecode).expect("Invalid hex in Binary");
513
514        let evm_verifier = EvmHalo2Verifier {
515            halo2_verifier_code: formatted_halo2_verifier_code,
516            openvm_verifier_code: formatted_openvm_verifier_code,
517            openvm_verifier_interface: formatted_interface,
518            artifact: EvmVerifierByteCode {
519                sol_compiler_version: "0.8.19".to_string(),
520                sol_compiler_options: solc_input.get("settings").unwrap().to_string(),
521                bytecode,
522            },
523        };
524        Ok(evm_verifier)
525    }
526
527    #[cfg(feature = "evm-verify")]
528    /// Uses the `verify(..)` interface of the `OpenVmHalo2Verifier` contract.
529    pub fn verify_evm_halo2_proof(
530        &self,
531        openvm_verifier: &types::EvmHalo2Verifier,
532        evm_proof: EvmProof,
533    ) -> Result<u64> {
534        let calldata = evm_proof.verifier_calldata();
535        let deployment_code = openvm_verifier.artifact.bytecode.clone();
536
537        let gas_cost = snark_verifier::loader::evm::deploy_and_call(deployment_code, calldata)
538            .map_err(|reason| eyre::eyre!("Sdk::verify_openvm_evm_proof: {reason:?}"))?;
539
540        Ok(gas_cost)
541    }
542}