Skip to main content

Your first proof

This guide walks through proving your first ZisK guest program step by step, using a fibonacci computation as the running example. It introduces the core concepts: setting up the proving key, feeding inputs to the prover, generating a proof, and verifying the result.

Set up the project

The fastest way to get a working project for this guide is to clone the zisk repo and move into the examples/fibonacci directory:

bash
git clone https://github.com/0xPolygonHermez/zisk.git
cd zisk/examples/fibonacci/

The fibonacci directory already contains the guest, host, and common crates wired together, the same project produced by Your First Guest Program. It is recommended you completed that guide first, if so, you can cd the previous project instead as both paths produce the same guest.


From guest program to proof

With the ELF in hand, there are two ways to drive the proving pipeline. zisk-sdk is the right choice when proving is part of a larger Rust application and you need to drive the pipeline programmatically, handle inputs dynamically, or integrate verification into your own logic. cargo-zisk is the right choice when you want to prove quickly from the terminal without writing any code, ideal for development, profiling, testing, and one-off runs. Both paths produce the same proof.


Proving with cargo-zisk

The cargo-zisk CLI exposes the full proving pipeline as a sequence of commands. This is the fastest way to go from a compiled guest binary to a verified proof without writing any code.

Build the guest program

Compile the guest to a RISC-V ELF binary using the ZisK toolchain:

bash
cd guest
cargo-zisk build --release
Compiling fibonacci-guest v0.1.0 ($HOME/zisk-examples/fibonacci/guest)
Finished `release` profile [optimized] target(s) in 3.32s

The compiled binary lands at:

target/elf/riscv64ima-zisk-zkvm-elf/release/fibonacci-guest

This ELF is the exact program the zkVM will execute. The elf/riscv64ima-zisk-zkvm-elf target in the path confirms it was compiled for ZisK's RISC-V environment rather than your host machine.

Set up the guest program

Before ZisK can generate a proof it needs a program key derived from your specific guest binary. This key encodes the structure of the program so the prover can efficiently commit to the correct execution trace. Generate it with:

bash
cargo-zisk setup --release
INFO: --- SETUP SUMMARY -------------
INFO: Setup completed for /root/zisk/examples/fibonacci/guest/target/elf/riscv64ima-zisk-zkvm-elf/release/fibonacci-guest
INFO: Program name: fibonacci-guest
INFO: Hash ID: 3f9abe9baa06555df4ca811012589b2ee37e4afcb05401d0f02b79a1b3c41d05

This is a one-time step per guest binary. Re-run it only when the guest source changes and you rebuild the ELF. You do not need to repeat it between proving runs with different inputs.

info

Setup is intentionally separate from proving because it can be expensive and take some time. If you skip this step, the prover will generate the proving key automatically at the start of the first proving run, but that means paying the setup cost then instead of preloading it.

Prove the guest program

With the program key in place, run the prover. Pass the input you want to prove and ZisK will produce a cryptographic proof of your program's executio.

info

Proof generation can take several minutes depending on your machine.

bash
cargo-zisk prove --release -i samples/example-input.bin -o proof.bin
2026-06-11T11:20:57.256805Z INFO: --- PROVE SUMMARY -------------
2026-06-11T11:20:57.256818Z INFO: Proof generated in 148.048s, steps: 13099
2026-06-11T11:20:57.256822Z INFO: Proof saved to proof.bin

By default the prover runs the guest through the software emulator. On Linux x86_64 you can append --asm to switch to the native Assembly executor, which is significantly faster for larger programs:

bash
cargo-zisk prove --release -i samples/example-input.bin -o proof.bin --asm

If your machine has a CUDA-capable GPU, append --gpu to offload proof generation to it for a substantial speedup:

bash
cargo-zisk prove --release -i samples/example-input.bin -o proof.bin --gpu

--asm and --gpu are independent and can be combined for the fastest configuration on supported hardware:

bash
cargo-zisk prove --release -i samples/example-input.bin -o proof.bin --asm --gpu

Verify the proof

With the proof file in hand, verify it. Unlike proving, verification does not re-execute the program. It will be fast regardless of how complex the original computation was. Pass the path to the proof file with -p:

bash
cargo-zisk verify -p proof.bin
INFO: ✓ STARK proof was verified
INFO: --- VERIFICATION SUMMARY ---
INFO: time: 21 milliseconds
INFO: ----------------------------

Anyone can run this command and be cryptographically certain that the committed result was produced by a correct execution of the guest, without seeing the input nor repeating the computation.


Proving with zisk-sdk

The whole proving pipeline can also be driven from Rust using zisk-sdk. You will write a host program that sets up the program key, feeds inputs, generates a proof, and verifies it. This is the natural path when the proving pipeline is executed in a larger application, or when you need to handle inputs and read results programmatically at runtime.

All the code in this section lives in the host crate (host/src/main.rs). The host is an ordinary Rust program that runs on your machine outside the zkVM.

Add the dependency

Start by importing the types you will use:

host/src/main.rs
use fibonacci_common::{fibonacci, U256};
use zisk_sdk::{GuestProgram, ProverClient, ZiskStdin, load_program};

Initialize the prover client

Now instantiate a ProverClient, your entry point to the ZisK SDK. It manages the connection to the prover and exposes the methods you will call at each stage of the pipeline: setup, prove, and verify. ProverClient::embedded().build()? returns the default embedded prover (emulator executor + CPU); if you have a CUDA-capable GPU and want faster runs while iterating, chain .gpu() into the builder before .build()?.

host/src/main.rs
use fibonacci_common::{fibonacci, U256};
use zisk_sdk::{GuestProgram, ProverClient, ZiskStdin, load_program};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

// Building the embbedded prover client.
let client = ProverClient::embedded().build()?;

Ok(())
}
info

This uses the emulator on CPU, which works on all platforms. If you are on Linux x86_64 with a CUDA-compatible GPU, see Choosing a prover client for the .assembly() and .gpu() builder methods that get maximum throughput.

Set up the guest program

Next, tell the prover which guest program to prove. The load_program! macro loads the compiled guest binary from disk at compile time, pointing to the ELF that cargo-zisk build produced. Once loaded, pass it to .setup() to derive the program key for your specific guest binary.

host/src/main.rs
use fibonacci_common::{fibonacci, U256};
use zisk_sdk::{GuestProgram, ProverClient, ZiskStdin, load_program};

/// Guest ELF binary, embedded into the host at build time.
static PROGRAM: GuestProgram = load_program!("fibonacci-guest");

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

// Instatiating a ProverClient
let client = ProverClient::embedded().build()?;

// One-time setup: derive the proving and verification keys for this guest.
client.setup(&PROGRAM).run_sync()?;

Ok(())
}
Loading the ELF at runtime

load_program! embeds the ELF into the host at compile time. Alternatively, resolve the guest at runtime with GuestProgram::from_uri, which takes a file path or file:// URI and returns a Result:

host/src/main.rs
let program = GuestProgram::from_uri(
"target/elf/riscv64ima-zisk-zkvm-elf/release/fibonacci-guest",
)?;

client.setup(&program).run_sync()?;

Prepare the input

With the program set up ready, prepare the input. ZiskStdin is the host-side input channel. Whatever you write into it is exactly what the guest reads with io::read() on the other side. The serialized type must match what the guest expects. In this case a u8:

host/src/main.rs
use fibonacci_common::{fibonacci, U256};
use zisk_sdk::{GuestProgram, ProverClient, ZiskStdin, load_program};

/// Guest ELF binary, embedded into the host at build time.
static PROGRAM: GuestProgram = load_program!("fibonacci-guest");

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

// Instatiating a ProverClient
let client = ProverClient::embedded().build()?;

// One-time setup: derive the proving and verification keys for this guest.
client.setup(&PROGRAM).run_sync()?;

// Write the input into the guest's standard input.
// The type must match what the guest reads: `ziskos::io::read::<u8>()`.
let stdin = ZiskStdin::new();
stdin.write::<u8>(&10);

Ok(())
}

Prove the guest program

Now call client.prove() to generate the proof. It re-executes the guest over the given input, builds arithmetic constraints over the execution trace, and returns a proof, a compact cryptographic object that attests to the correctness of the computation:

host/src/main.rs
use fibonacci_common::{fibonacci, U256};
use zisk_sdk::{GuestProgram, ProverClient, ZiskStdin, load_program};

/// Guest ELF binary, embedded into the host at build time.
static PROGRAM: GuestProgram = load_program!("fibonacci-guest");

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

// Instatiating a ProverClient
let client = ProverClient::embedded().build()?;

// One-time setup: derive the proving and verification keys for this guest.
client.setup(&PROGRAM).run_sync()?;

// Write the input into the guest's standard input.
// The type must match what the guest reads: `ziskos::io::read::<u8>()`.
let stdin = ZiskStdin::new();
stdin.write::<u8>(&10);

// Execute the guest and produce a zero-knowledge proof of the run.
let proof = client.prove(&PROGRAM, stdin).run_sync()?;

Ok(())
}

Verify the proof

Finally, verify the proof and read the public values. Chaining .with_program_vk(&PROGRAM.vk()?) binds verification to this guest's verification key, so .verify() fails if the proof was produced by a different program than the one you expect. Verification checks the proof without re-executing the program. Read the committed result back through proof.get_publics().read::<T>().

host/src/main.rs
use fibonacci_common::{fibonacci, U256};
use zisk_sdk::{load_program, GuestProgram, ProverClient, ZiskStdin};

/// Guest ELF binary, embedded into the host at build time.
static PROGRAM: GuestProgram = load_program!("fibonacci-guest");

fn main() -> Result<(), Box<dyn std::error::Error>> {

// Building the client builder with multiple configurations based on flags.
// The embedded executor runs entirely in-process; asm/gpu layers add acceleration.
let mut builder = ProverClient::embedded().build()?;

// One-time setup: derive the proving and verification keys for this guest.
client.setup(&PROGRAM).run_sync()?;

// Write the input into the guest's standard input.
// The type must match what the guest reads: `ziskos::io::read::<u8>()`.
let stdin = ZiskStdin::new();
stdin.write::<u8>(&10);

// Execute the guest and produce a zero-knowledge proof of the run.
let proof = client.prove(&PROGRAM, stdin).run_sync()?;

// Verify the proof against the guest's verification key.
// Returns an error if the proof is malformed or belongs to a different
// guest program.
if proof.with_program_vk(&PROGRAM.vk()?).verify().is_ok() {
println!("Proof was verified successfully.");
}

// Confirm the committed public output matches the locally computed value.
// The type must match what the guest committed: `ziskos::io::commit(&U256)`.
let expected_output = fibonacci(input);
assert_eq!(proof.get_publics().read::<U256>()?, expected_output);

println!("fibonacci({input}) => {expected_output}");

Ok(())
}

Run the proving pipeline

With all the pieces in place, run the host program. It will set up the keys, feed the input, prove, verify, and print the result in one shot:

bash
cd host
cargo run --release
fibonacci(10) => 55
Proof verified successfully!
Passing arguments to the cloned example

If you cloned the example from the repository, the host accepts trailing arguments after -- to change the input and select the execution backend:

bash
cargo run --release -- <n> --gpu --asm

<n> sets the Fibonacci input, --gpu runs proving on a CUDA GPU, and --asm uses the native Assembly executor (Linux x86_64). All three are optional and can be combined.


Summary

You just took a fibonacci computation and turned it into a cryptographic proof that anyone can verify instantly, without re-running the program or ever seeing the input. That same pipeline applies to any guest program you write.


Next steps

Now that you can prove a program end to end, the following sections cover the options available within the proving pipeline:

If you prefer to keep the defaults for now, jump to Managing guest I/O to further understand how inputs and outputs flow between host and guest.