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

Your First ZK Program

In the previous chapter, you ran a pre-compiled Fibonacci example. Now, let’s look at the actual Rust code for that guest program and understand how it works.

The goal of this program is to calculate the n-th Fibonacci number inside the Ceno zkVM and then commit the result to the public output.

The Code

Here is the complete source code for the Fibonacci example (examples/examples/fibonacci.rs):

extern crate ceno_rt;

fn main() {
    // Compute the (1 << log_n) 'th fibonacci number, using normal Rust code.
    let log_n: u32 = ceno_rt::read();
    let mut a = 0_u32;
    let mut b = 1_u32;
    let n = 1 << log_n;
    for _ in 0..n {
        let mut c = a + b;
        c %= 7919; // Modulus to prevent overflow.
        a = b;
        b = c;
    }
    // Constrain with public io
    ceno_rt::commit(&b);
}

Code Breakdown

Let’s break down the key parts of this program.

1. Importing the Ceno Runtime

#![allow(unused)]
fn main() {
extern crate ceno_rt;
}

Every Ceno guest program needs to import the ceno_rt crate. This crate provides essential functions for interacting with the zkVM environment, such as reading private inputs and committing public outputs.

2. Reading Private Inputs

fn main() {
    let log_n: u32 = ceno_rt::read();
}

The ceno_rt::read() function is used to read private data that the host provides. In the previous chapter, we passed --hints=10. This is the value that ceno_rt::read() retrieves. The program receives it as an Archived<u32>, which is then converted into a standard u32.

3. Core Logic

fn main() {
    let mut a = 0_u32;
    let mut b = 1_u32;
    let n = 1 << log_n;
    for _ in 0..n {
        let mut c = a + b;
        c %= 7919; // Modulus to prevent overflow.
        a = b;
        b = c;
    }
}

This is standard Rust code for calculating a Fibonacci sequence. It uses the log_n input to determine the number of iterations (n = 1 << log_n, which is 2^10 = 1024). The calculation is performed modulo 7919 to keep the numbers within a manageable size.

This is a key takeaway: You can write normal Rust code for your core computational logic.

4. Committing Public Output

fn main() {
    ceno_rt::commit(&b);
}

After the calculation is complete, ceno_rt::commit() is called. This function takes the final result (b) and commits it as a public output of the zkVM. The host can then verify that the program produced the correct public output. In our run command, this is checked against the --public-io=4191 argument.

Building the Program

To build this program so that it can be run inside the Ceno zkVM, you can use the cargo ceno build command:

cargo ceno build --example fibonacci

This will produce an ELF file at examples/target/riscv32im-ceno-zkvm-elf/release/fibonacci. This is the file that is executed by the run command.

Running the Program

As you saw in the previous chapter, you can run the program with cargo ceno run:

cargo ceno run --example fibonacci --hints=10 --public-io=4191

or alternatively using the hints file to provide the hints:

cargo ceno run --example fibonacci --hints-file=hints.bin --public-io=4191

where hints.bin can be generated by the following rust program:

use std::fs::File;
use std::io::Write;

fn main() {
    let mut file = File::create("hints.bin").unwrap();
    file.write_all(&10u32.to_le_bytes()).unwrap();
}

TODO: support generating hints file by running the guest program (possibly in a different mode, say, hint generating mode)

TODO: support providing public io also in binary file

Now that you understand the basic components of a Ceno program, the next section will explore the interaction between the host and the guest in more detail.