Skip to content

Using the SDK

While the CLI provides a convenient way to build, prove, and verify programs, you may want more fine-grained control over the process. The OpenVM Rust SDK allows you to customize various aspects of the workflow programmatically.

For more information on the basic CLI flow, see Overview of Basic Usage. Writing a guest program is the same as in the CLI.

Imports and Setup

If you have a guest program and would like to try running the host program specified in the next section, you can do so by adding the following imports and setup at the top of the file. You may need to modify the imports and/or the SomeStruct struct to match your program.

use std::fs;
 
use openvm_build::GuestOptions;
use openvm_sdk::{config::AggregationSystemParams, Sdk, StdIn};
use openvm_stark_sdk::config::{app_params_with_100_bits_security, MAX_APP_LOG_STACKED_HEIGHT};
use serde::{Deserialize, Serialize};
 
#[derive(Serialize, Deserialize)]
pub struct SomeStruct {
    pub a: u64,
    pub b: u64,
}

Configuring the VM

Preset Configurations

The following convenience methods are available to construct a GenericSdk object with a preset config:

  • Sdk::riscv32(app_params, agg_params) returns a GenericSdk supporting the RV32IM extension.
  • Sdk::standard(app_params, agg_params) returns a GenericSdk supporting all default OpenVM extensions, including RV32IM.

Note that to use Sdk::riscv32(app_params, agg_params) or Sdk::standard(app_params, agg_params) the app_vm_config field of your openvm.toml must exactly match a specific set configuration.

Sdk::riscv32(app_params, agg_params) supports normal Rust compiled to the rv32im target (for more information see OpenVM Rust Frontend) and openvm::io functions. To use it, the app_vm_config field of your openvm.toml must be:

openvm_riscv32.toml
[app_vm_config.rv32i]
[app_vm_config.rv32m]
[app_vm_config.io]

Sdk::standard(app_params, agg_params) supports a wider variety of VM extensions, increasing the performance of many cryptographic operations. The app_vm_config section of the openvm.toml of any guest program that uses Sdk::standard(app_params, agg_params) must also match a specific preset configuration.

Click here to view the Sdk::standard(app_params, agg_params) openvm.toml
openvm_standard.toml
[app_vm_config.rv32i]
[app_vm_config.rv32m]
[app_vm_config.io]
 
[app_vm_config.keccak]
[app_vm_config.sha2]
[app_vm_config.bigint]
 
[app_vm_config.modular]
supported_moduli = [
    # bn254 (alt bn128)
    "21888242871839275222246405745257275088696311157297823662689037894645226208583", # coordinate field
    "21888242871839275222246405745257275088548364400416034343698204186575808495617", # scalar field
    # secp256k1 (k256)
    "115792089237316195423570985008687907853269984665640564039457584007908834671663", # coordinate field
    "115792089237316195423570985008687907852837564279074904382605163141518161494337", # scalar field
    # secp256r1 (p256)
    "115792089210356248762697446949407573530086143415290314195533631308867097853951", # coordinate
    "115792089210356248762697446949407573529996955224135760342422259061068512044369", # scalar
    # bls12_381
    "4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787", # coordinate field
    "52435875175126190479447740508185965837690552500527637822603658699938581184513",                                       # scalar field
]
 
[app_vm_config.fp2]
supported_moduli = [
    [
        "Bn254Fp2",
        # bn254 (alt bn128)
        "21888242871839275222246405745257275088696311157297823662689037894645226208583",
    ],
    # Bls12_381
    [
        "Bls12_381Fp2",
        "4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787",
    ],
]
 
# bn254 (alt bn128)
[[app_vm_config.ecc.supported_curves]]
struct_name = "Bn254G1Affine"
modulus = "21888242871839275222246405745257275088696311157297823662689037894645226208583"
scalar = "21888242871839275222246405745257275088548364400416034343698204186575808495617"
a = "0"
b = "3"
 
# secp256k1 (k256)
[[app_vm_config.ecc.supported_curves]]
struct_name = "Secp256k1Point"
modulus = "115792089237316195423570985008687907853269984665640564039457584007908834671663"
scalar = "115792089237316195423570985008687907852837564279074904382605163141518161494337"
a = "0"
b = "7"
 
# secp256r1 (p256)
[[app_vm_config.ecc.supported_curves]]
struct_name = "P256Point"
modulus = "115792089210356248762697446949407573530086143415290314195533631308867097853951"
scalar = "115792089210356248762697446949407573529996955224135760342422259061068512044369"
a = "115792089210356248762697446949407573530086143415290314195533631308867097853948"
b = "41058363725152142129326129780047268409114441015993725554835256314039467401291"
 
# bls12_381
[[app_vm_config.ecc.supported_curves]]
struct_name = "Bls12_381G1Affine"
modulus = "4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787"
scalar = "52435875175126190479447740508185965837690552500527637822603658699938581184513"
a = "0"
b = "4"
 
[app_vm_config.pairing]
supported_curves = ["Bn254", "Bls12_381"]

Observe that this standard openvm.toml also enables normal Rust and openvm::io functions (via the rv32i, rv32m, and io extensions). keccak and sha2 enable intrinsic instructions for the Keccak and SHA-2 hashes respectively, and bigint supports Big Integer operations.

Modular operations for the BN254, Secp256k1 (i.e. K256), Secp256r1 (i.e. P256), and BLS12-381 curves' scalar and coordinate field moduli are also supported, as well as Complex Field Extension operations over the BN254 and BLS12-381 coordinate fields. Elliptic Curve Cryptography operations are also supported for the BN254, Secp256k1, Secp256r1, and BLS12-381 curves, and Elliptic Curve Pairing checks are supported for the BN254 and BLS12-381 curves.

For more information on extensions and guest libraries, see Acceleration Using Pre-Built Extensions.

Customizing the VM Configuration

To use a custom VM configuration, you can construct your own GenericSdk object which includes the extensions and system configuration your VM will use. To do this, you can use the SdkVmConfig::builder() method and set the desired extensions and system configuration as in the example below.

    let vm_config = SdkVmConfig::builder()
        .system(Default::default())
        .rv32i(Default::default())
        .rv32m(Default::default())
        .io(Default::default())
        .build()
        .optimize();
    let app_params = app_params_with_100_bits_security(MAX_APP_LOG_STACKED_HEIGHT);
    let agg_params = AggregationSystemParams::default();
    let sdk = Sdk::new(AppConfig::new(vm_config, app_params), agg_params)?;

A custom OpenVM configuration can also be specified in an openvm.toml file and read to create a GenericSdk.

    let app_vm_config = SdkVmConfig::from_toml(include_str!("your_path_project_root/openvm.toml"))?;
    let app_params = app_params_with_100_bits_security(MAX_APP_LOG_STACKED_HEIGHT);
    let agg_params = AggregationSystemParams::default();
    Sdk::new(AppConfig::new(app_vm_config, app_params), agg_params)?;
To customize your Sdk, you may need additional dependencies. Click here to view.
use openvm_sdk::{config::{AggregationSystemParams, AppConfig}, Sdk};
use openvm_sdk_config::SdkVmConfig;
use openvm_stark_sdk::config::{app_params_with_100_bits_security, MAX_APP_LOG_STACKED_HEIGHT};

Building and Transpiling a Program

The SDK provides lower-level control over the building and transpiling process. You can either use the SDK to build your ELF executable or read it in as bytes using fs::read.

    // 1. Initialize the SDK with the RV32IM preset and default aggregation parameters.
    let app_params = app_params_with_100_bits_security(MAX_APP_LOG_STACKED_HEIGHT);
    let agg_params = AggregationSystemParams::default();
    let sdk = Sdk::riscv32(app_params, agg_params);
 
    // 2a. Build the ELF with guest options and a target filter.
    let guest_opts = GuestOptions::default();
    let target_path = "your_path_project_root";
    let elf = sdk.build(guest_opts, target_path, &None, None)?;
    // 2b. Load the ELF from a file
    let elf: Vec<u8> = fs::read("your_path_to_elf")?;
 
    let exe = sdk.convert_to_exe(elf.clone())?;

Running a Program

To run your program and see the public value output, you can do the following:

    // 3. Format your input into StdIn
    let my_input = SomeStruct { a: 1, b: 2 }; // anything that can be serialized
    let mut stdin = StdIn::default();
    stdin.write(&my_input);
 
    // 4. Run the program
    let output = sdk.execute(exe.clone(), stdin.clone())?;
    println!("public values output: {output:?}");

Using StdIn

The StdIn struct allows you to format any serializable type into a VM-readable format by passing in a reference to your struct into StdIn::write as above. You also have the option to pass in a &[u8] into StdIn::write_bytes, or a &[F] into StdIn::write_field where F is the openvm_stark_sdk::p3_baby_bear::BabyBear field type.

Generating and Verifying Proofs

OpenVM supports generating three types of proofs. The sections below describe how to generate and verify each type of proof.

App Proof

Generating App Proofs

After building and transpiling a program, you can then generate a proof. To do so, you need to convert your ELF into a VmExe, commit your VmExe, generate an AppProvingKey, format your input into StdIn, and then generate a proof.

The SDK, however, combines the first few steps for you. Create a prover and pass in your input as a StdIn to generate an app proof.

    // 5. Generate an app proof.
    let mut prover = sdk.app_prover(elf)?.with_program_name("test_program");
    let proof = prover.prove(stdin)?;

For large guest programs, the program will be proved in multiple continuation segments and the returned proof: ContinuationVmProof object consists of multiple STARK proofs, one for each segment.

Verifying App Proofs

After generating a proof, you can verify it. To do so, you need your verifying key (which you can get from sdk.app_keygen()) and the output of your prove call.

    // 6. Do this once to save the app_vk, independent of the proof.
    let (_app_pk, app_vk) = sdk.app_keygen();
    // 7. Verify your program.
    let _ = verify_app_proof::<openvm_sdk::DefaultStarkEngine>(
        &app_vk.vk,
        app_vk.memory_dimensions,
        &proof,
    )?;
To use the above verification, you may need additional dependencies. Click here to view.
use openvm_build::GuestOptions;
use openvm_sdk::{config::AggregationSystemParams, prover::verify_app_proof, Sdk, StdIn};
use openvm_stark_sdk::config::{app_params_with_100_bits_security, MAX_APP_LOG_STACKED_HEIGHT};

STARK Proof

STARK Proof Generation and Verification

You can now run the aggregation keygen, proof, and verification functions for the STARK proof by either (a) calling sdk.prove(...) directly or (b) creating a prover with custom fields. STARK verification uses the VerificationBaseline returned by the proof flow, together with the aggregation verifying key.

    // 5a. Generate a proof and verification baseline directly.
    let (proof, baseline) = sdk.prove(exe.clone(), stdin.clone(), &[])?;
    // 5b. Or build a StarkProver with custom fields and generate the baseline separately.
    let mut prover = sdk.prover(exe)?.with_program_name("test_program");
    let baseline = prover.generate_baseline();
    let (proof, _metadata) = prover.prove(stdin.clone(), &[])?;

Once the proof is generated, you can verify it using the SDK as follows:

    // 6. Do this once to save the aggregation VK, independent of the proof.
    let (_agg_pk, agg_vk) = sdk.agg_keygen();
    // 7. Verify your program.
    Sdk::verify_proof(agg_vk, baseline, &proof)?;

Note that STARK verification requires the verification baseline to confirm that the submitted proof matches the app executable, VM configuration, and aggregation verifier configuration.

EVM Proof

Setup

To generate an EVM proof, you will first need to download trusted setup parameters from our S3 bucket. You can do so using the following script:

#!/bin/bash
 
for k in {5..23}
do
    wget -P ~/.openvm/params/ "https://axiom-crypto.s3.amazonaws.com/challenge_0085/kzg_bn254_${k}.srs"
done

Note that cargo openvm setup --evm CLI command will also download these parameters into the ~/.openvm/params/ directory. For more information on the setup process, see STARK and EVM Key Generation.

Note that there are additional dependencies for the EVM Proof flow. Click here to view.
use std::fs;
 
use eyre::Result;
use openvm_build::GuestOptions;
use openvm_sdk::{config::AggregationSystemParams, Sdk, StdIn};
use openvm_stark_sdk::config::{app_params_with_100_bits_security, MAX_APP_LOG_STACKED_HEIGHT};
use serde::{Deserialize, Serialize};
 
#[derive(Serialize, Deserialize)]
pub struct SomeStruct {
    pub a: u64,
    pub b: u64,
}

EVM Proof Generation and Verification

You can now run the aggregation keygen, proof, and verification functions for the EVM proof.

Note: you do not need to generate an app proof separately; the EVM proof flow handles the full proving pipeline automatically.

    // 5. Generate the SNARK verifier smart contract
    let verifier = sdk.generate_halo2_verifier_solidity()?;
 
    // 6. Generate an EVM proof
    // NOTE: if they have not been initialized already, this call will lazily generate the app,
    // aggregation, root, and Halo2 proving keys before producing the final EVM proof.
    let proof = sdk.prove_evm(elf, stdin, &[])?;
 
    // 7. Verify the EVM proof
    Sdk::verify_evm_halo2_proof(&verifier, proof)?;

Another Example: Using the Standard SDK

For many programs with modular arithmetic and/or cryptographic operations, the RISC-V instruction set alone may not be enough to achieve the performance desired. OpenVM supports many extensions to accelerate said operations, a standard set of which are included inside the standard SDK configuration.

Take, for example, this toy program that checks if several guest library implementations of IntMod are fields by loosely checking if their moduli are prime.

extern crate alloc;
 
use openvm_algebra_guest::IntMod;
use openvm_k256::{Secp256k1Coord, Secp256k1Scalar};
use openvm_p256::{P256Coord, P256Scalar};
use openvm_pairing::{bls12_381::Bls12_381Fp, bn254::Bn254Fp};
 
openvm::init!();
 
// Based on https://en.wikipedia.org/wiki/Fermat%27s_little_theorem. If this
// fails, then F::MODULUS is not prime.
fn fermat<F: IntMod>()
where
    F::Repr: AsRef<[u8]>,
{
    let mut pow = F::MODULUS;
    pow.as_mut()[0] -= 2;
 
    let a = F::from_u32(1234);
    let mut res = F::ONE;
    let mut mut_a = a.clone();
 
    for pow_byte in pow.as_ref() {
        for j in 0..8 {
            if pow_byte & (1 << j) != 0 {
                res *= &mut_a;
            }
            mut_a *= mut_a.clone();
        }
    }
 
    assert_eq!(res * a, F::ONE);
}
 
pub fn main() {
    fermat::<Bn254Fp>();
    fermat::<Bls12_381Fp>();
    fermat::<Secp256k1Coord>();
    fermat::<Secp256k1Scalar>();
    fermat::<P256Coord>();
    fermat::<P256Scalar>();
}

Its openvm.toml follows the preset configuration specified above.

Simply initialize Sdk using Sdk::standard(app_params, agg_params) and build, prove, and verify as above.

    // 1. Initialize the SDK with the standard configuration.
    let app_params = app_params_with_100_bits_security(MAX_APP_LOG_STACKED_HEIGHT);
    let agg_params = AggregationSystemParams::default();
    let sdk = Sdk::standard(app_params, agg_params);