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 thefibonacciexample.--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:
- Private Inputs (Hints): The host can pass private data to the guest.
- 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:
-
Key Generation (
cargo ceno keygen): Before proving, you need a proving key and a verification key. Thekeygencommand generates these keys for a given guest program.cargo ceno keygen --example <GUEST_EXAMPLE_NAME> --out-vk <PATH_TO_VERIFICATION_KEY_FILE> -
Proof Generation (using the witness): The final step is to use the proving key and the witness to generate the proof. The
provecommand automates this, but you can also perform this step manually using the lower-levelraw-provecommand 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 bycargo 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 (forprove). Defaults toproof.bin.--out-vk <PATH>: Path to the output verification key file (forkeygen). Defaults tovk.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 orceno_rt::mmio::read::<T>()to deserialize a specific type. - Public I/O: The
ceno_rt::mmio::commitfunction 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 (Wtable) 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.