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

Ceno Project Overview

Ceno is a non-uniform, segmentable, and parallelizable RISC-V Zero-Knowledge Virtual Machine (zkVM). It allows for the execution of Rust code in a verifiable manner, leveraging the power of zero-knowledge proofs.

Key Features

  • RISC-V Architecture: Ceno is built around the RISC-V instruction set, providing a standardized and open-source foundation for the virtual machine.
  • Zero-Knowledge Proofs: The core of Ceno is its ability to generate zero-knowledge proofs of computation, ensuring that programs have been executed correctly without revealing any private inputs.
  • Rust Support: Ceno is written in Rust and is designed to run programs also written in Rust, allowing developers to leverage the safety and performance of the Rust language.
  • Modularity: The project is divided into several key components, each with a specific role in the Ceno ecosystem.

Project Structure

The Ceno workspace is organized into the following main crates:

  • ceno_cli: A command-line interface for interacting with the Ceno zkVM.
  • ceno_emul: Provides emulation capabilities for the RISC-V instruction set.
  • ceno_host: The host component responsible for managing the zkVM and orchestrating the proof generation process.
  • ceno_rt: The runtime environment for guest programs running within the zkVM.
  • ceno_zkvm: The core zkVM implementation, including the prover and verifier.
  • examples: A collection of example programs that demonstrate how to use Ceno.

Getting Started

This chapter will guide you through setting up your local development environment for Ceno and running your first zero-knowledge program.

Local Build Requirements

Ceno is built in Rust, so you must install the Rust toolchain first.

We also use cargo-make to orchestrate the build process. You can install it with the following command:

cargo install cargo-make

Ceno executes RISC-V instructions, so you will also need to install the Risc-V target for Rust. You can do this with the following command:

rustup target add riscv32im-ceno-zkvm-elf

Installing cargo ceno

The cargo ceno command is the primary tool for interacting with the Ceno zkVM. You can install it by running the following command from the root of the repository:

JEMALLOC_SYS_WITH_MALLOC_CONF="retain:true,metadata_thp:always,thp:always,dirty_decay_ms:-1,muzzy_decay_ms:-1,abort_conf:true" \
    cargo install --git https://github.com/scroll-tech/ceno.git --features jemalloc --features nightly-features cargo-ceno

Building the Examples

The Ceno project includes a variety of examples to help you get started.

You can build all the examples using the cargo ceno command-line tool. Execute the following command in the Ceno repository root directory:

cargo ceno build --example fibonacci

This command will compile the example fibonacci located in the examples/examples directory and place the resulting ELF files in the examples/target/riscv32im-ceno-zkvm-elf/release directory.

Running an Example

Once the examples are built, you can run any of them using the cargo ceno run command. We will run the Fibonacci example.

This example calculates the n-th Fibonacci number, where n is determined by a hint value provided at runtime. For this guide, we will calculate the 1024-th number (corresponding to hint value 10 as 2^10=1024) in the sequence.

Execute the following command in the Ceno repository root directory to run the Fibonacci example with prove/verify:

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

Let’s break down the command:

  • cargo ceno prove: This is the command to prove a Ceno program.
  • --example fibonacci: This specifies that we want to run the fibonacci example.
  • --hints=10: This is a private input to our program. In this case, it tells the program to run 2^10 (1024) Fibonacci steps.
  • --public-io=4191: This is the expected public output. The program will verify that the result of the computation matches this value.

If the command runs successfully, you have just run your first ZK program with Ceno! The next chapter will dive into the code for this example.

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.

Host-Guest Interaction

A critical aspect of developing ZK applications is understanding how the “host” (the machine running the prover) and the “guest” (the ZK program running inside the vm) communicate with each other.

In Ceno, this communication happens in two main ways:

  1. Private Inputs (Hints): The host can pass private data to the guest.
  2. Public Inputs/Outputs (I/O): The guest can receive public data and commit to public outputs that the host can verify.

We saw both of these in the command used to run the Fibonacci example:

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

Private Inputs (Hints)

Private inputs, which Ceno refers to as “hints,” are data known only to the host and the guest. They are not revealed publicly and do not become part of the final proof. This is the primary way to provide secret inputs to your ZK program.

In the guest code, you use the ceno_rt::read() function to access this data.

Guest Code:

// Reads the private hint value provided by the host.

fn main() {
    let log_n: u32 = ceno_rt::read();
    // do something...
}

Host Command:

... --hints=10 ...

In this interaction, the value 10 is passed from the host to the guest. The guest program reads this value and uses it to determine how many Fibonacci iterations to perform. This input remains private.

Public Inputs and Outputs

Public I/O is data that is known to both the host and the verifier. It is part of the public record and is used to ensure the ZK program is performing the correct computation on the correct public data.

In Ceno, the guest program can commit data to the public record using the ceno_rt::commit() function.

Guest Code:

fn main() {
    // do something ...
    // Commits the final result `b` to the public output.
    ceno_rt::commit(&b);
}

Host Command:

... --public-io=4191 ...

Here, the guest calculates the final Fibonacci number and commits the result b. The Ceno host environment then checks that this committed value is equal to the value provided in the --public-io argument (4191). If they do not match, the proof will fail, indicating an incorrect computation or a different result than expected.

This mechanism is crucial for creating verifiable computations. You can use public I/O to:

  • Provide public inputs that the program must use.
  • Assert that the program produces a specific, known public output.

Generating and Verifying Proofs

The cargo ceno command provides a streamlined workflow for generating and verifying proofs of your ZK programs. This is handled primarily by the keygen, prove and verify subcommands.

Here’s a more detailed look at the steps involved in generating a proof:

  1. Key Generation (cargo ceno keygen): Before proving, you need a proving key and a verification key. The keygen command generates these keys for a given guest program.

    cargo ceno keygen --example <GUEST_EXAMPLE_NAME> --out-vk <PATH_TO_VERIFICATION_KEY_FILE>
    
  2. Proof Generation (using the witness): The final step is to use the proving key and the witness to generate the proof. The prove command automates this, but you can also perform this step manually using the lower-level raw-prove command if you have the ELF file, proving key, and witness.

By using cargo ceno prove, you get a simplified experience that handles these steps for you. For most use cases, cargo ceno prove and cargo ceno verify are the primary commands you will use.

cargo ceno prove --example <GUEST_EXAMPLE_NAME> --hints=<HINTS_SEPARATED_BY_COMMA> --public-io=<PUBLIC_IO> --out-proof target/fibonacci.proof

Concrete Example

You can use ceno to generate proofs for your own custom Rust programs. Let’s walk through how to set up a new project and use ceno with it.

1. Project Setup

First, create a new binary crate with cargo:

cargo new my-ceno-program
cd my-ceno-program

Your project will have the following structure:

my-ceno-program/
├── Cargo.toml
└── src/
    └── main.rs

2. Cargo.toml

Next, you need to add ceno_rt as a dependency in your Cargo.toml. ceno_rt provides the runtime environment and syscalls for guest programs.

[package]
name = "my-ceno-program"
version = "0.1.0"
edition = "2024"

[dependencies]
ceno_rt = { git = "https://github.com/scroll-tech/ceno.git" }
rkyv = { version = "0.8", default-features = false, features = [
    "alloc",
    "bytecheck",
] }

Note: For local development, you can use a path dependency: ceno_rt = { path = "../ceno/ceno_rt" }

Special notes, as ceno rely on nightly rust toolchain, please also add file rust-toolchain.toml with example content

[toolchain]
# refer toolchain from https://github.com/scroll-tech/ceno/blob/master/rust-toolchain.toml#L2
channel = "nightly-2025-11-20"
targets = ["riscv32im-unknown-none-elf"]
# We need the sources for build-std.
components = ["rust-src"]

3. Writing the Guest Program

Now, let’s write a simple guest program in src/main.rs. This program will read one u32 values from the input, add a constant to it, and write the result to the output.

extern crate ceno_rt;

fn main() {
    let a: u32 = ceno_rt::read();
    let b: u32 = 3;
    let c = a.wrapping_add(b);

    ceno_rt::commit(&c);
}

4. Building, Proving, and Verifying

With your custom program ready, you can use ceno to manage the workflow. These commands are typically run from the root of your project (my-ceno-program).

4.1. Build the program

The build command compiles your guest program into a RISC-V ELF file.

cargo ceno build

This will create an ELF file at target/riscv32im-ceno-zkvm-elf/debug/my-ceno-program.

4.2. Generate Keys

Next, generate the proving and verification keys.

cargo ceno keygen --out-vk vk.bin

This will save the keys in a keys directory.

4.3. Generate a Proof

Now, run the program and generate a proof. You can provide input via the --stdin flag.

cargo ceno prove --hints=5 --public-io=8 --out-proof proof.bin

This command executes the ELF, generates a proof, and saves it as proof.bin.

4.4. Providing Inputs via File

In addition to providing hints and public I/O directly on the command line, you can also use files to provide these inputs. This is particularly useful for larger inputs or when you want to reuse the same inputs across multiple runs.

Hints via File

You can provide hints to the prover using the --hints-file flag. The hints file should be a raw binary file containing the hint data. You can create this file using a simple Rust program. For example, to create a hints file with the value 5u32 and 8u32:

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

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

Then, you can use this file when generating a proof:

cargo ceno prove --hints-file hints.bin --public-io=8 --out-proof proof.bin

This approach is useful when you have a large amount of hint data that is inconvenient to pass through the command line.

4.5. Verify the Proof

Finally, verify the generated proof.

cargo ceno verify --vk vk.bin --proof proof.bin

If the proof is valid, you’ll see a success message. This workflow allows you to integrate ceno’s proving capabilities into your own Rust projects.

The ceno CLI

The ceno command-line interface is the primary way to interact with the Ceno ZKVM. It allows you to build, run, and verify your ZK programs.

The available commands are:

  • cargo ceno build: Compiles a guest program written in Rust into a RISC-V ELF file.
  • cargo ceno info: Provides information about a compiled ELF file, such as its size and segments.
  • cargo ceno keygen: Generates a proving key and a verification key for a guest program.
  • cargo ceno prove: Compiles, runs, and proves a Ceno guest program in one go.
  • cargo ceno run: Executes a guest program.
  • cargo ceno verify: Verifies a proof generated by cargo ceno prove.
  • cargo ceno raw-keygen: A lower-level command to generate keys from a compiled ELF file.
  • cargo ceno raw-prove: A lower-level command to prove a program from a compiled ELF file and a witness.
  • cargo ceno raw-run: A lower-level command to run a program without the full proof generation, useful for debugging.

For detailed usage of each command, you can use the --help flag, for example: cargo ceno run --help. The next sections will explain the three core commands.

cargo ceno build

The cargo ceno build command compiles a Ceno program. It is a wrapper around the standard cargo build command, but it automatically sets the correct target and rustflags for building Ceno programs.

Usage

cargo ceno build [OPTIONS]

Options

The build command accepts all the same options as cargo build. Some of the most common options are:

  • --example <NAME>: Build a specific example.
  • --release: Build in release mode.
  • --package <NAME> or -p <NAME>: Specify which package to build.
  • --workspace: Build all packages in the workspace.

For a full list of options, run cargo ceno build --help.

Run, prove, and keygen

The run, prove, and keygen commands are used to execute Ceno programs. They all share a similar set of options.

  • cargo ceno run: Executes a Ceno program in the ZKVM.
  • cargo ceno prove: Executes a Ceno program and generates a proof of its execution.
  • cargo ceno keygen: Generates a proving key and a verification key for a Ceno program.

Usage

cargo ceno run [OPTIONS]
cargo ceno prove [OPTIONS]
cargo ceno keygen [OPTIONS]

Options

These commands accept all the same options as cargo build. Some of the most common options are:

  • --example <NAME>: Run a specific example.
  • --release: Run in release mode.
  • --package <NAME> or -p <NAME>: Specify which package to run.

In addition, the prove and keygen commands have some Ceno-specific options:

  • --proof <PATH>: Path to the output proof file (for prove). Defaults to proof.bin.
  • --out-vk <PATH>: Path to the output verification key file (for keygen). Defaults to vk.bin.

For a full list of options, run cargo ceno <COMMAND> --help.

Raw Commands

The raw-run, raw-prove, and raw-keygen commands are lower-level commands that operate on ELF files directly. These are useful for debugging and for integrating Ceno with other build systems.

cargo ceno raw-run

Executes a pre-compiled ELF file in the Ceno ZKVM.

Usage

cargo ceno raw-run <ELF_PATH>

cargo ceno raw-prove

Generates a proof for a pre-compiled ELF file.

Usage

cargo ceno raw-prove <ELF_PATH>

cargo ceno raw-keygen

Generates a proving key and a verification key for a pre-compiled ELF file.

Usage

cargo ceno raw-keygen <ELF_PATH>

Walkthroughs of Examples

This chapter will contain detailed walkthroughs of selected examples from the examples/ directory.

Fibonacci

The fibonacci example computes the n-th Fibonacci number, where n is a power of 2. The purpose of this example is to show how to perform a simple computation within the zkVM.

Guest Code

The guest program for the fibonacci example is located at 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);
}

The guest program reads a private input log_n from the host. This input determines the number of iterations to perform. The program then calculates n = 1 << log_n and computes the n-th Fibonacci number using a standard iterative approach. To prevent the numbers from growing too large, the computation is performed modulo 7919. Finally, the program commits the result back to the host as a public output.

This example demonstrates the basic workflow of a Ceno zkVM program: reading private inputs, performing computations, and committing public outputs. It shows that you can write standard Rust code for the guest, and the zkVM will execute it.

Is Prime

The is_prime example counts the number of prime numbers up to a given integer n. This example showcases a slightly more complex algorithm and control flow.

Guest Code

The guest program for the is_prime example is located at examples/examples/is_prime.rs.

extern crate ceno_rt;

fn is_prime(n: u32) -> bool {
    if n < 2 {
        return false;
    }
    let mut i = 2;
    while i * i <= n {
        if n.is_multiple_of(i) {
            return false;
        }
        i += 1;
    }

    true
}

fn main() {
    let n: u32 = ceno_rt::read();
    let mut cnt_primes = 0;

    for i in 0..=n {
        cnt_primes += is_prime(i) as u32;
    }

    if cnt_primes > 1000 * 1000 {
        panic!();
    }
}

The guest program reads an integer n from the host. It then iterates from 0 to n, checking if each number is prime using a helper function is_prime. The is_prime function implements the trial division method. The total count of prime numbers is accumulated in cnt_primes. If the count of primes exceeds a certain threshold, the program will panic. This demonstrates how to handle exceptional cases in the guest. The program does not commit any public output, but the proof generated by the zkVM still guarantees that the computation was performed correctly.

This example highlights the ability to define and use helper functions within the guest code, as well as the use of control flow constructs like loops and conditional statements.

BN254 Curve Syscalls

The bn254_curve_syscalls example demonstrates the use of syscalls for elliptic curve operations on the BN254 curve. This is useful for cryptographic applications that require curve arithmetic.

Guest Code

The guest program for the bn254_curve_syscalls example is located at examples/examples/bn254_curve_syscalls.rs.

// Test addition of two curve points. Assert result inside the guest
extern crate ceno_rt;
use ceno_syscall::{syscall_bn254_add, syscall_bn254_double};

use substrate_bn::{AffineG1, Fr, G1, Group};
fn bytes_to_words(bytes: [u8; 64]) -> [u32; 16] {
    let mut bytes = bytes;
    // Reverse the order of bytes for each coordinate
    bytes[0..32].reverse();
    bytes[32..].reverse();
    std::array::from_fn(|i| u32::from_le_bytes(bytes[4 * i..4 * (i + 1)].try_into().unwrap()))
}

fn g1_to_words(elem: G1) -> [u32; 16] {
    let elem = AffineG1::from_jacobian(elem).unwrap();
    let mut x_bytes = [0u8; 32];
    elem.x().to_big_endian(&mut x_bytes).unwrap();
    let mut y_bytes = [0u8; 32];
    elem.y().to_big_endian(&mut y_bytes).unwrap();

    let mut bytes = [0u8; 64];
    bytes[..32].copy_from_slice(&x_bytes);
    bytes[32..].copy_from_slice(&y_bytes);

    bytes_to_words(bytes)
}

fn main() {
    let a = G1::one() * Fr::from_str("237").unwrap();
    let b = G1::one() * Fr::from_str("450").unwrap();
    let mut a = g1_to_words(a);
    let b = g1_to_words(b);

    log_state(&a);
    log_state(&b);

    syscall_bn254_add(&mut a, &b);

    assert_eq!(
        a,
        [
            3533671058, 384027398, 1667527989, 405931240, 1244739547, 3008185164, 3438692308,
            533547881, 4111479971, 1966599592, 1118334819, 3045025257, 3188923637, 1210932908,
            947531184, 656119894
        ]
    );
    log_state(&a);

    let c = G1::one() * Fr::from_str("343").unwrap();
    let mut c = g1_to_words(c);
    log_state(&c);

    syscall_bn254_double(&mut c);
    log_state(&c);

    let one = g1_to_words(G1::one());
    log_state(&one);

    syscall_bn254_add(&mut c, &one);
    log_state(&c);

    // 2 * 343 + 1 == 237 + 450, one hopes
    assert_eq!(a, c);
}

#[cfg(debug_assertions)]
fn log_state(state: &[u32]) {
    use ceno_rt::info_out;
    info_out().write_frame(unsafe {
        core::slice::from_raw_parts(state.as_ptr() as *const u8, size_of_val(state))
    });
}

#[cfg(not(debug_assertions))]
fn log_state(_state: &[u32]) {}

The guest program initializes two points on the BN254 curve. It then uses the syscall_bn254_add syscall to add the two points and asserts that the result is correct by comparing it with a pre-computed value. It also demonstrates the use of the syscall_bn254_double syscall to double a point. The program includes helper functions to convert between different representations of curve points.

This example showcases how to leverage syscalls to perform complex and computationally expensive operations. By using syscalls, the guest can delegate these operations to the host, which can often execute them more efficiently. The guest can still verify the results of the syscalls to ensure the integrity of the computation.

Ceno RT Alloc

The ceno_rt_alloc example shows how to use the allocator in the Ceno runtime to allocate memory on the heap.

Guest Code

The guest program for the ceno_rt_alloc example is located at examples/examples/ceno_rt_alloc.rs.

use core::ptr::{addr_of, read_volatile};

extern crate ceno_rt;

extern crate alloc;
use alloc::{vec, vec::Vec};

static mut OUTPUT: u32 = 0;

fn main() {
    // Test writing to a global variable.
    unsafe {
        OUTPUT = 0xf00d;
        black_box(addr_of!(OUTPUT));
    }

    // Test writing to the heap.
    let v: Vec<u32> = vec![0xbeef];
    black_box(&v[0]);

    // Test writing to a larger vector on the heap
    let mut v: Vec<u32> = vec![0; 128 * 1024];
    ceno_syscall::syscall_phantom_log_pc_cycle("finish allocation");
    v[999] = 0xdead_beef;
    black_box(&v[0]);

    ceno_syscall::syscall_phantom_log_pc_cycle("start fibonacci");
    let log_n: u32 = 12;
    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;
    }
    ceno_syscall::syscall_phantom_log_pc_cycle("end fibonacci");

    // write to heap which allocated earlier shard
    v[999] = 0xbeef_dead;
    let mut v: Vec<u32> = vec![0; 128 * 1024];
    // write to heap allocate in current non-first shard
    v[0] = 0xdead_beef;
}

/// Prevent compiler optimizations.
fn black_box<T>(x: *const T) -> T {
    unsafe { read_volatile(x) }
}

The guest program demonstrates three different memory operations. First, it writes to a global static variable. Second, it allocates a small Vec on the heap. Third, it allocates a much larger Vec on the heap and writes a value to an element in it. The black_box function is used to ensure that the compiler does not optimize away these memory operations.

This example is important for understanding how memory management works in the Ceno zkVM. It shows that the guest has access to both static memory and heap memory, and can perform dynamic memory allocation.

Keccak Syscall

The keccak_syscall example demonstrates how to use a syscall to perform the Keccak permutation, which is the core of the Keccak hash function (used in SHA-3).

Guest Code

The guest program for the keccak_syscall example is located at examples/examples/keccak_syscall.rs.

//! Compute the Keccak permutation using a syscall.
//!
//! Iterate multiple times and log the state after each iteration.

extern crate ceno_rt;
use ceno_syscall::syscall_keccak_permute;

const ITERATIONS: usize = 100;

fn main() {
    let mut state = [0_u64; 25];

    for i in 0..ITERATIONS {
        syscall_keccak_permute(&mut state);
        if i == 0 {
            log_state(&state);
        }
    }
}

#[cfg(debug_assertions)]
fn log_state(state: &[u64; 25]) {
    use ceno_rt::info_out;
    info_out().write_frame(unsafe {
        core::slice::from_raw_parts(state.as_ptr() as *const u8, state.len() * size_of::<u64>())
    });
}

#[cfg(not(debug_assertions))]
fn log_state(_state: &[u64; 25]) {}

The guest program initializes a 25-word state and then enters a loop where it repeatedly applies the Keccak permutation to the state using the syscall_keccak_permute syscall. The log_state function is used to log the state after the first permutation, which can be useful for debugging.

This example further illustrates the power of syscalls. The Keccak permutation is a complex operation, and implementing it efficiently in the guest would be challenging. By providing it as a syscall, the Ceno platform makes it easy for guest programs to use this important cryptographic primitive.

Median

The median example shows how to find the median of a list of numbers. It demonstrates a common pattern where the host provides a candidate answer, and the guest verifies it.

Guest Code

The guest program for the median example is located at examples/examples/median.rs.

//! Find the median of a collection of numbers.
//!
//! Of course, we are asking our good friend, the host, for help, but we still need to verify the answer.
extern crate ceno_rt;
use ceno_rt::debug_println;
#[cfg(debug_assertions)]
use core::fmt::Write;

fn main() {
    let numbers: Vec<u32> = ceno_rt::read();
    let median_candidate: u32 = ceno_rt::read();
    let smaller = numbers.iter().filter(|x| **x < median_candidate).count();
    assert_eq!(smaller, numbers.len() / 2);
    debug_println!("{}", median_candidate);
}

The guest program reads a list of numbers and a median candidate from the host. It then verifies that the candidate is indeed the median by counting the number of elements in the list that are smaller than the candidate. If the count is equal to half the length of the list, the assertion passes, and the program successfully completes.

This example showcases a powerful pattern for zkVM programming: verifiable computation. The host, which is not constrained by the limitations of the zkVM, can perform a complex computation (like finding the median of a large list) and then provide the answer to the guest. The guest’s task is then to perform a much simpler computation to verify that the host’s answer is correct. This allows for the verification of complex computations that would be too expensive to perform directly in the zkVM.

Sorting

The sorting example demonstrates how to sort a list of numbers inside the guest.

Guest Code

The guest program for the sorting example is located at examples/examples/sorting.rs.

extern crate ceno_rt;
use ceno_rt::debug_println;
#[cfg(debug_assertions)]
use core::fmt::Write;

fn main() {
    let mut scratch: Vec<u32> = ceno_rt::read();
    scratch.sort();
    // Print any output you feel like, eg the first element of the sorted vector:
    debug_println!("{}", scratch[0]);
}

The guest program reads a list of numbers from the host, creates a mutable copy of it, and then sorts the copy using the standard sort method from the Rust standard library. The debug_println! macro is used to print the first element of the sorted vector, which can be useful for debugging.

This example demonstrates that the Ceno zkVM supports a significant portion of the Rust standard library, including common data structures and algorithms. This makes it easy to write complex programs for the guest, as you can leverage the power and convenience of the standard library.

Advanced Topics

This chapter is for developers who want to dive deeper into the internals of Ceno.

Guest Programming (ceno_rt)

The ceno_rt crate provides the runtime environment for guest programs running inside the Ceno ZKVM. It offers essential functionalities for interacting with the host and the ZKVM environment.

Execution Environment

A guest program’s execution begins at the _start symbol, which is defined in ceno_rt. This entry point sets up the global pointer and stack, and then calls the standard Rust main function.

After main returns, or to exit explicitly, the program can call ceno_rt::halt(exit_code). An exit_code of 0 indicates success.

Key features of ceno_rt include:

Memory-Mapped I/O (mmio)

The ceno_rt::mmio module provides a way to read data provided by the host through memory-mapped regions.

  • Hints: These are private inputs from the host. You can read them using ceno_rt::mmio::read_slice() to get a byte slice or ceno_rt::mmio::read::<T>() to deserialize a specific type.
  • Public I/O: The ceno_rt::mmio::commit function is used to reveal public outputs. It verifies that the output produced by the guest matches the expected output provided by the host.

Standard I/O (io)

The ceno_rt::io module contains IOWriter for writing data. In debug builds, a global IOWriter instance is available through ceno_rt::io::info_out(). You can use the debug_print! and debug_println! macros for logging during development.

Dynamic Memory (allocator)

For dynamic memory needs, ceno_rt::allocator provides a simple allocator.

System Calls (syscalls)

The ceno_rt::syscalls module offers low-level functions to access precompiled operations for performance-critical tasks. For more details, see the “Accelerated Operations with Precompiles” chapter.

Weakly Linked Functions

ceno_rt defines several weakly linked functions (e.g., sys_write, sys_alloc_words, sys_rand) that provide default implementations for certain system-level operations. These can be overridden by the host environment.

When writing a guest program, you will typically include ceno_rt as a dependency in your Cargo.toml.

Accelerated Operations with Precompiles (Syscalls)

Ceno provides “precompiles” to accelerate common, computationally intensive operations. These are implemented as syscalls that the guest program can invoke. Using precompiles is much more efficient than executing the equivalent logic in standard RISC-V instructions.

Using Precompiles

To use a precompile, you need to call the corresponding function from the ceno_rt::syscalls module in your guest code.

For example, to compute a Keccak permutation, you can use syscall_keccak_permute:

#![allow(unused)]
fn main() {
use ceno_rt::syscalls::syscall_keccak_permute;

let mut state = [0u64; 25];
// ... initialize state ...
syscall_keccak_permute(&mut state);
}

Available Precompiles

Ceno currently offers the following precompiles:

  • Hashing:

    • syscall_keccak_permute: For Keccak-f[1600] permutation, the core of Keccak and SHA-3.
    • syscall_sha256_extend: For the SHA-256 message schedule (W table) extension.
  • Cryptography:

    • syscall_secp256k1_add: Elliptic curve addition on secp256k1.
    • syscall_secp256k1_double: Elliptic curve point doubling on secp256k1.
    • syscall_secp256k1_decompress: Decompress a secp256k1 public key.
    • syscall_bn254_add: Elliptic curve addition on BN254.
    • syscall_bn254_double: Elliptic curve point doubling on BN254.
    • syscall_bn254_fp_addmod, syscall_bn254_fp_mulmod: Field arithmetic for BN254’s base field.
    • syscall_bn254_fp2_addmod, syscall_bn254_fp2_mulmod: Field arithmetic for BN254’s quadratic extension field.

You can find examples of how to use each of these syscalls in the examples/ directory of the project.

Profiling & Performance

Ceno includes tools to help you analyze and optimize the performance of your ZK programs.

Execution Profiling

You can profile the execution of your guest program by using the --profiling flag with the cargo ceno run or other binary commands. This will output detailed statistics about the execution, such as cycle counts for different parts of the program.

For example:

cargo ceno run --profiling=1 -- <your_program>

The value passed to --profiling controls the granularity of the profiling information.

The output will show a tree of spans with timing information, allowing you to identify performance bottlenecks in your code.

Benchmarking

The ceno_zkvm crate contains a benches directory with several benchmarks. You can use these as a reference for writing your own benchmarks using criterion.

Running the benchmarks can give you an idea of the performance of different operations and help you optimize your code. To run the benchmarks, you can use the standard cargo bench command.

Differences from RISC-V

Integration with Ethereum

Prover Network

On-chain verification