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 aGenericSdk
supporting the RV32IM extension.Sdk::standard()
returns aGenericSdk
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:
[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
[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();