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::{Sdk, StdIn};
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() returns a GenericSdk supporting the RV32IM extension.
  • Sdk::standard() returns a GenericSdk supporting all default OpenVM extensions, including RV32IM.

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

Sdk::riscv32() 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() 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() must also match a specific preset configuration.

Click here to view the Sdk::standard() openvm.toml
openvm_standard.toml
[app_vm_config.rv32i]
[app_vm_config.rv32m]
[app_vm_config.io]
 
[app_vm_config.keccak]
[app_vm_config.sha256]
[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 sha256 enable intrinsic instructions for the Keccak and SHA-256 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 sdk = Sdk::new(
        AppConfig::new(FriParameters::standard_fast(), vm_config)
    )?;

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

    Sdk::new(SdkVmConfig::from_toml(include_str!("your_path_project_root/openvm.toml"))?)?;
To customize your Sdk, you may need additional dependencies. Click here to view.
use openvm_sdk::{
    config::{AppConfig, SdkVmConfig},
    Sdk,
};
use openvm_stark_sdk::config::FriParameters;

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. Build the VmConfig with the extensions needed.
    let sdk = Sdk::riscv32();
 
    // 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")?;
 

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(elf.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: Generates STARK proof(s) of the guest program
  • STARK Proof: Generates a STARK proof that can be posted on-chain
  • EVM Proof: Generates a halo2 proof that can be posted on-chain

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.
    verify_app_proof(&app_vk, &proof)?;
To use the above verification, you may need additional dependencies. Click here to view.
use openvm_build::GuestOptions;
use openvm_sdk::{prover::verify_app_proof, Sdk, StdIn};

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. You should also generate an app commit, e.g. hashes of both the app config and executable used to generate the proof.

    // 5a. Generate a proof
    let (proof, app_commit) = sdk.prove(elf.clone(), stdin.clone())?;
    // 5b. Generate a proof with a StarkProver with custom fields
    let mut prover = sdk.prover(elf)?.with_program_name("test_program");
    let app_commit = prover.app_commit();
    let proof = 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 agg_vk, independent of the proof.
    let (_agg_pk, agg_vk) = sdk.agg_keygen()?;
    // 7. Verify your program
    Sdk::verify_proof(&agg_vk, app_commit, &proof)?;

Note that STARK verification requires the app commit to confirm that the submitted proof is for the app config and executable specified.

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::{Sdk, StdIn};
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 the app proof with the generate_app_proof function, as the EVM proof function will handle this automatically.

    // 5. Generate the SNARK verifier smart contract
    let verifier = sdk.generate_halo2_verifier_solidity()?;
 
    // 6. Generate an EVM proof
    // NOTE: this will do app_keygen, agg_keygen, halo2_keygen automatically if they have never been
    // called before. As a consequence, the first call to `prove_evm` will take longer if you do not
    // explicitly call `app_keygen`, `agg_keygen`, and `halo2_keygen` before calling `prove_evm`.
    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() and build, prove, and verify as above.

    // 1. Initialize Sdk with the standard configuration.
    let sdk = Sdk::standard();