Understanding the RISC Zero zkVM Starter Template
We've created a cargo risczero
tool for creating, managing, and testing RISC Zero projects. This tool makes the Rust starter template repository obsolete, as a starter project can now be generated via cargo risczero new
. See the cargo risczero
documentation for details on how to do this. While we intend to update this page soon, in the meantime its information on the structure and purpose of the Rust starter template remains relevant to the cargo risczero new
version of the template.
The RISC Zero Rust starter template provides a starting point for RISC Zero zkVM projects. This article will describe what the template code does, and why we've written it this way. In particular, it should help you understand:
- The host, guest, and build components of RISC Zero zkVM programs
- How guest methods are built and made available to the host
- How the host calls guest methods
- What is included in Cargo files to be able to run the zkVM
This tutorial will not include:
- How to create a project based on the starter template (see Hello, Multiply)
- The cryptographic theory behind the RISC Zero zkVM (see our proof system explainers and reference materials)
- The internal components of the RISC Zero zkVM (see our Overview of the zkVM article)
- Design considerations for programs that use multiple RISC Zero zkVM guest methods as part of larger systems to accomplish complex tasks (see our voting machine example)
Structure of a RISC Zero zkVM Program
Like other virtual machines, the RISC Zero zkVM has both host and guest components. The guest component contains the code to be proven. The host component provides any required data to the guest, executes the guest code, and handles the guest's output.
In typical use cases, a RISC Zero zkVM program will actually be structured with three components:
- Source code for the guest,
- Code that builds the guest's source code into executable methods, and
- Source code for the host, which will call these built methods.
The code for each of these components uses its own associated RISC Zero crate or module:
- The guest code uses the
guest
module of therisc0-zkvm
crate - The build code for building guest methods uses the
risc0-build
crate - The host code uses the
risc0-zkvm
crate
It is possible to organize the files for these components in various ways. However, in code published by RISC Zero we use a standard directory structure for zkVM code, and we recommend you use this structure as well. See below for a diagram of this directory structure with annotations. You can also see this structure in the Rust starter template used by cargo risczero new
.
project_name
├── Cargo.toml
├── host
│ ├── Cargo.toml
│ └── src
│ └── main.rs <-- Host code goes here
└── methods
├── Cargo.toml
├── build.rs <-- Build (embed) code goes here
├── guest
│ ├── Cargo.toml
│ └── src
│ └── bin
│ └── method_name.rs <-- Guest code goes here
└── src
└── lib.rs <-- Build (include) code goes here
Now let's go through these three components in detail.
Guest code
The guest code is the code the prover wants to demonstrate is faithfully executed. The template starts from the simplest possible guest code -- its guest method does nothing:
#![no_std]
#![no_main]
risc0_zkvm::guest::entry!(main);
pub fn main() {
// TODO: Implement your guest code here
}
Let's see what each of these lines does.
#![no_std]
The guest code should be as lightweight as possible for performance reasons. So, since we aren't using std, we exclude it.
#![no_main]
risc0_zkvm_guest::entry!(main);
The guest code is never launched as a standalone Rust executable, so we specify #![no_main]
. However, we must make the guest code available for the host to launch, and to do that we must specify which function to call when the host starts executing this guest code. We use the risc0_zkvm_guest::entry!
macro to indicate the initial guest function to call, which in this case is main
.
pub fn main() {
// TODO: Implement your guest code here
}
Here is the actual guest code. Notice that the function is named main
, matching the name specified in entry!
, so this is the function that will be called when the host launches the guest. In real use cases, you would do more than nothing in this function.
Building Guest Methods
The risc0-build
crate has two functions, embed_methods
and embed_methods_with_options
, which are used to build guest code into a method (or methods) that the host can call. Simple use cases have no need to do any customization for the build step, and you can just call these functions as described below. For more complex cases, it is sometimes useful to replace embed_methods
with embed_methods_with_options
.
These functions are called at build time using Cargo build scripts. The resulting files with the built methods must then be included so that the host can depend on them.
Embedding
The guest methods are embedded using a build.rs
file in the methods directory where you want the methods embedded. This is where the host code will need to look to find the guest methods. A basic build.rs
file for embedding methods looks as follows:
fn main() {
risc0_build::embed_methods();
}
For more advanced cases, replace embed_methods
with a call to embed_methods_with_options
and set appropriate options for your use case.
Including
Embedding the guest methods using these build scripts creates source files in the Rust output directory. To make this code available to the host, these generated files must be included somewhere the host can find them. So the methods directory contains a file src/lib.rs
with the following include command:
include!(concat!(env!("OUT_DIR"), "/methods.rs"));
Build dependencies in Cargo
Embedding depends on risc0-build
. Since embedding happens in built scripts, Cargo needs to know they are build dependencies. Therefore, in the methods directory cargo file (for embedding), we include
[build-dependencies]
risc0-build = "0.12"
(or adjust the version number if you want to use a different version of risc0).
Additionally, the embed_methods
code needs to know where to find the guest code. This is indicated with custom risc0
metadata in the methods directory cargo file, which looks like
[package.metadata.risc0]
methods = ["guest"]
Here "guest"
is the relative path to the root of the directory with the guest source code, and can be adjusted if you aren't following the directory structure outlined above.
Host code
Now let's look at the host code need to execute the guest. The code in the template does not communicate with the guest or provide a method for sending the receipt to an external verifier. Let's look at the code first in full, then line by line:
use methods::{METHOD_NAME_ELF, METHOD_NAME_ID};
use risc0_zkvm::Prover;
fn main() {
// Make the prover.
let mut prover = Prover::new(METHOD_NAME_ELF, METHOD_NAME_ID).expect(
"Prover should be constructed from valid method source code and corresponding method ID",
);
// Run prover & generate receipt
let receipt = prover.run()
.expect("Code should be provable unless it had an error or overflowed the maximum cycle count");
// Optional: Verify receipt to confirm that recipients will also be able to verify your receipt
receipt.verify(METHOD_NAME_ID).expect(
"Code you have proven should successfully verify; did you specify the correct method ID?",
);
}
We start with use declarations
use methods::{METHOD_NAME_ELF, METHOD_NAME_ID};
use risc0_zkvm::host::Prover;
For Prover
this is straightforward, but the methods
are coming from computer generated code. Specifically, the methods.rs
file you included earlier contains generated constants needed to call guest methods. For each guest code file, two constants are generated: <FILENAME>_ELF
and <FILENAME>_ID
(where <FILENAME>
is the name of the file rendered in all capital letters). The <FILENAME>_ID
is a image ID, a cryptographic hash that will be committed to the receipt and allows you to convince a verifier that the code you proved is the same code you are showing to them. The <FILENAME>_ELF
is your compiled guest code (in ELF format).
fn main() {
The host is executed directly, so this is the normal Rust main
function.
We will replace expect
s with unwrap
s in the following lines so we can focus on the core functionality:
let mut prover = Prover::new(METHOD_NAME_ELF, METHOD_NAME_ID).unwrap();
This creates a prover, which can be run to execute its associated guest code and produce a receipt proving execution. It must be initialized with an image ID and with an ELF file with the code to be executed. These have be created in the build step, and can be accessed via the <FILENAME>_ID
and <FILENAME>_ELF
constants.
let receipt = prover.run().unwrap();
This line actually runs the guest code inside the prover, the result of which is a receipt proving the execution. From here we can transfer the receipt to anyone we wish to verify our code -- in the template, we do so in the same process for simplicity.
receipt.verify(METHOD_NAME_ID).unwrap();
This line verifies that a receipt corresponds to the execution of guest code whose image ID is <FILENAME>_ID
. It's not necessary for the prover to run this line to make a valid proof. Instead, this is needed by anyone who wishes to verify that they have an honest receipt.