Using the zkVM
SDK Quick Start

SDK Quick Start

1. Install the Nexus zkVM

First, install Rust: Rust Install Instructions (opens in a new tab).

Also, make sure you have a working version of cmake (opens in a new tab).

Next, install the RISC-V target:

rustup target add riscv32i-unknown-none-elf

Then, install the Nexus zkVM:

cargo install --git https://github.com/nexus-xyz/nexus-zkvm cargo-nexus --tag 'v0.2.3'

Verify the installation:

cargo nexus --help

This should print the available CLI commands.

2. Create a new Nexus host project

To use the zkVM programmatically, we need two programs: a guest program that runs on the zkVM, and a host program that operates the zkVM itself.

Somewhere out of the nexus-zkvm checkout, run:

cargo nexus host nexus-host

This will create a new Rust project directory with the following structure:

./nexus-host
├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs
    └── guest
        └── Cargo.toml
        └── rust-toolchain.toml
        └── src
            └── main.rs

Here, ./src/main.rs is our host program, while ./src/guest/src/main.rs is our guest program.

As an example, you can change the content of ./src/guest/src/main.rs to:

#![cfg_attr(target_arch = "riscv32", no_std, no_main)]
 
use nexus_rt::{println, read_private_input, write_output};
 
#[nexus_rt::main]
fn main() {
    let input = read_private_input::<(u32, u32)>();
 
    let mut z: i32 = -1;
    if let Ok((x, y)) = input {
        println!("Read private input: ({}, {})", x, y);
 
        z = (x * y) as i32;
    } else {
        println!("No private input provided...");
    }
 
    write_output::<i32>(&z)
}

This guest program tries to read two integers off the input tape, logs whether they exist, and then returns their product if they do.

Then, change the content of ./src/main.rs to:

use nexus_sdk::{
    compile::CompileOpts,
    nova::seq::{Generate, Nova, PP},
    Local, Prover, Verifiable,
};
 
type Input = (u32, u32);
type Output = i32;
 
const PACKAGE: &str = "guest";
 
fn main() {
    println!("Setting up Nova public parameters...");
    let pp: PP = PP::generate().expect("failed to generate parameters");
 
    let mut opts = CompileOpts::new(PACKAGE);
    opts.set_memlimit(8); // use an 8mb memory
 
    println!("Compiling guest program...");
    let prover: Nova<Local> = Nova::compile(&opts).expect("failed to compile guest program");
 
    let input: Input = (3, 5);
 
    print!("Proving execution of vm...");
    let proof = prover
        .prove_with_input::<Input>(&pp, &input)
        .expect("failed to prove program");
 
    println!(
        " output is {}!",
        proof
            .output::<Output>()
            .expect("failed to deserialize output")
    );
 
    println!(">>>>> Logging\n{}<<<<<", proof.logs().join("\n"));
 
    print!("Verifying execution...");
    proof.verify(&pp).expect("failed to verify proof");
 
    println!("  Succeeded!");
}

This host program compiles the guest program with a custom memory limit, and then invokes the resultant binary with (3, 5) as the input.

The zkVM will then run the guest program and produce a proof of its correct execution.

After the proving completes, the host program then reads the output off the output tape and prints it, along with any logs, and then verifies the proof.

3. Run your program

cargo run -r

Notice that we use cargo run, rather than cargo nexus run. Our host program is just a normal Rust program, whereas cargo nexus run is used to execute guest programs from the command line.

You should see the host program print:

Setting up Nova public parameters...
Proving execution of vm... output is 15!
>>>>> Logging
Read private input: (3, 5)
<<<<<
Verifying execution...  Succeeded!

If you want to execute guest programs from the command line, you can navigate into ./src/guest and then use cargo nexus commands to do so.

4. Other host program options

There are a few ways to configure how the guest program is run and proven. Perhaps most importantly, the SDK supports two alternative provers: HyperNova and Jolt.

To use HyperNova, just use the example above, replacing nova with hypernova in the program.

Jolt support is experimental, and in particular does not currently allow inputs, outputs, logging, or assertions in the guest program. You can test it using a guest program like

#![no_std]
#![no_main]
 
fn fib(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}
 
#[nexus_rt::main]
fn main() {
    let n = 10;
    let result = fib(n);
    core::hint::black_box(result);
}

and a host program like

use nexus_sdk::{compile::CompileOpts, jolt::Jolt, Local};
 
const PACKAGE: &str = "guest";
 
fn main() {
    let opts = CompileOpts::new(PACKAGE);
 
    // defaults to local proving
    let prover: Jolt<Local> = Jolt::compile(&opts).expect("failed to load program");
 
    println!("Proving execution of vm...");
    let proof = prover.prove().expect("failed to prove program");
 
    print!("Verifying execution...");
    proof.verify().expect("failed to verify proof");
 
    println!("  Succeeded!");
}

To see more example of using the SDK, check out the examples folder (opens in a new tab).