OpenVM

A performant and modular zkVM framework built for customization and extensibility

OpenVM is an open-source zero-knowledge virtual machine (zkVM) framework focused on modularity at every level of the stack. OpenVM is designed for customization and extensibility without sacrificing performance or maintainability.

Key Features

  • Modular no-CPU Architecture: Unlike traditional machine architectures, the OpenVM architecture has no central processing unit. This design choice allows for seamless integration of custom chips, without forking or modifying the core architecture.

  • Extensible Instruction Set: The instruction set architecture (ISA) is designed to be extended with new custom instructions that integrate directly with the virtual machine. Current extensions available for OpenVM include:

    • RISC-V support via RV32IM
    • A native field arithmetic extension for proof recursion and aggregation
    • The Keccak-256 hash function
    • Int256 arithmetic
    • Modular arithmetic over arbitrary fields
    • Elliptic curve operations, including multi-scalar multiplication and ECDSA scalar multiplication.
    • Pairing operations on the BN254 and BLS12-381 curves.
  • Rust Frontend: ISA extensions are directly accessible through a Rust frontend via intrinsic functions, providing a smooth developer experience.

  • On-chain Verification: Every VM made using the framework comes with out-of-the-box support for unbounded program proving with verification on Ethereum.

Using This Book

The following chapters will guide you through:

Security Status

As of December 2024, OpenVM has not been audited and is currently not recommended for production use. We plan to continue development towards a production-ready release in 2025.

đź“– About this book

The book is continuously rendered here! You can contribute to this book on GitHub.

Install

To use OpenVM for generating proofs, you must install the OpenVM command line tool cargo-openvm.

cargo-openvm can be installed in two different ways. You can either install via git URL or build from source.

You will need the nightly toolchain. You can install it with:

rustup toolchain install nightly

Then, begin the installation.

cargo +nightly install --git http://github.com/openvm-org/openvm.git cargo-openvm

This will globally install cargo-openvm. You can validate a successful installation with:

cargo openvm --version

Option 2: Build from source

To build from source, you will need the nightly toolchain. You can install it with:

rustup toolchain install nightly

Then, clone the repository and begin the installation.

git clone https://github.com/openvm-org/openvm.git
cd openvm
cargo +nightly install --force --path crates/cli

This will globally install cargo-openvm. You can validate a successful installation with:

cargo openvm --version

Install Rust Toolchain

In order for the cargo-openvm build command to work, you must install certain Rust nightly components:

rustup install nightly-2024-10-30
rustup component add rust-src --toolchain nightly-2024-10-30

Quickstart

In this section we will build and run a fibonacci program.

Setup

First, create a new Rust project.

cargo init fibonacci

In Cargo.toml, add the following dependency:

[dependencies]
openvm = { git = "https://github.com/openvm-org/openvm.git", features = ["std"] }

Note that std is not enabled by default, so explicitly enabling it is required.

The fibonacci program

The read function takes input from the stdin (it also works with OpenVM runtime).

// src/main.rs
use openvm::io::{read, reveal};

openvm::entry!(main);

fn main() {
    let n: u64 = read();
    let mut a: u64 = 0;
    let mut b: u64 = 1;
    for _ in 0..n {
        let c: u64 = a.wrapping_add(b);
        a = b;
        b = c;
    }
    reveal(a as u32, 0);
    reveal((a >> 32) as u32, 1);
}

Build

To build the program, run:

cargo openvm build

This will output an OpenVM executable file to ./openvm/app.vmexe.

Keygen

Before generating any proofs, we will also need to generate the proving and verification keys.

cargo openvm keygen

This will output a serialized proving key to ./openvm/app.pk and a verification key to ./openvm/app.vk.

Proof Generation

Now we are ready to generate a proof! Simply run:

OPENVM_FAST_TEST=1 cargo openvm prove app --input "0x0A00000000000000"

The --input field is passed to the program which receives it via the io::read function. In our main.rs we called read() to get n: u64. The input here is n = 100u64 in little endian. Note that this value must be padded to exactly 8 bytes (64 bits).

The serialized proof will be output to ./openvm/app.proof.

The OPENVM_FAST_TEST environment variable is used to enable fast proving for testing purposes. To run with proof with secure parameters, remove the environmental variable.

Proof Verification

Finally, the proof can be verified.

cargo openvm verify app

The process should exit with no errors.

Runtime Execution

If necessary, the executable can also be run without proof generation. This can be useful for testing purposes.

cargo openvm run --input "0x0A00000000000000"

Overview of Basic Usage

Writing a Program

The first step to using OpenVM is to write a Rust program that can be executed by an OpenVM virtual machine. Writing a program for OpenVM is very similar to writing a standard Rust program, with a few key differences necessary to support the OpenVM environment. For more detailed information about writing programs, see the Writing Programs guide.

Building and Transpiling a Program

At this point, you should have a guest program with a Cargo.toml file in the root of your project directory. What's next?

The first thing you will want to do is build and transpile your program using the following command:

cargo openvm build

By default this will build the project located in the current directory. To see if it runs correctly, you can try executing it with the following:

cargo openvm run --input <path_to_input | hex_string>

Note if your program doesn't require inputs, you can omit the --input flag.

For more information on both commands, see the build docs.

Inputs

The --input field needs to either be a hex string or a file path to a file that will be read as bytes. Note that if your hex string represents a single number, it should be written in little-endian format (as this is what the VM expects). To see how more complex inputs can be converted into a VM-readable format, see the Using StdIn section of the SDK doc.

Generating a Proof

Given an app configuration TOML file, you first need to generate a proving and verifying key:

cargo openvm keygen

After generating the keys, you can generate a proof by running:

cargo openvm prove app --input <path_to_input | hex_string>

Again, if your program doesn't require inputs, you can omit the --input flag.

For more information on the keygen and prove commands, see the prove doc.

Verifying a Proof

To verify a proof using the CLI, you need to provide the verifying key and the proof.

cargo openvm verify app

For more information on the verify command, see the verify doc.

End-to-end EVM Proof Generation and Verification

The process above details the workflow necessary to build, prove, and verify a guest program at the application level. However, to generate the end-to-end EVM proof, you need to (a) setup the aggregation proving key and verifier contract and (b) generate/verify the proof at the EVM level.

To do (a), you need to run the following command. If you've run it previously on your machine, there is no need to do so again. This will write files necessary for EVM proving in ~/.openvm/.

cargo openvm setup

⚠️ WARNING
This command requires very large amounts of computation and memory (~200 GB).

To do (b), you simply need to replace app in cargo openvm prove and cargo openvm verify as such:

cargo openvm prove evm --input <path_to_input | hex_string>
cargo openvm verify evm

Writing a Program

Writing a guest program

See the example fibonacci program.

The guest program should be a no_std Rust crate. As long as it is no_std, you can import any other no_std crates and write Rust as you normally would. Import the openvm library crate to use openvm intrinsic functions (for example openvm::io::*).

The guest program also needs #![no_main] because no_std does not have certain default handlers. These are provided by the openvm::entry! macro. You should still create a main function, and then add openvm::entry!(main) for the macro to set up the function to run as a normal main function. While the function can be named anything when target_os = "zkvm", for compatibility with std you should still name the function main.

To support both std and no_std execution, the top of your guest program should have:

#![allow(unused)]
#![cfg_attr(not(feature = "std"), no_main)]
#![cfg_attr(not(feature = "std"), no_std)]
fn main() {
}

More examples of guest programs can be found in the benchmarks/programs directory.

no-std

Although it's usually ok to use std (like in quickstart), not all std functionalities are supported (e.g., randomness). There might be unexpected runtime errors if one uses std, so it is recommended you develop no_std libraries if possible to reduce surprises. Even without std, assert! and panic! can work as normal. To use std features, one should add the following to Cargo.toml feature sections:

[features]
std = ["openvm/std"]

Handling I/O

The program can take input from stdin, with some functions provided by openvm::io.

openvm::io::read takes from stdin and deserializes it into a generic type T, so one should specify the type when calling it:

#![allow(unused)]
fn main() {
let n: u64 = read();
}

openvm::io::read_vec will just read a vector and return Vec<u8>.

openvm::io::reveal sends public values to the final proof (to be read by the smart contract).

For debugging purposes, openvm::io::print and openvm::io::println can be used normally, but println! will only work if std is enabled.

Building and running

See the overview on how to build and run the program.

Compiling a Program

First let's define some key terms used in cross-compilation:

  • host - the machine you're compiling and/or proving on. Note that one can compile and prove on different machines, but they are both called host as they are traditional machine architectures.
  • guest - the executable to be run in a different VM architecture (e.g. the OpenVM runtime, or Android app).

The command cargo openvm build compiles the program on host to an executable for guest target. It first compiles the program normally on your host platform with RISC-V and then transpiles it to a different target. See here for some explanation of cross-compilation. Right now we use riscv32im-risc0-zkvm-elf target which is available in the Rust toolchain, but we will contribute an OpenVM target to Rust in the future.

Build flags

The following flags are available for the cargo openvm build command:

  • --manifest-dir <MANIFEST_DIR>

    Description: Specifies the directory containing the Cargo.toml file for the guest code.

    Default: The current directory (.).

    Usage Example: If your Cargo.toml is located in my_project/, you can run:

    cargo openvm build --manifest-dir my_project
    

    This ensures the build command is executed in that directory.

  • --features <FEATURES>

    Description: Passes a list of feature flags to the Cargo build process. These flags enable or disable conditional compilation features defined in your Cargo.toml.

    Usage Example: To enable the my_feature feature:

    cargo openvm build --features my_feature
    
  • --bin <NAME>

    Description: Restricts the build to the binary target with the given name, similar to cargo build --bin <NAME>. If your project has multiple target types (binaries, libraries, examples, etc.), using --bin <NAME> narrows down the build to the binary target with the given name.

    Usage Example:

    cargo openvm build --bin my_bin
    
  • --example <NAME>

    Description: Restricts the build to the example target with the given name, similar to cargo build --example <NAME>. Projects often include code samples or demos under the examples directory, and this flag focuses on compiling a specific example.

    Usage Example:

    cargo openvm build --example my_example
    
  • --no-transpile

    Description: After building the guest code, doesn't transpile the target ELF into an OpenVM-compatible executable (by default it does).

    Usage Example:

    cargo openvm build --no-transpile
    
  • --config <CONFIG>

    Description: Specifies the path to a .toml configuration file that defines which VM extensions to use.

    Default: ./openvm.toml if --config flag is not provided.

    Usage Example:

    cargo openvm build --config path/to/openvm.toml
    

    This allows you to customize the extensions. Currently the CLI only supports known extensions listed in the Using Existing Extensions section. To use other extensions, use the SDK.

  • --exe-output <EXE_OUTPUT>

    Description: Sets the output path for the transpiled program.

    Default: ./openvm/app.vmexe if --exe-output flag is not provided.

    Usage Example: To specify a custom output filename:

    cargo openvm build --exe-output ./output/custom_name.vmexe
    
  • --profile <PROFILE>

    Description: Determines the build profile used by Cargo. Common profiles are dev (faster builds, less optimization) and release (slower builds, more optimization).

    Default: release

    Usage Example:

    cargo openvm build --profile dev
    
  • --help

    Description: Prints a help message describing the available options and their usage.

    Usage Example:

    cargo openvm build --help
    

Running a Program

After building and transpiling a program, you can execute it using the run command. The run command has the following arguments:

cargo openvm run
    --exe <path_to_transpiled_program>
    --config <path_to_app_config>
    --input <path_to_input>

If --exe and/or --config are not provided, the command will search for these files in ./openvm/app.vmexe and ./openvm.toml respectively. If ./openvm.toml is not present, a default configuration will be used.

If your program doesn't require inputs, you can (and should) omit the --input flag.

Generating Proofs

Generating a proof using the CLI is simple - first generate a key, then generate your proof. Using command defaults, this looks like:

cargo openvm keygen
cargo openvm prove [app | evm]

Key Generation

The keygen CLI command has the following optional arguments:

cargo openvm keygen
    --config <path_to_app_config>
    --output <path_to_app_pk>
    --vk_output <path_to_app_vk>

If --config is not provided, the command will search for ./openvm.toml and use that as the application configuration if present. If it is not present, a default configuration will be used.

If --output and/or --vk_output are not provided, the keys will be written to default locations ./openvm/app.pk and/or ./openvm/app.vk respectively.

Proof Generation

The prove CLI command has the following optional arguments:

cargo openvm prove [app | evm]
    --app_pk <path_to_app_pk>
    --exe <path_to_transpiled_program>
    --input <path_to_input>
    --output <path_to_output>

If your program doesn't require inputs, you can (and should) omit the --input flag.

If --app_pk and/or --exe are not provided, the command will search for these files in ./openvm/app.pk and ./openvm/app.vmexe respectively. Similarly, if --output is not provided then the command will write the proof to ./openvm/[app | evm].proof by default.

The app subcommand is used to generate an application-level proof, while the evm command generates an end-to-end EVM proof.

⚠️ WARNING
In order to run the evm subcommand, you must have previously called the costly cargo openvm setup, which requires very large amounts of computation and memory (~200 GB).

Verifying Proofs

Application Level

Verifying a proof at the application level requires both the proof and application verifying key.

cargo openvm verify app
    --app_vk <path_to_app_vk>
    --proof <path_to_proof>

If you omit --app_vk and/or --proof, the command will search for those files at ./openvm/app.vk and ./openvm/app.proof respectively.

Once again, if you omitted --output and --vk_output in the keygen and prove commands, you can omit --app_vk and --proof in the verify command.

EVM Level

EVM level proof setup requires large amounts of computation and memory (~200GB). It is recommended to run this process on a server.

Install Solc

Install solc 0.8.19 using svm

# Install svm
cargo install --version 0.5.7 svm-rs
# Add the binary to your path
export PATH="$HOME/.cargo/bin:$PATH"

# Install solc 0.8.19
svm install 0.8.19
svm use 0.8.19

Generating the Aggregation Proving Key and EVM Verifier Contract

The workflow for generating an end-to-end EVM proof requires first generating an aggregation proving key and EVM verifier contract. This can be done by running the following command:

cargo openvm setup

⚠️ WARNING
This command requires very large amounts of computation and memory (~200 GB).

Upon a successful run, the command will write agg.pk and verifier.sol to ~/.openvm/, where ~ is the directory specified by environment variable $HOME. Every command that requires these files will look for them in this directory.

⚠️ WARNING
If the $HOME environment variable is not set, this command may fail.

Note that cargo openvm setup may attempt to download other files (i.e. KZG parameters) from an AWS S3 bucket into ~/.openvm/.

This command can take ~20mins on a m6a.16xlarge instance due to the keygen time.

Generating and Verifying an EVM Proof

To generate and verify an EVM proof, you need to run the following commands:

cargo openvm prove evm --input <path_to_input>
cargo openvm verify evm --proof <path_to_proof>

If proof is omitted, the verify command will search for the proof at ./openvm/evm.proof.

Using Existing Extensions

You can seamlessly integrate certain performance-optimized extensions maintained by the OpenVM team to enhance your arithmetic operations and cryptographic computations.

In this chapter, we will explain how to use the following existing extensions:

Some extensions such as openvm-keccak-guest and openvm-bigint-guest can be enabled without specifying any additional configuration.

On the other hand certain arithmetic operations, particularly modular arithmetic, can be optimized significantly when the modulus is known at compile time. This approach requires a framework to inform the compiler about all the moduli and associated arithmetic structures we intend to use. To achieve this, three steps are involved:

  1. Declare: Introduce a modular arithmetic or related structure, along with its modulus and functionality. This can be done in any library or binary file.
  2. Init: Performed exactly once in the final binary. It aggregates all previously declared structures, assigns them stable indices, and sets up linkage so that they can be referenced in generated code.
  3. Setup: A one-time runtime procedure for security. This ensures that the compiled code matches the virtual machine’s expectations and that each instruction set is tied to the correct modulus or extension.

These steps ensure both performance and security: performance because the modulus is known at compile time, and security because runtime checks confirm that the correct structures have been initialized.

Our design for the configuration procedure above was inspired by the EVMMAX proposal.

Configuration

To use these extensions, you must populate a openvm.toml in your package root directory (where the Cargo.toml file is located). We will explain in each extension how to configure the openvm.toml file.

The template openvm.toml file is as follows:

[app_vm_config.rv32i]
[app_vm_config.rv32m]
[app_vm_config.io]
[app_vm_config.keccak]
[app_vm_config.native]
[app_vm_config.bigint]
[app_vm_config.modular]
supported_modulus = ["<modulus_1>", "<modulus_2>", ...]
[app_vm_config.fp2]
supported_modulus = ["<modulus_1>", "<modulus_2>", ...]
[app_vm_config.pairing]
supported_curves = ["Bls12_381", "Bn254"]
[[app_vm_config.ecc.supported_curves]]
modulus = "<modulus_1>"
scalar = "<scalar_1>"
a = "<a_1>"
b = "<b_1>"
[[app_vm_config.ecc.supported_curves]]
modulus = "<modulus_2>"
scalar = "<scalar_2>"
a = "<a_2>"
b = "<b_2>"

rv32i, io, and rv32m need to be always included if you make an openvm.toml file while the rest are optional and should be included if you want to use the corresponding extension. All moduli and scalars must be provided in decimal format. Currently pairing supports only pre-defined Bls12_381 and Bn254 curves. To add more ecc curves you need to add more [[app_vm_config.ecc.supported_curves]] entries.

OpenVM Keccak256

The OpenVm Keccak256 extension provides tools for using the Keccak-256 hash function. The functional part is provided by the openvm-keccak-guest crate, which is a guest library that can be used in any OpenVM program.

Functions for guest code

The OpenVM Keccak256 Guest extension provides two functions for using in your guest code:

  • keccak256(input: &[u8]) -> [u8; 32]: Computes the Keccak-256 hash of the input data and returns it as an array of 32 bytes.
  • set_keccak256(input: &[u8], output: &mut [u8; 32]): Sets the output to the Keccak-256 hash of the input data into the provided output buffer.

See the full example here.

Example:

use hex::FromHex;
use openvm_keccak256_guest::keccak256;

pub fn main() {
    let test_vectors = [
        ("", "C5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470"),
        ("CC", "EEAD6DBFC7340A56CAEDC044696A168870549A6A7F6F56961E84A54BD9970B8A"),
    ];
    for (input, expected_output) in test_vectors.iter() {
        let input = Vec::from_hex(input).unwrap();
        let expected_output = Vec::from_hex(expected_output).unwrap();
        let output = keccak256(&black_box(input));
        if output != *expected_output {
            panic!();
        }
    }
}

To be able to import the keccak256 function, add the following to your Cargo.toml file:

openvm-keccak256-guest = { git = "https://github.com/openvm-org/openvm.git" }
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }

Native Keccak256

Keccak guest extension also provides another way to use the native Keccak-256 implementation. It provides a function that is meant to be linked to other external libraries. The external libraries can use this function as a hook for the Keccak-256 native implementation. Enabled only when the target is zkvm.

  • native_keccak256(input: *const u8, len: usize, output: *mut u8): This function has C ABI. It takes in a pointer to the input, the length of the input, and a pointer to the output buffer.

In the external library, you can do the following:

#![allow(unused)]
fn main() {
extern "C" {
    fn native_keccak256(input: *const u8, len: usize, output: *mut u8);
}

fn keccak256(input: &[u8]) -> [u8; 32] {
    #[cfg(target_os = "zkvm")] {
    let mut output = [0u8; 32];
        unsafe {
            native_keccak256(input.as_ptr(), input.len(), output.as_mut_ptr() as *mut u8);
        }
        output
    }
    #[cfg(not(target_os = "zkvm"))] {
        // Regular Keccak-256 implementation
    }
}
}

Config parameters

For the guest program to build successfully add the following to your .toml file:

[app_vm_config.keccak]

OpenVM BigInt

The OpenVM BigInt extension (aka Int256) provides two structs: U256 and I256. These structs can be used to perform 256 bit arithmetic operations. The functional part is provided by the openvm-bigint-guest crate, which is a guest library that can be used in any OpenVM program.

U256

The U256 struct is a 256-bit unsigned integer type.

Constants

The U256 struct has the following constants:

  • MAX: The maximum value of a U256.
  • MIN: The minimum value of a U256.
  • ZERO: The zero constant.

Constructors

The U256 struct implements the following constructors: from_u8, from_u32, and from_u64.

Binary Operations

The U256 struct implements the following binary operations: addition, subtraction, multiplication, bitwise and, bitwise or, bitwise xor, bitwise shift right, and bitwise shift left. All operations will wrap the result when the result is outside the range of the U256 type.

All of the operations can be used in 6 different ways: U256 op U256 or U256 op &U256 or &U256 op U256 or &U256 op &U256 or U256 op= U256 or &U256 op= U256.

Other

When using the U256 struct with target_os = "zkvm", the struct utilizes efficient implementations of comparison operators as well as the clone method.

Example matrix multiplication using U256

See the full example here.

#![cfg_attr(not(feature = "std"), no_main)]
#![cfg_attr(not(feature = "std"), no_std)]

openvm::entry!(main);
use core::array;
use openvm_bigint_guest::U256;

const N: usize = 16;
type Matrix = [[U256; N]; N];

pub fn get_matrix(val: u8) -> Matrix {
    array::from_fn(|_| array::from_fn(|_| U256::from_u8(val)))
}

pub fn mult(a: &Matrix, b: &Matrix) -> Matrix {
    let mut c = get_matrix(0);
    for i in 0..N {
        for j in 0..N {
            for k in 0..N {
                c[i][j] += &a[i][k] * &b[k][j];
            }
        }
    }
    c
}

pub fn get_identity_matrix() -> Matrix {
    let mut res = get_matrix(0);
    for i in 0..N {
        res[i][i] = U256::from_u8(1);
    }
    res
}

pub fn main() {
    let a: Matrix = get_identity_matrix();
    let b: Matrix = get_matrix(28);
    let c: Matrix = mult(&a, &b);
    assert_eq!(c, b);
}

To be able to import the U256 struct, add the following to your Cargo.toml file:

openvm-bigint-guest = { git = "https://github.com/openvm-org/openvm.git" }

I256

The I256 struct is a 256-bit signed integer type. The I256 struct is very similar to the U256 struct.

Constants

The I256 struct has the following constants:

  • MAX: The maximum value of a I256.
  • MIN: The minimum value of a I256.
  • ZERO: The zero constant.

Binary Operations

The I256 struct implements the following binary operations: addition, subtraction, multiplication, bitwise and, bitwise or, bitwise xor, bitwise shift right, and bitwise shift left. All operations will wrap the result when the result is outside the range of the I256 type. Note that unlike the U256, when performing the shift right operation I256 will perform an arithmetic shift right (i.e. sign extends the result).

All of the operations can be used in 6 different ways: I256 op I256 or I256 op &I256 or &I256 op I256 or &I256 op &I256 or I256 op= I256 or &I256 op= I256.

Constructors

The I256 struct implements the following constructors: from_i8, from_i32, and from_i64.

Other

When using the I256 struct with target_os = "zkvm", the struct utilizes efficient implementations of comparison operators as well as the clone method.

Example matrix multiplication using I256

See the full example here.

#![cfg_attr(not(feature = "std"), no_main)]
#![cfg_attr(not(feature = "std"), no_std)]

openvm::entry!(main);
use core::array;
use openvm_bigint_guest::I256;

const N: usize = 16;
type Matrix = [[I256; N]; N];

pub fn get_matrix(val: i32) -> Matrix {
    array::from_fn(|_| array::from_fn(|_| I256::from_i32(val)))
}

pub fn mult(a: &Matrix, b: &Matrix) -> Matrix {
    let mut c = get_matrix(0);
    for i in 0..N {
        for j in 0..N {
            for k in 0..N {
                c[i][j] += &a[i][k] * &b[k][j];
            }
        }
    }
    c
}

pub fn get_identity_matrix() -> Matrix {
    let mut res = get_matrix(0);
    for i in 0..N {
        res[i][i] = I256::from_i32(1);
    }
    res
}

pub fn main() {
    let a: Matrix = get_identity_matrix();
    let b: Matrix = get_matrix(-28);
    let c: Matrix = mult(&a, &b);
    assert_eq!(c, b);
}

To be able to import the I256 struct, add the following to your Cargo.toml file:

openvm-bigint-guest = { git = "https://github.com/openvm-org/openvm.git" }

External Functions

The Bigint Guest extension provides another way to use the native implementation. It provides external functions that are meant to be linked to other external libraries. The external libraries can use these functions as a hook for the 256 bit integer native implementations. Enabled only when the target_os = "zkvm". All of the functions are defined as unsafe extern "C" fn. Also, note that you must enable the feature export-intrinsics to make them globally linkable.

  • zkvm_u256_wrapping_add_impl(result: *mut u8, a: *const u8, b: *const u8): takes in a pointer to the result, and two pointers to the inputs. result = a + b.
  • zkvm_u256_wrapping_sub_impl(result: *mut u8, a: *const u8, b: *const u8): takes in a pointer to the result, and two pointers to the inputs. result = a - b.
  • zkvm_u256_wrapping_mul_impl(result: *mut u8, a: *const u8, b: *const u8): takes in a pointer to the result, and two pointers to the inputs. result = a * b.
  • zkvm_u256_bitxor_impl(result: *mut u8, a: *const u8, b: *const u8): takes in a pointer to the result, and two pointers to the inputs. result = a ^ b.
  • zkvm_u256_bitand_impl(result: *mut u8, a: *const u8, b: *const u8): takes in a pointer to the result, and two pointers to the inputs. result = a & b.
  • zkvm_u256_bitor_impl(result: *mut u8, a: *const u8, b: *const u8): takes in a pointer to the result, and two pointers to the inputs. result = a | b.
  • zkvm_u256_wrapping_shl_impl(result: *mut u8, a: *const u8, b: *const u8): takes in a pointer to the result, and two pointers to the inputs. result = a << b.
  • zkvm_u256_wrapping_shr_impl(result: *mut u8, a: *const u8, b: *const u8): takes in a pointer to the result, and two pointers to the inputs. result = a >> b.
  • zkvm_u256_arithmetic_shr_impl(result: *mut u8, a: *const u8, b: *const u8): takes in a pointer to the result, and two pointers to the inputs. result = a.arithmetic_shr(b).
  • zkvm_u256_eq_impl(a: *const u8, b: *const u8) -> bool: takes in two pointers to the inputs. Returns true if a == b, otherwise false.
  • zkvm_u256_cmp_impl(a: *const u8, b: *const u8) -> Ordering: takes in two pointers to the inputs. Returns the ordering of a and b.
  • zkvm_u256_clone_impl(result: *mut u8, a: *const u8): takes in a pointer to the result buffer, and a pointer to the input. result = a.

And in the external library, you can do the following:

#![allow(unused)]
fn main() {
extern "C" {
    fn zkvm_u256_wrapping_add_impl(result: *mut u8, a: *const u8, b: *const u8);
}

fn wrapping_add(a: &Custom_U256, b: &Custom_U256) -> Custom_U256 {
    #[cfg(target_os = "zkvm")] {
        let mut result: MaybeUninit<Custom_U256> = MaybeUninit::uninit();
        unsafe {
            zkvm_u256_wrapping_add_impl(result.as_mut_ptr() as *mut u8, a as *const u8, b as *const u8);
        }
        unsafe { result.assume_init() }
    }
    #[cfg(not(target_os = "zkvm"))] {
        // Regular wrapping add implementation
    }
}
}

Config parameters

For the guest program to build successfully add the following to your .toml file:

[app_vm_config.bigint]

OpenVM Algebra

The OpenVM Algebra extension provides tools to create and manipulate modular arithmetic structures and their complex extensions. For example, if \(p\) is prime, OpenVM Algebra can handle modular arithmetic in \(\mathbb{F}_p\)​ and its quadratic extension fields \(\mathbb{F}_p[x]/(x^2 + 1)\).

The functional part is provided by the openvm-algebra-guest crate, which is a guest library that can be used in any OpenVM program. The macros for creating corresponding structs are in the openvm-algebra-moduli-setup and openvm-algebra-complex-macros crates.

Available traits and methods

  • IntMod trait: Defines the type Repr and constants MODULUS, NUM_LIMBS, ZERO, and ONE. It also provides basic methods for constructing a modular arithmetic object and performing arithmetic operations.

    • Repr typically is [u8; NUM_LIMBS], representing the number's underlying storage.
    • MODULUS is the compile-time known modulus.
    • ZERO and ONE represent the additive and multiplicative identities, respectively.
    • Constructors include from_repr, from_le_bytes, from_be_bytes, from_u8, from_u32, and from_u64.
  • Field trait: Provides constants ZERO and ONE and methods for basic arithmetic operations within a field.

Modular arithmetic

To leverage compile-time known moduli for performance, you declare, initialize, and then set up the arithmetic structures:

  1. Declare: Use the moduli_declare! macro to define a modular arithmetic struct. This can be done multiple times in various crates or modules:
#![allow(unused)]
fn main() {
moduli_declare! {
    Bls12_381Fp { modulus = "0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab" },
    Bn254Fp { modulus = "21888242871839275222246405745257275088696311157297823662689037894645226208583" },
}
}

This creates Bls12_381Fp and Bn254Fp structs, each implementing the IntMod trait. The modulus parameter must be a string literal in decimal or hexadecimal format.

  1. Init: Use the moduli_init! macro exactly once in the final binary:
#![allow(unused)]
fn main() {
moduli_init! {
    "0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab",
    "21888242871839275222246405745257275088696311157297823662689037894645226208583"
}
}

This step enumerates the declared moduli (e.g., 0 for the first one, 1 for the second one) and sets up internal linkage so the compiler can generate the appropriate RISC-V instructions associated with each modulus.

  1. Setup: At runtime, before performing arithmetic, a setup instruction must be sent to ensure security and correctness. For the \(i\)-th modulus, you call setup_<i>() (e.g., setup_0() or setup_1()). Alternatively, setup_all_moduli() can be used to handle all declared moduli.

Summary:

  • moduli_declare!: Declares modular arithmetic structures and can be done multiple times.
  • moduli_init!: Called once in the final binary to assign and lock in the moduli.
  • setup_<i>()/setup_all_moduli(): Ensures at runtime that the correct modulus is in use, providing a security check and finalizing the environment for safe arithmetic operations.

Complex field extension

Complex extensions, such as \(\mathbb{F}_p[x]/(x^2 + 1)\), are defined similarly using complex_declare! and complex_init!:

  1. Declare:
#![allow(unused)]
fn main() {
complex_declare! {
    Bn254Fp2 { mod_type = Bn254Fp }
}
}

This creates a Bn254Fp2 struct, representing a complex extension field. The mod_type must implement IntMod.

  1. Init: Called once, after moduli_init!, to enumerate these extensions and generate corresponding instructions:
#![allow(unused)]
fn main() {
complex_init! {
    Bn254Fp2 { mod_idx = 0 },
}
}

Note that you need to use the same type name in complex_declare! and complex_init!. For example, the following code will fail to compile:

#![allow(unused)]
fn main() {
// moduli related macros...

complex_declare! {
    Bn254Fp2 { mod_type = Bn254Fp },
}

pub type Fp2 = Bn254Fp2;

complex_init! {
    Fp2 { mod_idx = 0 },
}
}

Here, mod_idx refers to the index of the underlying modulus as initialized by moduli_init!

  1. Setup: Similar to moduli, call setup_complex_<i>() or setup_all_complex_extensions() at runtime to secure the environment.

Config parameters

For the guest program to build successfully, all used moduli must be declared in the .toml config file in the following format:

[app_vm_config.modular]
supported_modulus = ["115792089237316195423570985008687907853269984665640564039457584007908834671663"]

[app_vm_config.fp2]
supported_modulus = ["115792089237316195423570985008687907853269984665640564039457584007908834671663"]

The supported_modulus parameter is a list of moduli that the guest program will use. They must be provided in decimal format in the .toml file.

Example program

Here is a toy example using both the modular arithmetic and complex field extension capabilities:

#![cfg_attr(not(feature = "std"), no_main)]
#![cfg_attr(not(feature = "std"), no_std)]

use openvm_algebra_guest::{IntMod, moduli_setup::*};

openvm::entry!(main);

// This macro will create two structs, `Mod1` and `Mod2`,
// one for arithmetic modulo 998244353, and the other for arithmetic modulo 1000000007.
moduli_declare! {
    Mod1 { modulus = "998244353" },
    Mod2 { modulus = "1000000007" }
}

// This macro will initialize the moduli.
// Now, `Mod1` is the "zeroth" modular struct, and `Mod2` is the "first" one.
moduli_init! {
    "998244353", "1000000007"
}

// This macro will create two structs, `Complex1` and `Complex2`,
// one for arithmetic in the field $\mathbb{F}_{998244353}[x]/(x^2 + 1)$,
// and the other for arithmetic in the field $\mathbb{F}_{1000000007}[x]/(x^2 + 1)$.
openvm_algebra_complex_macros::complex_declare! {
    Complex1 { mod_type = Mod1 },
    Complex2 { mod_type = Mod2 },
}

// The order of these structs does not matter,
// given that we specify the `mod_idx` parameters properly.
openvm_algebra_complex_macros::complex_init! {
    Complex2 { mod_idx = 1 }, Complex1 { mod_idx = 0 },
}

pub fn main() {
    // Since we only use an arithmetic operation with `Mod1` and not `Mod2`,
    // we only need to call `setup_0()` here.
    setup_0();
    setup_all_complex_extensions();
    let a = Complex1::new(Mod1::ZERO, Mod1::from_u32(0x3b8) * Mod1::from_u32(0x100000)); // a = -i in the corresponding field
    let b = Complex2::new(Mod2::ZERO, Mod2::from_u32(1000000006)); // b = -i in the corresponding field
    assert_eq!(a.clone() * &a * &a * &a * &a, a); // a^5 = a
    assert_eq!(b.clone() * &b * &b * &b * &b, b); // b^5 = b
    // Note that these assertions would fail, have we provided the `mod_idx` parameters wrongly.
}

To have the correct imports for the above example, add the following to the Cargo.toml file:

[dependencies]
openvm = { git = "https://github.com/openvm-org/openvm.git" }
openvm-platform = { git = "https://github.com/openvm-org/openvm.git" }
openvm-algebra-guest = { git = "https://github.com/openvm-org/openvm.git" }
openvm-algebra-complex-macros = { git = "https://github.com/openvm-org/openvm.git" }
serde = { version = "1.0.216", default-features = false }

Here is the full openvm.toml to accompany the above example:

[app_vm_config.rv32i]
[app_vm_config.rv32m]
[app_vm_config.io]
[app_vm_config.modular]
supported_modulus = ["998244353","1000000007"]

[app_vm_config.fp2]
supported_modulus = ["998244353","1000000007"]

OpenVM ECC

The OpenVM Elliptic Curve Cryptography Extension provides support for elliptic curve operations through the openvm-ecc-guest crate.

Available traits and methods

  • Group trait: This represents an element of a group where the operation is addition. Therefore the trait includes functions for add, sub, and double.

    • IDENTITY is the identity element of the group.
  • CyclicGroup trait: It's a group that has a generator, so it defines GENERATOR and NEG_GENERATOR.

  • WeierstrassPoint trait: It represents an affine point on a Weierstrass elliptic curve and it extends Group.

    • Coordinate type is the type of the coordinates of the point, and it implements IntMod.
    • x(), y() are used to get the affine coordinates
    • from_xy is a constructor for the point, which checks if the point is either identity or on the affine curve.
    • The point supports elliptic curve operations through intrinsic functions add_ne_nonidentity and double_nonidentity.
    • decompress: Sometimes an elliptic curve point is compressed and represented by its x coordinate and the odd/even parity of the y coordinate. decompress is used to decompress the point back to (x, y).
  • msm: for multi-scalar multiplication.

  • ecdsa: for doing ECDSA signature verification and public key recovery from signature.

Macros

For elliptic curve cryptography, the openvm-ecc-guest crate provides macros similar to those in openvm-algebra-guest:

  1. Declare: Use sw_declare! to define elliptic curves over the previously declared moduli. For example:
#![allow(unused)]
fn main() {
sw_declare! {
    Bls12_381G1Affine { mod_type = Bls12_381Fp, b = BLS12_381_B },
    Bn254G1Affine { mod_type = Bn254Fp, b = BN254_B },
}
}

Each declared curve must specify the mod_type (implementing IntMod) and a constant b for the Weierstrass curve equation \(y^2 = x^3 + b\). This creates Bls12_381G1Affine and Bn254G1Affine structs which implement the Group and WeierstrassPoint traits. The underlying memory layout of the structs uses the memory layout of the Bls12_381Fp and Bn254Fp structs, respectively.

  1. Init: Called once, it enumerates these curves and allows the compiler to produce optimized instructions:
#![allow(unused)]
fn main() {
sw_init! {
    Bls12_381Fp, Bn254Fp,
}
}
  1. Setup: Similar to the moduli and complex extensions, runtime setup instructions ensure that the correct curve parameters are being used, guaranteeing secure operation.

Summary:

  • sw_declare!: Declares elliptic curve structures.
  • sw_init!: Initializes them once, linking them to the underlying moduli.
  • setup_sw_<i>()/setup_all_curves(): Secures runtime correctness.

To use elliptic curve operations on a struct defined with sw_declare!, it is expected that the struct for the curve's coordinate field was defined using moduli_declare!. In particular, the coordinate field needs to be initialized and set up as described in the algebra extension chapter.

For the basic operations provided by the WeierstrassPoint trait, the scalar field is not needed. For the ECDSA functions in the ecdsa module, the scalar field must also be declared, initialized, and set up.

Example program

See a working example here.

To use the ECC extension, add the following dependencies to Cargo.toml:

openvm-algebra-guest = { git = "https://github.com/openvm-org/openvm.git" }
openvm-ecc-guest = { git = "https://github.com/openvm-org/openvm.git", features = ["k256"] }

One can define their own ECC structs but we will use the Secp256k1 struct from openvm-ecc-guest and thus the k256 feature should be enabled.

#![allow(unused)]
fn main() {
use openvm_ecc_guest::{
    k256::{Secp256k1Coord, Secp256k1Point, Secp256k1Scalar},
    Group, weierstrass::WeierstrassPoint,
};

openvm_algebra_guest::moduli_setup::moduli_init! {
    "0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F",
    "0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141"
}

openvm_ecc_guest::sw_setup::sw_init! {
    Secp256k1Coord,
}
}

We moduli_init! both the coordinate and scalar field because they were declared in the k256 module, although we will not be using the scalar field below.

With the above we can start doing elliptic curve operations like adding points:

pub fn main() {
    setup_all_moduli();
    setup_all_curves();
    let x1 = Secp256k1Coord::from_u32(1);
    let y1 = Secp256k1Coord::from_le_bytes(&hex!(
        "EEA7767E580D75BC6FDD7F58D2A84C2614FB22586068DB63B346C6E60AF21842"
    ));
    let p1 = Secp256k1Point::from_xy_nonidentity(x1, y1).unwrap();

    let x2 = Secp256k1Coord::from_u32(2);
    let y2 = Secp256k1Coord::from_le_bytes(&hex!(
        "D1A847A8F879E0AEE32544DA5BA0B3BD1703A1F52867A5601FF6454DD8180499"
    ));
    let p2 = Secp256k1Point::from_xy_nonidentity(x2, y2).unwrap();

    let p3 = &p1 + &p2;
}

Config parameters

For the guest program to build successfully, all used moduli and curves must be declared in the .toml config file in the following format:

[app_vm_config.modular]
supported_modulus = ["115792089237316195423570985008687907853269984665640564039457584007908834671663", "115792089237316195423570985008687907852837564279074904382605163141518161494337"]

[[app_vm_config.ecc.supported_curves]]
modulus = "115792089237316195423570985008687907853269984665640564039457584007908834671663"
scalar = "115792089237316195423570985008687907852837564279074904382605163141518161494337"
a = "0"
b = "7"

The supported_modulus parameter is a list of moduli that the guest program will use. The ecc.supported_curves parameter is a list of supported curves that the guest program will use. They must be provided in decimal format in the .toml file. For multiple curves create multiple [[app_vm_config.ecc.supported_curves]] sections.

Elliptic Curve Pairing

The pairing extension enables usage of the optimal Ate pairing check on the BN254 and BLS12-381 elliptic curves. The following field extension tower for \(\mathbb{F}_{p^{12}}\) is used for pairings in this crate:

$$ \mathbb{F_{p^2}} = \mathbb{F_{p}}[u]/(u^2 - \beta)\\ \mathbb{F_{p^6}} = \mathbb{F_{p^2}}[v]/(v^3 - \xi)\\ \mathbb{F_{p^{12}}} = \mathbb{F_{p^6}}[w]/(w^2 - v) $$

A full guest program example is available here: pairing_check.rs

Guest program setup

We'll be working with an example using the BLS12-381 elliptic curve. This is in addition to the setup that needs to be done in the Writing a Program section.

In the guest program, we will import the PairingCheck and IntMod traits, along with the BLS12-381 curve structs (IMPORTANT: this requires the bls12_381 feature enabled in Cargo.toml for the openvm-pairing-guest dependency), and a few other values that we will need:

#![allow(unused)]
fn main() {
use openvm_pairing_guest::{
    pairing::PairingCheck,
    bls12_381::{Bls12_381, Fp, Fp2},
};
use openvm_ecc_guest::AffinePoint;
use openvm_algebra_guest::IntMod;
use openvm::io::read;
}

Additionally, we'll need to initialize our moduli and Fp2 struct via the following macros. For a more in-depth description of these macros, please see the OpenVM Algebra section.

#![allow(unused)]
fn main() {
// These correspond to the BLS12-381 coordinate and scalar moduli, respectively
openvm_algebra_moduli_setup::moduli_init! {
    "0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab",
    "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"
}

openvm_algebra_complex_macros::complex_init! {
    Bls12_381Fp2 { mod_idx = 0 },
}
}

And we'll run the required setup functions at the top of the guest program's main() function:

#![allow(unused)]
fn main() {
setup_0();
setup_all_complex_extensions();
}

There are two moduli defined internally in the Bls12_381 feature. The moduli_init! macro thus requires both of them to be initialized. However, we do not need the scalar field of BLS12-381 (which is at index 1), and thus we only initialize the modulus from index 0, thus we only use setup_0() (as opposed to setup_all_moduli(), which will save us some columns when generating the trace).

Input values

The inputs to the pairing check are AffinePoints in \(\mathbb{F}_p\) and \(\mathbb{F}_{p^2}\). They can be constructed via the AffinePoint::new function, with the inner Fp and Fp2 values constructed via various from_... functions.

We can create a new struct to hold these AffinePoints for the purpose of this guide. You may instead put them into a custom struct to serve your use case.

#![allow(unused)]
fn main() {
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct PairingCheckInput {
    p0: AffinePoint<Fp>,
    p1: AffinePoint<Fp2>,
    q0: AffinePoint<Fp>,
    q1: AffinePoint<Fp2>,
}
}

Pairing check

Most users that use the pairing extension will want to assert that a pairing is valid (the final exponentiation equals one). With the PairingCheck trait imported from the previous section, we have access to the pairing_check function on the Bls12_381 struct. After reading in the input struct, we can use its values in the pairing_check:

#![allow(unused)]
fn main() {
let res = Bls12_381::pairing_check(
    &[p0, p1],
    &[q0, q1],
);
assert!(res.is_ok())
}

Additional functionality

We also have access to each of the specific functions that the pairing check utilizes for either the BN254 or BLS12-381 elliptic curves.

Multi-Miller loop

The multi-Miller loop requires the MultiMillerLoop trait can also be ran separately via:

#![allow(unused)]
fn main() {
let f = Bls12_381::multi_miller_loop(
    &[p0, p1],
    &[q0, q1],
);
}

Running via CLI

Config parameters

For the guest program to build successfully, we'll need to create an openvm.toml configuration file somewhere. It contains all of the necessary configuration information for enabling the OpenVM components that are used in the pairing check.

# openvm.toml
[app_vm_config.pairing]
supported_curves = ["Bls12_381"]

[app_vm_config.modular]
supported_modulus = [
    "4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787",
]

[app_vm_config.fp2]
supported_modulus = [
    "4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787",
]

Also note that since this is a complicated computation, the keygen step requires quite a lot of memory. Run it with RUST_MIN_STACK set to a large value, e.g.

RUST_MIN_STACK=8388608 cargo openvm keygen

Full example code

This example code contains hardcoded values and no inputs as an example that can be run via the CLI.

#![no_main]
#![no_std]

use hex_literal::hex;
use openvm_algebra_guest::{field::FieldExtension, IntMod};
use openvm_ecc_guest::AffinePoint;
use openvm_pairing_guest::{
    bls12_381::{Bls12_381, Fp, Fp2},
    pairing::PairingCheck,
};

openvm::entry!(main);

openvm_algebra_moduli_setup::moduli_init! {
    "0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab",
    "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"
}

openvm_algebra_complex_macros::complex_init! {
    Bls12_381Fp2 { mod_idx = 0 },
}

pub fn main() {
    setup_0();
    setup_all_complex_extensions();

    let p0 = AffinePoint::new(
        Fp::from_be_bytes(&hex!("17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb")),
        Fp::from_be_bytes(&hex!("08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1"))
    );
    let p1 = AffinePoint::new(
        Fp2::from_coeffs([
            Fp::from_be_bytes(&hex!("1638533957d540a9d2370f17cc7ed5863bc0b995b8825e0ee1ea1e1e4d00dbae81f14b0bf3611b78c952aacab827a053")),
            Fp::from_be_bytes(&hex!("0a4edef9c1ed7f729f520e47730a124fd70662a904ba1074728114d1031e1572c6c886f6b57ec72a6178288c47c33577"))
        ]),
        Fp2::from_coeffs([
            Fp::from_be_bytes(&hex!("0468fb440d82b0630aeb8dca2b5256789a66da69bf91009cbfe6bd221e47aa8ae88dece9764bf3bd999d95d71e4c9899")),
            Fp::from_be_bytes(&hex!("0f6d4552fa65dd2638b361543f887136a43253d9c66c411697003f7a13c308f5422e1aa0a59c8967acdefd8b6e36ccf3"))
        ]),
    );
    let q0 = AffinePoint::new(
        Fp::from_be_bytes(&hex!("0572cbea904d67468808c8eb50a9450c9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e")),
        Fp::from_be_bytes(&hex!("166a9d8cabc673a322fda673779d8e3822ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28"))
    );
    let q1 = AffinePoint::new(
        Fp2::from_coeffs([
            Fp::from_be_bytes(&hex!("024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8")),
            Fp::from_be_bytes(&hex!("13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e"))
        ]),
        Fp2::from_coeffs([
            Fp::from_be_bytes(&hex!("0ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801")),
            Fp::from_be_bytes(&hex!("0606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be"))
        ]),
    );

    let res = Bls12_381::pairing_check(&[p0, -q0], &[p1, q1]);
    assert!(res.is_ok());
}

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 below, 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.

#![allow(unused)]
fn main() {
use std::{fs, sync::Arc};
use eyre::Result;
use openvm::platform::memory::MEM_SIZE;
use openvm_build::{GuestOptions, TargetFilter};
use openvm_native_recursion::halo2::utils::CacheHalo2ParamsReader;
use openvm_sdk::{
    config::{AggConfig, AppConfig, SdkVmConfig},
    prover::AppProver,
    Sdk, StdIn,
};
use openvm_stark_sdk::config::FriParameters;
use openvm_transpiler::elf::Elf;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct SomeStruct {
    pub a: u64,
    pub b: u64,
}
}

Building and Transpiling a Program

The SDK provides lower-level control over the building and transpiling process.

#![allow(unused)]
fn main() {
// 1. Build the VmConfig with the extensions needed.
let sdk = Sdk;
let vm_config = SdkVmConfig::builder()
    .system(Default::default())
    .rv32i(Default::default())
    .rv32m(Default::default())
    .io(Default::default())
    .build();

// 2a. Build the ELF with guest options and a target filter.
let target_path = "your_path_project_root";
let guest_opts = GuestOptions::default();
let target_filter = TargetFilter {
    name: target_path.to_string(),
    kind: "bin".to_string(),
};
let elf = sdk.build(guest_opts, target_path, &Some(target_filter))?;
// 2b. Load the ELF from a file
let elf_bytes = fs::read("your_path_to_elf")?;
let elf = Elf::decode(&elf_bytes, MEM_SIZE as u32)?;

// 3. Transpile the ELF into a VmExe
let exe = sdk.transpile(elf, vm_config.transpiler())?;
}

Using SdkVmConfig

The SdkVmConfig struct allows you to specify the extensions and system configuration your VM will use. To customize your own configuration, you can use the SdkVmConfig::builder() method and set the extensions and system configuration you want.

Running a Program

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

#![allow(unused)]
fn main() {
// 4. 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);

// 5. Run the program
let output = sdk.execute(exe.clone(), vm_config.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 CLI Bytes
To get the VM byte representation of a serializable struct data (i.e. for use in the CLI), you can print out the result of openvm::serde::to_vec(data).unwrap() in a Rust host program.

Generating Proofs

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

#![allow(unused)]
fn main() {
// 6. Set app configuration
let app_log_blowup = 2;
let app_fri_params = FriParameters::standard_with_100_bits_conjectured_security(app_log_blowup);
let app_config = AppConfig::new(app_fri_params, vm_config);

// 7. Commit the exe
let app_committed_exe = sdk.commit_app_exe(app_fri_params, exe)?;

// 8. Generate an AppProvingKey
let app_pk = Arc::new(sdk.app_keygen(app_config)?);

// 9a. Generate a proof
let proof = sdk.generate_app_proof(app_pk.clone(), app_committed_exe.clone(), stdin.clone())?;
// 9b. Generate a proof with an AppProver with custom fields
let app_prover = AppProver::new(app_pk.app_vm_pk.clone(), app_committed_exe.clone())
    .with_program_name("test_program");
let proof = app_prover.generate_app_proof(stdin.clone());
}

Verifying Proofs

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

#![allow(unused)]
fn main() {
// 10. Verify your program
let app_vk = app_pk.get_vk();
sdk.verify_app_proof(&app_vk, &proof)?;
}

End-to-end EVM Proof Generation and Verification

Generating and verifying an EVM proof is an extension of the above process.

#![allow(unused)]
fn main() {
// 11. Generate the aggregation proving key
const DEFAULT_PARAMS_DIR: &str = concat!(env!("HOME"), "/.openvm/params/");
let halo2_params_reader = CacheHalo2ParamsReader::new(DEFAULT_PARAMS_DIR);
let agg_config = AggConfig::default();
let agg_pk = sdk.agg_keygen(agg_config, &halo2_params_reader)?;

// 12. Generate the SNARK verifier contract
let verifier = sdk.generate_snark_verifier_contract(&halo2_params_reader, &agg_pk)?;

// 13. Generate an EVM proof
let proof = sdk.generate_evm_proof(
    &halo2_params_reader,
    app_pk,
    app_committed_exe,
    agg_pk,
    stdin,
)?;

// 14. Verify the EVM proof
let success = sdk.verify_evm_proof(&verifier, &proof);
assert!(success);
}

⚠️ WARNING
Generating an EVM proof will require a substantial amount of computation and memory. If you have run cargo openvm setup and don't need a specialized aggregation configuration, consider deserializing the proving key from the file ~/.openvm/agg.pk instead of generating it.

⚠️ WARNING
The aggregation proving key agg_pk above is large. Avoid cloning it if possible.

Note that DEFAULT_PARAMS_DIR is the directory where Halo2 parameters are stored by the cargo openvm setup CLI command. For more information on the setup process, see the EVM Level section of the verify doc.

⚠️ WARNING
cargo openvm setup requires very large amounts of computation and memory (~200 GB).

Creating a New Extension

Extensions in OpenVM let you introduce additional functionality without disrupting the existing system. Consider, for example, an extension that provides two new operations on u32 values: one that squares a number, and another that multiplies it by three. With such an extension:

  1. You define functions like square(x: u32) -> u32 and mul3(x: u32) -> u32 for use in guest code.
  2. When the compiler encounters these functions, it generates corresponding custom RISC-V instructions.
  3. During the transpilation phase, these custom instructions are translated into OpenVM instructions.
  4. At runtime, the OpenVM program sends these new instructions to a specialized chip that computes the results and ensures their correctness.

This modular architecture means the extension cleanly adds new capabilities while leaving the rest of OpenVM untouched. The entire system, including the extension’s operations, can still be proven correct.

Conceptually, a new extension consists of three parts:

  • Guest: High-level Rust code that defines and uses the new operations.
  • Transpiler: Logic that converts custom RISC-V instructions into corresponding OpenVM instructions.
  • Circuit: The special chips that enforce correctness of instruction execution through polynomial constraints.

Guest

In the guest component, your goal is to produce the instructions that represent the new operations. When you want to perform an operation (for example, “calculate this new function of these arguments and write the result here”), you generate a custom instruction. You can use the helper macros custom_insn_r! and custom_insn_i! from openvm_platform to ensure these instructions follow the RISC-V specification. For more details, see the RISC-V contributor documentation.

Transpiler

The transpiler maps the newly introduced RISC-V instructions to their OpenVM counterparts. To achieve this, implement a struct that provides the TranspilerExtension trait, which includes:

#![allow(unused)]
fn main() {
fn process_custom(&self, instruction_stream: &[u32]) -> Option<(Instruction<F>, usize)>;
}

This function checks if the given instruction stream starts with one of your custom instructions. If so, it returns the corresponding OpenVM instruction and how many 32-bit words were consumed. If not, it returns None.

Note that almost always the valid instruction consists of a single 32-bit RISC-V word (so whenever Some(_, sz) is returned, sz is 1), but in general this may not be the case.

Circuit

The circuit component is where the extension’s logic is enforced in a zero-knowledge proof context. Here, you create a chip that:

  • Implements the computing logic, so that the output always corresponds to the correct result of the new operation. The chip has access to the memory shared with the other chips from the VM via our special architecture.
  • Properly constrains all the inputs, outputs and intermediate variables using polynomial equations in such a way that there is no way to fill these variables with values that correspond to an incorrect output while fitting the constraints.

For more technical details on writing circuits and constraints, consult the OpenVM contributor documentation, which provides specifications and guidelines for integrating your extension into the OpenVM framework.