Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 and SHA2-256 hash functions
    • Int256 arithmetic
    • Modular arithmetic over arbitrary fields
    • Elliptic curve operations, including multi-scalar multiplication and ECDSA signature verification, including for the secp256k1 and secp256r1 curves
    • 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 June 2025, OpenVM v1.2.0 and later are recommended for production use. OpenVM completed an external audit on Cantina from January to March 2025 as well as an internal audit by members of the Axiom team during the same timeframe.

📖 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.

Begin the installation:

cargo +1.85 install --locked --git http://github.com/openvm-org/openvm.git --tag v1.3.0 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, clone the repository and begin the installation.

git clone --branch v1.3.0 --single-branch https://github.com/openvm-org/openvm.git
cd openvm
cargo install --locked --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-2025-02-14
rustup component add rust-src --toolchain nightly-2025-02-14

Quickstart

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

Setup

First, create a new Rust project.

cargo openvm init fibonacci

This will generate an OpenVM-specific starter package. Notice Cargo.toml has the following dependency:

[dependencies]
openvm = { git = "https://github.com/openvm-org/openvm.git", tag = "v1.3.0", 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_u32};

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_u32(a as u32, 0);
    reveal_u32((a >> 32) as u32, 1);
}

Build

To build the program, run:

cargo openvm build

This will output an OpenVM executable file to ./target/openvm/release/fibonacci.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 ./target/openvm/app.pk and a verification key to ./target/openvm/app.vk.

Proof Generation

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

cargo openvm prove app --input "0x010A00000000000000"

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 = 10u64 in little endian. Note that this value must be padded to exactly 8 bytes (64 bits) and is prefixed with 0x01 to indicate that the input is composed of raw bytes.

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

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 "0x010A00000000000000"

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 see the build and run docs.

Inputs

The --input field needs to either be a single hex string or a file path to a json file that contains the key input and an array of hex strings. Also note that if you need to provide multiple input streams, you have to use the file path option. Each hex string (either in the file or as the direct input) is either:

  • Hex string of bytes, which is prefixed with 0x01
  • Hex string of native field elements (represented as u32, little endian), prefixed with 0x02

If you are providing input for a struct of type T that will be deserialized by the openvm::io::read() function, then the corresponding hex string should be prefixed by 0x01 followed by the serialization of T into bytes according to openvm::serde::to_vec. The serialization will serialize primitive types (e.g., u8, u16, u32, u64) into little-endian bytes. All serialized bytes are zero-padded to a multiple of 4 byte length. For more details on how to serialize complex types into a VM-readable format, see the Using StdIn section of the SDK doc.

Generating a Proof

To generate a proof, you first need to generate a proving and verifying key:

cargo openvm keygen

If you are using custom VM extensions, this will depend on the openvm.toml file which encodes the VM extension configuration; see the custom extensions docs for more information about openvm.toml. 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

To initialize an OpenVM guest program package, you can use the following CLI command:

cargo openvm init

For a guest program example, see this fibonacci program. More examples can be found in the benchmarks/guest directory.

Handling I/O

The program can take input from stdin, with some functions provided by openvm::io. Make sure to import the openvm library crate to use openvm intrinsic functions.

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_bytes32 sets the user public values in 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.

⚠️ WARNING

The maximum memory address for an OpenVM program is 2^29. The majority of that (approximately 480-500 MB depending on transpilation) is available to the guest program, but large reads may exceed the maximum memory and thus fail.

Rust std library support

OpenVM supports standard Rust written using the std library, with the following limitations that users should be aware of:

  • Standard input (e.g., from console) is not supported. Use the read methods above instead.
  • Standard output and standard error (e.g., println!, eprintln!) are supported and will both print to the host standard output.
  • System randomness calls are supported by default. Important: system randomness requests randomness from the host, and the provided randomness is unvalidated. Users must be aware of this and only use system randomness in settings where this meets their security requirements. In particular, system randomness should not be used for cryptographic purposes.
  • Reading of environmental variables will always return None.
  • Reading of argc and argv will always return 0.

The above applies to the Rust std library. Users should also be aware that when writing a standard Rust program, usage of external crates that use foreign function interfaces (FFI) may not work as expected.

To use the standard library, you must enable the "std" feature in the openvm crate. This is not one of the default features.

Note: If you write a program that only imports openvm in Cargo.toml but does not import it anywhere in your crate, the Rust linker may optimize away the dependency, which will cause a compile error. To fix this, you may need to explicitly import the openvm crate in your code.

When to use std vs no_std

Due to the limitations described above, our general recommendation is that developers should write OpenVM library crates as Rust no_std libraries when possible (see below). Binary crates can generally be written using the standard library, although for more control over the expected behavior, we provide entrypoints for writing no_std binaries.

Writing no_std Rust

OpenVM fully supports no_std Rust. We refer to the Embedded Rust Book for a more detailed introduction to no_std Rust.

no_std library crates

In a library crate, you should add the following to lib.rs to declare your crate as no_std:

#![allow(unused)]
fn main() {
// lib.rs
#![no_std]
}

If you want to feature gate the usage of the standard library, you can do so by adding a "std" feature to your Cargo.toml, where the feature must also enable the "std" feature in the openvm crate:

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

To tell Rust to selectively enable the standard library, add the following to lib.rs (in place of the header above):

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

no_std binary crates

In addition to declaring a binary crate no_std, there is additional handling that must be done around the main function. First, add the following header to main.rs:

#![allow(unused)]
fn main() {
// main.rs
#![no_std]
#![no_main]
}

This tells Rust there is no handler for the main function. OpenVM provides a separate entrypoint for the main function, with panic handler, via the openvm::entry! macro. You should write a main function in the normal way, and add the following to main.rs:

openvm::entry!(main);

fn main() {
    // Your code here
}

If you want to feature gate the usage of the standard library, you can add

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

to Cargo.toml as discussed above. In this case, the main.rs header should be modified to:

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

and you still need the openvm::entry!(main) line. This tells Rust to use the custom main handler when the environment is no_std, but to use the Rust std library and the standard main handler when the feature "std" is enabled.

Building and running

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

Using crates that depend on getrandom

OpenVM is compatible with getrandom v0.2 and v0.3. The cargo openvm CLI will always compile with the custom getrandom backend.

By default the openvm crate has a default feature "getrandom-unsupported" which exports a __getrandom_v03_custom function that always returns Err(Error::UNSUPPORTED). This is enabled by default to allow compilation of guest programs that pull in dependencies which require getrandom but where the executed code does not actually use getrandom functions.

To override the default behavior and provide a custom implementation, turn off the "getrandom-unsupported" feature in the openvm crate and supply your own __getrandom_v03_custom function as specified in the getrandom docs. Similar customization options are available for getrandom v0.2.

Read-only reflection

OpenVM partially supports reflective programming by allowing read-only access to the program code itself during runtime execution. Program code that is modified during runtime will not be executed.

More specifically, data and executable code from the RISC-V ELF are loaded into the initial memory image at the start of runtime execution, and this memory may be freely accessed during execution. However, execution will always run with respect to the initial executable code from the ELF, and all runtime modifications will be ignored.

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. You can run cargo openvm build --help for this list within the command line.

Generally, outputs will always be built to the target directory, which will either be determined by the manifest path or explicitly set using the --target-dir option. By default Cargo sets this to be <workspace_or_package_root>/target/.

OpenVM-specific artifacts will be placed in ${target_dir}/openvm/, but if --output-dir is specified they will be copied to ${output-dir}/ as well.

OpenVM Options

  • --no-transpile

    Description: Skips transpilation into an OpenVM-compatible .vmexe executable when set.

  • --config <CONFIG>

    Description: Path to the OpenVM config .toml file that specifies the VM extensions. By default will search the manifest directory for openvm.toml. If no file is found, OpenVM will use a default configuration. Currently the CLI only supports known extensions listed in the Using Existing Extensions section. To use other extensions, use the SDK.

  • --output_dir <OUTPUT_DIR>

    Description: Output directory for OpenVM artifacts to be copied to. Keys will be placed in ${output-dir}/, while all other artifacts will be in ${output-dir}/${profile}.

  • --init-file-name <INIT_FILE_NAME>

    Description: Name of the generated initialization file, which will be written into the manifest directory.

    Default: openvm_init.rs

Package Selection

As with cargo build, default package selection depends on the working directory. If the working directory is a subdirectory of a specific package, then only that package will be built. Else, all packages in the workspace will be built by default.

  • --package <PACKAGES>

    Description: Builds only the specified packages. This flag may be specified multiple times or as a comma-separated list.

  • --workspace

    Description: Builds all members of the workspace (alias --all).

  • --exclude <PACKAGES>

    Description: Excludes the specified packages. Must be used in conjunction with --workspace. This flag may be specified multiple times or as a comma-separated list.

Target Selection

By default all package libraries and binaries will be built. To build samples or demos under the examples directory, use either the --example or --examples option.

  • --lib

    Description: Builds the package's library.

  • --bin <BIN>

    Description: Builds the specified binary. This flag may be specified multiple times or as a comma-separated list.

  • --bins

    Description: Builds all binary targets.

  • --example <EXAMPLE>

    Description: Builds the specified example. This flag may be specified multiple times or as a comma-separated list.

  • --examples

    Description: Builds all example targets.

  • --all-targets

    Description: Builds all package targets. Equivalent to specifying --lib --bins --examples.

Feature Selection

The following options enable or disable conditional compilation features defined in your Cargo.toml.

  • -F, --features <FEATURES>

    Description: Space or comma separated list of features to activate. Features of workspace members may be enabled with package-name/feature-name syntax. This flag may also be specified multiple times.

  • --all-features

    Description: Activates all available features of all selected packages.

  • --no-default-features

    Description: Do not activate the default feature of the selected packages.

Compilation Options

  • --profile <NAME>

    Description: Builds with the given profile. Common profiles are dev (faster builds, less optimization) and release (slower builds, more optimization). For more information on profiles, see Cargo's reference page.

    Default: release

Output Options

  • --target_dir <TARGET_DIR>

    Description: Directory for all generated artifacts and intermediate files. Defaults to directory target/ at the root of the workspace.

Display Options

  • -v, --verbose

    Description: Use verbose output.

  • -q, --quiet

    Description: Do not print Cargo log messages.

  • --color <WHEN>

    Description: Controls when colored output is used.

    Default: always

Manifest Options

  • --manifest-path <PATH>

    Description: Path to the guest code Cargo.toml file. By default, build searches for the file in the current or any parent directory. The build command will be executed in that directory.

  • --ignore-rust-version

    Description: Ignores rust-version specification in packages.

  • --locked

    Description: Asserts the same dependencies and versions are used as when the existing Cargo.lock file was originally generated.

  • --offline

    Description: Prevents Cargo from accessing the network for any reason.

  • --frozen

    Description: Equivalent to specifying both --locked and --offline.

Running a Program

After building and transpiling a program, you can execute it using the run command. For example, you can call:

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

If --exe is not provided, OpenVM will call build prior to attempting to run the executable. Note that only one executable may be run, so if your project contains multiple targets you will have to specify which one to run using the --bin or --example flag.

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

Run Flags

Many of the options for cargo openvm run will be passed to cargo openvm build if --exe is not specified. For more information on build (or run's Feature Selection, Compilation, Output, Display, and/or Manifest options) see Compiling.

OpenVM Options

  • --exe <EXE>

    Description: Path to the OpenVM executable, if specified build will be skipped.

  • --config <CONFIG>

    Description: Path to the OpenVM config .toml file that specifies the VM extensions. By default will search the manifest directory for openvm.toml. If no file is found, OpenVM will use a default configuration. Currently the CLI only supports known extensions listed in the Using Existing Extensions section. To use other extensions, use the SDK.

  • --output_dir <OUTPUT_DIR>

    Description: Output directory for OpenVM artifacts to be copied to. Keys will be placed in ${output-dir}/, while all other artifacts will be in ${output-dir}/${profile}.

  • --input <INPUT>

    Description: Input to the OpenVM program, or a hex string.

  • --init-file-name <INIT_FILE_NAME>

    Description: Name of the generated initialization file, which will be written into the manifest directory.

    Default: openvm_init.rs

Package Selection

  • --package <PACKAGES>

    Description: The package to run, by default the package in the current workspace.

Target Selection

Only one target may be built and run.

  • --bin <BIN>

    Description: Runs the specified binary.

  • --example <EXAMPLE>

    Description: Runs the specified example.

Examples

Running a Specific Binary

cargo openvm run --bin bin_name

Skipping Build Using --exe

cargo openvm build --output-dir ./my_output_dir
cargo openvm run --exe ./my_output_dir/bin_name.vmexe

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 | stark | evm]

Key Generation

The keygen command generates both an application proving and verification key.

cargo openvm keygen
    --config <path_to_app_config>

Similarly to build, run, and prove, options --manifest-path, --target-dir, and --output-dir are provided.

If --config is not specified, the command will search for openvm.toml in the manifest directory. If the file isn't found, a default configuration will be used.

The proving and verification key will be written to ${target_dir}/openvm/ (and --output-dir if specified).

Proof Generation

The prove CLI command, at its core, uses the options below. prove gets access to all of the options that run has (see Running a Program for more information).

cargo openvm prove [app | stark | evm]
    --app-pk <path_to_app_pk>
    --exe <path_to_transpiled_program>
    --input <path_to_input>
    --proof <path_to_proof_output>

If --app-pk is not provided, the command will search for a proving key at ${target_dir}/openvm/app.pk.

If --exe is not provided, the command will call build before generating a proof.

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

If --proof is not provided then the command will write the proof to ./${bin_name}.[app | stark | evm].proof by default, where bin_name is the file stem of the executable run.

The app subcommand generates an application-level proof, the stark command generates an aggregated root-level proof, while the evm command generates an end-to-end EVM proof. For more information on aggregation, see this specification.

⚠️ 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).

See EVM Proof Format for details on the output format for cargo openvm prove evm.

Commit Hashes

To see the commit hash for an executable, you may run:

cargo openvm commit
    --app-pk <path_to_app_pk>
    --exe <path_to_transpiled_program>

The commit command has all the auxiliary options that prove does, and outputs Bn254 commits for both your executable and VM. Commits are written to ${target_dir}/openvm/ (and --output-dir if specified).

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>

Options --manifest-path, --target-dir are also available to verify. If you omit --app_vk the command will search for the verifying key at ${target_dir}/openvm/app.vk.

If you omit --proof, the command will search the working directory for files with the .app.proof extension. Note that for this default case a single proof is expected to be found, and verify will fail otherwise.

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

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.

Upon a successful run, the command will write the files

  • agg.pk
  • halo2/src/[OPENVM_VERSION]/Halo2Verifier.sol
  • halo2/src/[OPENVM_VERSION]/OpenVmHalo2Verifier.sol
  • halo2/src/[OPENVM_VERSION]/interfaces/IOpenVmHalo2Verifier.sol
  • halo2/src/[OPENVM_VERSION]/verifier.bytecode.json

to ~/.openvm/, where ~ is the directory specified by environment variable $HOME and OPENVM_VERSION is the version of OpenVM. Every command that requires these files will look for them in this directory.

The agg.pk contains all aggregation proving keys necessary for aggregating to a final EVM proof. The OpenVmHalo2Verifier.sol file contains a Solidity contract to verify the final EVM proof. The contract is named OpenVmHalo2Verifier and it implements the IOpenVmHalo2Verifier interface.

interface IOpenVmHalo2Verifier {
    function verify(bytes calldata publicValues, bytes calldata proofData, bytes32 appExeCommit, bytes32 appVmCommit)
        external
        view;
}

In addition, the command outputs a JSON file verifier.bytecode.json of the form

{
    "sol_compiler_version": "0.8.19",
    "sol_compiler_options": "",
    "bytecode": "0x..."
}

where sol_compiler_version is the Solidity compiler version used to compile the contract (currently fixed to 0.8.19), sol_compiler_options are additional compiler options used, and bytecode is the compiled EVM bytecode as a hex string.

⚠️ WARNING

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

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

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 a file with extension .evm.proof in the working directory.

EVM Proof: JSON Format

The EVM proof is written as a JSON of the following format:

{
  "app_exe_commit": "0x..",
  "app_vm_commit": "0x..",
  "user_public_values": "0x..",
  "proof_data": {
    "accumulator": "0x..",
    "proof": "0x.."
  },
}

where each field is a hex string. We explain what each field represents:

  • app_exe_commit: 32 bytes for the commitment of the app executable.
  • app_vm_commit: 32 bytes for the commitment of the app VM configuration.
  • user_public_values: concatenation of 32 byte chunks for user public values. The number of user public values is a configuration parameter.
  • accumulator: 12 * 32 bytes representing the KZG accumulator of the proof, where the proof is from a SNARK using the KZG commitment scheme.
  • proof: The rest of the proof required by the SNARK as a hex string of 43 * 32 bytes.

EVM Proof: Calldata Format

The cargo openvm verify evm command reads the EVM proof from JSON file and then simulates the call to the verifier contract using Revm. This function should only be used for testing and development purposes but not for production.

To verify the EVM proof in an EVM execution environment, the entries of the JSON can be passed as function arguments for the verify function, where the proofData argument is constructed by proofData = abi.encodePacked(accumulator, proof).

Solidity SDK

As a supplement to OpenVM, we provide a Solidity SDK containing OpenVM verifier contracts generated at official release commits using the cargo openvm setup command. The contracts are built at every minor release as OpenVM guarantees verifier backward compatibility across patch releases.

Note that these builds are for the default aggregation VM config which should be sufficient for most users. If you use a custom config, you will need to manually generate the verifier contract using the OpenVM SDK.

Installation

To install the Solidity SDK as a dependency into your forge project, run the following command:

forge install openvm-org/openvm-solidity-sdk

Usage

Once you have the SDK installed, you can import the SDK contracts into your Solidity project:

import "openvm-solidity-sdk/v1.3/OpenVmHalo2Verifier.sol";

If you are using an already-deployed verifier contract, you can simply import the IOpenVmHalo2Verifier interface:

import { IOpenVmHalo2Verifier } from "openvm-solidity-sdk/v1.3/interfaces/IOpenVmHalo2Verifier.sol";

contract MyContract {
    function myFunction() public view {
        // ... snip ...

        IOpenVmHalo2Verifier(verifierAddress)
            .verify(publicValues, proofData, appExeCommit, appVmCommit);

        // ... snip ...
    }
}

The arguments to the verify function are the fields in the EVM Proof JSON Format. Since the builds use the default aggregation VM config, the number of public values is fixed to 32.

If you want to import the verifier contract into your own repository for testing purposes, note that it is locked to Solidity version 0.8.19. If your project uses a different version, the import may not compile. As a workaround, you can compile the contract separately and use vm.etch() to inject the raw bytecode into your tests.

Deployment

To deploy an instance of a verifier contract, you can clone the repo and simply use forge create:

git clone --recursive https://github.com/openvm-org/openvm-solidity-sdk.git
cd openvm-solidity-sdk
forge create src/v1.3/OpenVmHalo2Verifier.sol:OpenVmHalo2Verifier --rpc-url $RPC --private-key $PRIVATE_KEY --broadcast

We recommend a direct deployment from the SDK repo since the proper compiler configurations are all pre-set.

Acceleration Using Pre-Built Extensions

OpenVM ships with a set of pre-built extensions maintained by the OpenVM team. Below, we highlight six of these extensions designed to accelerate common arithmetic and cryptographic operations that are notoriously expensive to execute. Some of these extensions have corresponding guest libraries which provide convenient, high-level interfaces for your guest program to interact with the extension.

Optimizing Modular Arithmetic

Some of these extensions—specifically algebra, ecc, and pairing—perform modular arithmetic, which can be significantly optimized when the modulus is known at compile time. Therefore, these extensions provide a framework to inform the compiler about all the moduli and associated arithmetic structures we intend to use. To achieve this, two 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.

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.

Automating the init! step

The openvm crate provides an init! macro to automate the init step:

  1. Call openvm::init!() exactly once in the code of the final program binary.
  2. When compiling the program, cargo openvm build will read the configuration file to automatically generate the correct init code and write it to <INIT_FILE_NAME>, which defaults to openvm_init.rs in the manifest directory.
  3. The openvm::init!() macro will include the openvm_init.rs file into the final binary to complete the init process. You can call openvm::init!(INIT_FILE_NAME) to include init code from a different file if needed.

Configuration

To use these extensions, you must populate an 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.

A template openvm.toml file using the default VM extensions shipping with OpenVM is as follows:

[app_vm_config.rv32i]

[app_vm_config.rv32m]
range_tuple_checker_sizes = [256, 8192]

[app_vm_config.io]

[app_vm_config.keccak]

[app_vm_config.sha256]

[app_vm_config.native]

[app_vm_config.bigint]
range_tuple_checker_sizes = [256, 8192]

[app_vm_config.modular]
supported_moduli = ["<modulus_1>", "<modulus_2>", ...]

[app_vm_config.fp2]
supported_moduli = ["<modulus_1>", "<modulus_2>", ...]

[app_vm_config.pairing]
supported_curves = ["Bls12_381", "Bn254"]

[[app_vm_config.ecc.supported_curves]]
struct_name = "<curve_name_1>"
modulus = "<modulus_1>"
scalar = "<scalar_1>"
a = "<a_1>"
b = "<b_1>"

[[app_vm_config.ecc.supported_curves]]
struct_name = "<curve_name_2>"
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.

Keccak256

The Keccak256 extension guest 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 intrinsic. This is 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]

SHA-256

The SHA-256 extension guest 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 SHA-256 intrinsic. This is enabled only when the target is zkvm.

  • zkvm_sha256_impl(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 zkvm_sha256_impl(input: *const u8, len: usize, output: *mut u8);
}

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

Config parameters

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

[app_vm_config.sha256]

Big Integers

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.

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.

External Linking

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]

Algebra (Modular Arithmetic)

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-macros 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_le_bytes_unchecked, from_be_bytes_unchecked, from_u8, from_u32, and from_u64.
  • Field trait: Provides constants ZERO and ONE and methods for basic arithmetic operations within a field.

  • Sqrt trait: Implements square root in a field using hinting.

Modular arithmetic

To leverage compile-time known moduli for performance, you declare and initialize 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. Since both moduli are prime, both structs also implement the Field and Sqrt traits. The modulus parameter must be a string literal in decimal or hexadecimal format.

  1. Init: Use the openvm::init! macro exactly once in the final binary:
#![allow(unused)]
fn main() {
openvm::init!();
/* This expands to
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.

Summary:

  • moduli_declare!: Declares modular arithmetic structures and can be done multiple times.
  • init!: Called once in the final binary to assign and lock in the moduli.

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: After calling complex_declare!, the openvm::init! macro will now expand to the appropriate call to complex_init!.
#![allow(unused)]
fn main() {
openvm::init!();
/* This expands to:
moduli_init! {
    "0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab",
    "21888242871839275222246405745257275088696311157297823662689037894645226208583"
}
complex_init! {
    Bn254Fp2 { mod_idx = 0 },
}
*/
}

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_moduli = ["115792089237316195423570985008687907853269984665640564039457584007908834671663"]

[app_vm_config.fp2]
supported_moduli = [["Bn254Fp2", "115792089237316195423570985008687907853269984665640564039457584007908834671663"]]

The supported_moduli parameter is a list of moduli that the guest program will use. They must be provided in decimal format in the .toml file. The order of moduli in [app_vm_config.modular] must match the order in the moduli_init! macro. Similarly, the order of moduli in [app_vm_config.fp2] must match the order in the complex_init! macro. Also, each modulus in [app_vm_config.fp2] must be paired with the name of the corresponding struct in complex_declare!.

Example program

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

extern crate alloc;

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

// 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 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_guest::complex_macros::complex_declare! {
    Complex1 { mod_type = Mod1 },
    Complex2 { mod_type = Mod2 },
}

openvm::init!();
/* The init! macro will expand to the following (excluding comments):
// This macro will initialize the moduli.
// Now, `Mod1` is the "zeroth" modular struct, and `Mod2` is the "first" one.
moduli_init! {
    "998244353", "1000000007"
}

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

pub fn main() {
    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 the above assertions would fail, had 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-algebra-guest = { 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_moduli = ["998244353","1000000007"]

[app_vm_config.fp2]
supported_moduli = [["Complex1", "998244353"], ["Complex2", "1000000007"]]

Elliptic Curve Cryptography

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

Developers can enable arbitrary Weierstrass curves by configuring this extension with the modulus for the coordinate field and the coefficients in the curve equation. Preset configurations for the secp256k1 and secp256r1 curves are provided through the K256 and P256 guest libraries.

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 },
    P256Affine { mod_type = P256Coord, a = P256_A, b = P256_B },
}
}

Each declared curve must specify the mod_type (implementing IntMod) and a constant b for the Weierstrass curve equation \(y^2 = x^3 + ax + b\). a is optional and defaults to 0 for short Weierstrass curves. This creates Bls12_381G1Affine and P256Affine structs which implement the Group and WeierstrassPoint traits. The underlying memory layout of the structs uses the memory layout of the Bls12_381Fp and P256Coord structs, respectively.

  1. Init: Called once, the openvm::init! macro produces a call to sw_init! that enumerates these curves and allows the compiler to produce optimized instructions:
#![allow(unused)]
fn main() {
openvm::init!();
/* This expands to
sw_init! {
    Bls12_381G1Affine, P256Affine,
}
*/
}

Summary:

  • sw_declare!: Declares elliptic curve structures.
  • init!: Initializes them once, linking them to the underlying moduli.

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.

ECDSA

The ECC extension supports ECDSA signature verification on any elliptic curve, and pre-defined implementations are provided for the secp256k1 and secp256r1 curves. To verify an ECDSA signature, first call the VerifyingKey::recover_from_prehash_noverify associated function to recover the verifying key, then call the VerifyingKey::verify_prehashed method on the recovered verifying key.

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) $$

The main feature of the pairing extension is the pairing_check function, which asserts that a product of pairings evaluates to 1. For example, for the BLS12-381 curve,

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

This asserts that \(e(p_0, q_0) e(p_1, q_1) = 1\). Naturally, this can be extended to more points by adding more elements to the arrays.

The pairing extension additionally provides field operations in \(\mathbb{F_{p^{12}}}\) for both BN254 and BLS12-381 curves where \(\mathbb{F}\) is the coordinate field.

See the pairing guest library for usage details.

Keccak256

The OpenVM Keccak256 guest library provides two functions for use 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 core::hint::black_box;

use hex::FromHex;
use openvm_keccak256::keccak256;
openvm::entry!(main);

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 = { git = "https://github.com/openvm-org/openvm.git" }
hex = { version = "0.4.3" }

Config parameters

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

[app_vm_config.keccak]

SHA-2

The OpenVM SHA-2 guest library provides access to a set of accelerated SHA-2 family hash functions. Currently, it supports the following:

  • SHA-256

SHA-256

Refer here for more details on SHA-256.

For SHA-256, the SHA2 guest library provides two functions for use in your guest code:

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

See the full example here.

Example

use core::hint::black_box;

use hex::FromHex;
use openvm_sha2::sha256;
openvm::entry!(main);

pub fn main() {
    let test_vectors = [(
        "",
        "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    )];
    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 = sha256(&black_box(input));
        if output != *expected_output {
            panic!();
        }
    }
}

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

openvm-sha2 = { git = "https://github.com/openvm-org/openvm.git" }
hex = { version = "0.4.3" }

Config parameters

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

[app_vm_config.sha256]

Ruint

The Ruint guest library is a fork of ruint that allows for patching of U256 operations with logic from openvm-bigint-guest.

Example matrix multiplication using U256

See the full example here.

#![allow(clippy::needless_range_loop)]
use core::array;

use openvm_ruint::aliases::U256;

openvm::entry!(main);

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

pub fn get_matrix(val: u32) -> Matrix {
    array::from_fn(|_| array::from_fn(|_| U256::from(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(1u32);
    }
    res
}

pub fn main() {
    let a: Matrix = get_identity_matrix();
    let b: Matrix = get_matrix(28);
    let c: Matrix = mult(&a, &b);
    if c != b {
        panic!("Matrix multiplication failed");
    }
}

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

openvm-ruint = { git = "https://github.com/openvm-org/openvm.git", package = "ruint" }

Example matrix multiplication using I256

See the full example here.

#![allow(clippy::needless_range_loop)]
use core::array;

use alloy_primitives::I256;

openvm::entry!(main);

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

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

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::try_from(1i32).unwrap();
    }
    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-ruint = { git = "https://github.com/openvm-org/openvm.git", package = "ruint" }

Config parameters

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

[app_vm_config.bigint]

K256

The K256 guest library uses openvm-ecc-guest to provide elliptic curve operations over the Secp256k1 curve. It is intended as a patch for the k256 rust crate and can be swapped in for accelerated signature verification usage. Note that signing from a private key is not supported.

Example program

See a working example here.

To use the K256 guest library, 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" }
openvm-k256 = { git = "https://github.com/openvm-org/openvm.git", package = "k256" }

The guest library provides a Secp256k1Coord, which represents a field element on the coordinate field of Secp256k1, and a Secp256k1Point, which represents an Secp256k1 elliptic curve point.

The K256 guest library handles the "Declare" phase described in Optimizing Modular Arithmetic. The consuming guest program is responsible for running the "Init" phase via openvm::init!().

use hex_literal::hex;
use openvm_algebra_guest::IntMod;
use openvm_ecc_guest::weierstrass::WeierstrassPoint;
use openvm_k256::{Secp256k1Coord, Secp256k1Point};
openvm::init!();
/* The init! macro will expand to the following
openvm_algebra_guest::moduli_macros::moduli_init! {
    "0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F",
    "0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141"
}

openvm_ecc_guest::sw_macros::sw_init! {
    Secp256k1Point,
}
*/

moduli_init! is called for 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() {
    let x1 = Secp256k1Coord::from_u32(1);
    let y1 = Secp256k1Coord::from_le_bytes_unchecked(&hex!(
        "EEA7767E580D75BC6FDD7F58D2A84C2614FB22586068DB63B346C6E60AF21842"
    ));
    let p1 = Secp256k1Point::from_xy_nonidentity(x1, y1).unwrap();

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

    #[allow(clippy::op_ref)]
    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_moduli = ["115792089237316195423570985008687907853269984665640564039457584007908834671663", "115792089237316195423570985008687907852837564279074904382605163141518161494337"]

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

The supported_moduli parameter is a list of moduli that the guest program will use. As mentioned in the algebra extension chapter, the order of moduli in [app_vm_config.modular] must match the order in the moduli_init! macro.

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. The order of curves in [[app_vm_config.ecc.supported_curves]] must match the order in the sw_init! macro. Also, the struct_name field must be the name of the elliptic curve struct created by sw_declare!. In this example, the Secp256k1Point struct is created in openvm_ecc_guest::k256.

P256

The P256 guest library uses openvm-ecc-guest to provide elliptic curve operations over the Secp256r1 curve. It is intended as a patch for the p256 rust crate and can be swapped in for accelerated signature verification usage. Note that signing from a private key is not supported.

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_moduli = ["115792089210356248762697446949407573530086143415290314195533631308867097853951", "115792089210356248762697446949407573529996955224135760342422259061068512044369"]

[[app_vm_config.ecc.supported_curves]]
struct_name = "P256Point"
modulus = "115792089210356248762697446949407573530086143415290314195533631308867097853951"
scalar = "115792089210356248762697446949407573529996955224135760342422259061068512044369"
a = "115792089210356248762697446949407573530086143415290314195533631308867097853948"
b = "41058363725152142129326129780047268409114441015993725554835256314039467401291"

The supported_moduli parameter is a list of moduli that the guest program will use. As mentioned in the algebra extension chapter, the order of moduli in [app_vm_config.modular] must match the order in the moduli_init! macro.

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. The order of curves in [[app_vm_config.ecc.supported_curves]] must match the order in the sw_init! macro. Also, the struct_name field must be the name of the elliptic curve struct created by sw_declare!.

Elliptic Curve Pairing

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 dependency), and a few other values that we will need:

use openvm_algebra_guest::{field::FieldExtension, IntMod};
use openvm_ecc_guest::AffinePoint;
use openvm_pairing::{
    bls12_381::{Bls12_381, Fp, Fp2},
    PairingCheck,
};

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.

openvm::init!();
/* The init! macro will expand to the following
openvm_algebra_moduli_macros::moduli_init! {
    "0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab",
    "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"
}

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

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:

    let res = Bls12_381::pairing_check(&[p0, -q0], &[p1, 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 run 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.rv32i]
[app_vm_config.rv32m]
[app_vm_config.io]
[app_vm_config.pairing]
supported_curves = ["Bls12_381"]

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

[app_vm_config.fp2]
supported_moduli = [
    ["Bls12_381Fp2", "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 program

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

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

openvm::init!();
/* The init! macro will expand to the following
openvm_algebra_moduli_macros::moduli_init! {
    "0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab",
    "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"
}

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

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

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

Verify STARK

A library to facilitate the recursive verification of OpenVM STARK proofs within a guest program. The library includes helpers for both the guest to verify the proofs and for the host to supply them.

You can find an example of using the library for recursive verification here.

Guest

For guest programs, the library provides the define_verify_stark_proof! macro which can generate a function to verify an OpenVM STARK proof, directly usable within the guest. Users will need to specify:

  • A function name.
  • An ASM file containing the instructions to verify OpenVM STARK proofs. More specifically, the ASM must verify proofs outputted by the CLI's cargo openvm prove stark (or prove_e2e_stark from the SDK). The verification logic itself is tied to an aggregation config (so the ASM is reusable across OpenVM STARKs with different app VM configs) and therefore, can be generated during aggregation keygen.
    • The SDK provides the helper generate_root_verifier_asm to generate the ASM for a given aggregation config.
    • Since OpenVM maintains verifier compatibility across patch releases, the same ASM is also reusable with STARKs generated across all 1.x.* for some x.

The macro will output a function with the following interface:

#![allow(unused)]
fn main() {
fn verify_stark(app_exe_commit: &[u32; 8], app_vm_commit: &[u32; 8], user_pvs: &[u8])
}

where

  • verify_stark is the user-supplied name of the function.
  • app_exe_commit is the commitment to the OpenVM application executable whose execution is being verified.
  • app_vm_commit is the commitment to the app VM configuration.
  • user_pvs are the public values revealed by the app.

Proofs are expected to be passed via the hintable key-value store. Guests will query for proofs at the key asm_filename || exe_commit_in_u32 || vm_commit_in_u32 || user_pvs.

The function will panic on failure. Successful STARK verification implies that the app execution was successful and terminated with exit code 0.

⚠️ For Advanced Users

Note that if your guest program directly writes data to the native address space (address space 4), the verify_stark function will likely overwrite it. Any data the guest placed in the native address space should be persisted (or treated as corrupt) before invocations to verify_stark.

This is not a concern if you are writing vanilla rust with the default RV32 I and M extensions.

Host

Hosts are expected to properly hint the hintable key-value store with the OpenVM STARK proofs. To that end, the library provides the following utilities:

  • compute_hint_key_for_verify_openvm_stark will compute the exact key at which the guest will look for the proof.
  • encode_proof_to_kv_store_value will serialize the proof into the structure expected by the verify_stark function.

Using the SDK

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

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

Imports and Setup

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

use std::{fs, sync::Arc};

use eyre::Result;
use openvm::platform::memory::MEM_SIZE;
use openvm_build::GuestOptions;
use openvm_sdk::{
    config::{AppConfig, SdkVmConfig},
    prover::AppProver,
    Sdk, StdIn,
};
use openvm_stark_sdk::config::{baby_bear_poseidon2::BabyBearPoseidon2Engine, 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.

    // 1. Build the VmConfig with the extensions needed.
    let sdk = Sdk::new();

    // 2a. Build the ELF with guest options and a target filter.
    let guest_opts = GuestOptions::default();
    let target_path = "your_path_project_root";
    let elf = sdk.build(
        guest_opts,
        &vm_config,
        target_path,
        &Default::default(),
        None,
    )?;
    // 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.

    let vm_config = SdkVmConfig::builder()
        .system(Default::default())
        .rv32i(Default::default())
        .rv32m(Default::default())
        .io(Default::default())
        .build();

ℹ️ When using Rust to write the guest program, the VM system configuration should keep the default value pointer_max_bits = 29 to match the hardcoded memory limit of the memory allocator. Otherwise, the guest program may fail due to out of bounds memory access in the VM.

Running a Program

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

    // 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 and Verifying Proofs

There are two types of proofs that you can generate, with the sections below continuing from this point.

  • App Proof: Generates STARK proof(s) of the guest program
  • EVM Proof: Generates a halo2 proof that can be posted on-chain

App Proof

Generating App Proofs

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

    // 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::<_, BabyBearPoseidon2Engine>::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());

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

Verifying App Proofs

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

    // 10. Verify your program
    let app_vk = app_pk.get_app_vk();
    sdk.verify_app_proof(&app_vk, &proof)?;

EVM Proof

Setup

To generate an EVM proof, you'll first need to ensure that you have followed the CLI installation steps. get the appropriate KZG params by running the following command.

cargo openvm setup

⚠️ WARNING

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

Also note that there are additional dependencies for the EVM Proof flow. Click here to view.
use std::{fs, sync::Arc};

use eyre::Result;
use openvm::platform::memory::MEM_SIZE;
use openvm_build::GuestOptions;
use openvm_sdk::{
    config::{AppConfig, SdkVmConfig},
    prover::AppProver,
    Sdk, StdIn,
};
use openvm_stark_sdk::config::{baby_bear_poseidon2::BabyBearPoseidon2Engine, FriParameters};
use openvm_transpiler::elf::Elf;
use serde::{Deserialize, Serialize};

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

Keygen

Now, you'll need to generate the app proving key for the next step.

    // 5. 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);

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

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

⚠️ WARNING

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, to save computation.

EVM Proof Generation and Verification

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

Note: you do not need to generate the app proof with the generate_app_proof function, as the EVM proof function will handle this automatically.

    // 8. 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,
        &DefaultStaticVerifierPvHandler,
    )?;

    // 9. Generate the SNARK verifier smart contract
    let verifier = sdk.generate_halo2_verifier_solidity(&halo2_params_reader, &agg_pk)?;

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

    // 11. Verify the EVM proof
    sdk.verify_evm_halo2_proof(&verifier, proof)?;

⚠️ 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.

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.

Recursive Verification

OpenVM supports recursively verifying its own proofs using the Verify STARK guest library. See its dedicated page to learn more.