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:
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:
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:
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.
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.
Proof generation can take several minutes depending on your machine.
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:
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:
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:
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:
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:
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()?.
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(())
}
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.
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(())
}
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:
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:
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:
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>().
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:
cd host
cargo run --release
fibonacci(10) => 55
Proof verified successfully!
If you cloned the example from the repository, the host accepts
trailing arguments after -- to change the input and select the
execution backend:
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:
- Choosing a prover client: configure the prover backend and execution mode.
- Choosing a proof format: understand the available proof types and when to use each.
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.