Skip to main content
Version: 1.2

Building zkVM Hello World

This tutorial will walk you through building your first zkVM application. By following the steps in this guide, you will learn how:

  • to use the cargo-risczero tool to create a new project
  • to send private data to the guest program
  • the zkVM executes and generates a proof of a guest program
  • the guest writes public output

Step 1: Create a New Project

Firstly, visit the installation page for how to install the necessary software.

Once installed, clone the risc0 monorepo and change into the hello-world example directory:

git clone https://github.com/risc0/risc0
cd examples/hello-world

Next, check the version of cargo risczero installed:

cargo risczero --version
cargo-risczero $MAJOR.$MINOR.$PATCH # e.g. release-1.2

To match the example release version with your local installation, check out the corresponding branch of the example:

git checkout release-$MAJOR.$MINOR # e.g. release-1.2

For example, if your local version is cargo-risczero 1.2.0, run:

git checkout release-1.2

Once on the correct release branch, run the example:

cargo run --release

Use this command any time you'd like to check your progress.

Step 2 (Host): Share Private Data as Input with the Guest

The zkVM (or a prover) runs on the host. The host code is located in hello-world/src/main.rs and hello-world/src/lib.rs.

The host creates an executor environment ExecutorEnv before constructing a prover. This executor environment is responsible for managing guest-readable memory. The host makes the value input available to the guest program before execution by adding input to the executor environment.

When the prover executes the program, it can access input via the .write() method on ExecutorEnv::builder():

// hello-world/src/lib.rs
use risc0_zkvm::{default_prover, ExecutorEnv, Receipt};

pub fn multiply(a: u64, b: u64) -> (Receipt, u64) {
let env = ExecutorEnv::builder()
// Send a & b to the guest
.write(&a)
.unwrap()
.write(&b)
.unwrap()
.build()
.unwrap();

...
}

Step 3 (Guest): Read Input and Commit Output

The guest code is located in methods/guest/src/main.rs. This is the portion of the code that will be proven. In the code snippet below, the guest reads the two a and b values from the host and commits them to the journal portion of the receipt.

// hello-world/methods/guest/src/main.rs
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);

fn main() {
// Load the first number from the host
let a: u64 = env::read();
// Load the second number from the host
let b: u64 = env::read();
// Verify that neither of them are 1 (i.e. nontrivial factors)
if a == 1 || b == 1 {
panic!("Trivial factors")
}
// Compute the product while being careful with integer overflow
let product = a.checked_mul(b).expect("Integer overflow");
env::commit(&product);
}

The env::commit function commits public results to the journal. Once committed to the journal, anyone with the receipt can read this value. Notice, by committing any private information to the journal, we make this private data public. For this reason, be sure to avoid committing sensitive data to the public journal.

Step 4 (Host): Generate a Receipt and Read Its Journal Contents

Let's look at how the host generates a receipt and extracts the contents of the journal.

First, the receipt is generated, and afterwards the journal can be extracted:

// hello-world/src/main.rs
use hello_world::multiply;
use hello_world_methods::MULTIPLY_ID;

fn main() {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();

// Pick two numbers
let (receipt, _) = multiply(17, 23);

// Verify receipt, panic if it's wrong
receipt.verify(MULTIPLY_ID).expect(
"Code you have proven should successfully verify; did you specify the correct image ID?",
);
}

where the full multiply function is:

// hello-world/src/lib.rs
// Compute the product a*b inside the zkVM
pub fn multiply(a: u64, b: u64) -> (Receipt, u64) {
let env = ExecutorEnv::builder()
// Send a & b to the guest
.write(&a)
.unwrap()
.write(&b)
.unwrap()
.build()
.unwrap();

// Obtain the default prover.
let prover = default_prover();

// Produce a receipt by proving the specified ELF binary.
let receipt = prover.prove(env, MULTIPLY_ELF).unwrap().receipt;

// Extract journal of receipt (i.e. output c, where c = a * b)
let c: u64 = receipt.journal.decode().expect(
"Journal output should deserialize into the same types (& order) that it was written",
);

// Report the product
println!("I know the factors of {}, and I can prove it!", c);

(receipt, c)
}

The contents of the journal are stored in the variable c. prover.prove() carries out proving of the guest program, and it also completes internal verification of the receipt.

Step 5: Run the Hello World Example

To run this example locally, and see if proving completed successfully, run:

cargo run --release

If proving is successful, the line "I know the factors of 391, and I can prove it!" is printed.

To change the number that is factorized, head to hello-world/src/main.rs and change this line:

let (receipt, _) = multiply(17, 23); // where 17 * 23 = 391

If something went wrong, we hope that troubleshooting will get you familiar with the system, and we'd love to chat with you on Discord. Or, if you believe you've found a bug or other problem in our code, please open an issue describing the problem.

If you're ready to start building more complex projects, we recommend taking a look at the other examples for more project ideas that use zero-knowledge proofs.