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:
- Getting started.
- Writing applications in Rust targeting OpenVM and generating proofs.
- Using existing extensions to optimize your Rust programs.
- How to add custom VM extensions.
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.
Option 1: Install Via Git URL (Recommended)
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 inmy_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 theevm
subcommand, you must have previously called the costlycargo 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:
openvm-keccak-guest
- Keccak256 hash function.openvm-bigint-guest
- Big integer arithmetic for 256-bit signed and unsigned integers.openvm-algebra-guest
- Modular arithmetic and complex field extensions.openvm-ecc-guest
- Elliptic curve cryptography.openvm-pairing-guest
- Elliptic curve optimal Ate pairings.
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:
- Declare: Introduce a modular arithmetic or related structure, along with its modulus and functionality. This can be done in any library or binary file.
- 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.
- 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 hasC
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 aU256
.MIN
: The minimum value of aU256
.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 aI256
.MIN
: The minimum value of aI256
.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. Returnstrue
ifa == b
, otherwisefalse
.zkvm_u256_cmp_impl(a: *const u8, b: *const u8) -> Ordering
: takes in two pointers to the inputs. Returns the ordering ofa
andb
.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 typeRepr
and constantsMODULUS
,NUM_LIMBS
,ZERO
, andONE
. 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
andONE
represent the additive and multiplicative identities, respectively.- Constructors include
from_repr
,from_le_bytes
,from_be_bytes
,from_u8
,from_u32
, andfrom_u64
.
-
Field
trait: Provides constantsZERO
andONE
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:
- 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.
- 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.
- 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()
orsetup_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!
:
- 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
.
- 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!
- Setup: Similar to moduli, call
setup_complex_<i>()
orsetup_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 foradd
,sub
, anddouble
.IDENTITY
is the identity element of the group.
-
CyclicGroup
trait: It's a group that has a generator, so it definesGENERATOR
andNEG_GENERATOR
. -
WeierstrassPoint
trait: It represents an affine point on a Weierstrass elliptic curve and it extendsGroup
.Coordinate
type is the type of the coordinates of the point, and it implementsIntMod
.x()
,y()
are used to get the affine coordinatesfrom_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
anddouble_nonidentity
. decompress
: Sometimes an elliptic curve point is compressed and represented by itsx
coordinate and the odd/even parity of they
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
:
- 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.
- Init: Called once, it enumerates these curves and allows the compiler to produce optimized instructions:
#![allow(unused)] fn main() { sw_init! { Bls12_381Fp, Bn254Fp, } }
- 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 AffinePoint
s 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 AffinePoint
s 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 structdata
(i.e. for use in the CLI), you can print out the result ofopenvm::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 runcargo 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 keyagg_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:
- You define functions like
square(x: u32) -> u32
andmul3(x: u32) -> u32
for use in guest code. - When the compiler encounters these functions, it generates corresponding custom RISC-V instructions.
- During the transpilation phase, these custom instructions are translated into OpenVM instructions.
- 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.