Choosing a proof format
This guide covers the three proof types ZisK supports: a STARK as the base type, a Minimal STARK that wraps it into a smaller proof, and a PLONK that makes it verifiable on-chain. The right choice depends on where the proof will be consumed.
Understanding proof types
STARK is always the starting point. Minimal STARK and PLONK are independent post-processing steps that wrap it, each trading extra proving time for a different output property: a smaller proof for off-chain use, or a constant-size proof verifiable on-chain. The two paths are mutually exclusive; you cannot convert a Minimal STARK into a PLONK.
STARK proof
Generates a single aggregated STARK proof by splitting the execution trace across specialized state machines, each proven independently and then recursively combined. Use this when proof size does not matter and you want the fastest turnaround.
Minimal STARK proof
Generates a recursive proof that verifies the STARK final proof, producing a smaller output. Use this when you need a more compact proof for off-chain distribution but do not require on-chain verification.
PLONK proof
Works like compressed, but instead of wrapping the STARK in another STARK it wraps it in a SNARK verifier. The result is a constant-size proof that can be verified on-chain. It is the only proof type the Solidity verifier accepts.
Working on a real example
To try each format against a real proof, set up the gcd example: a
guest that reads two u64 values, computes their greatest common
divisor, and commits the result.
gcd project?If you followed Choosing a prover client, you already
built this exact gcd example, there is nothing to set up again. Skip
straight to Selecting a proof type.
Set up the project
You have two ways to get a working ZisK project for this guide. Pick whichever fits your situation; the rest of the guide is identical either way.
Clone the examples repository
If you want the finished version of the program, clone the companion
examples repo and move into the gcd directory:
git clone https://github.com/0xPolygonHermez/zisk.git
cd zisk/examples/gcd/guest
Scaffold a new project
To start from an empty project and write the program yourself, use the
cargo-zisk CLI to scaffold a new workspace:
cargo-zisk new gcd
cd gcd/guest
Download the sample input this guide uses into a samples/ folder
inside the guest/ crate, where the later commands expect it:
mkdir samples
BASE=https://raw.githubusercontent.com/0xPolygonHermez/zisk/refs/heads/main/examples/gcd
curl -L -o samples/example-input.bin $BASE/guest/samples/example-input.bin
Either path lands you in a project with a guest/ crate where the
ZisK program lives and a common/ crate with the shared logic.
Define the shared logic
Define the gcd helper in the common crate. It is pure logic with no
extra dependencies:
/// Returns the greatest common divisor of `a` and `b` using the Euclidean algorithm.
pub fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let r = a % b;
a = b;
b = r;
}
a
}
Write the guest program
Now open guest/src/main.rs. It reads two u64 values, computes
their GCD, and commits the result as a public output:
#![no_main]
ziskos::entrypoint!(main);
use gcd_common::gcd;
fn main() {
// Read the two inputs from the guest's standard input stream.
let a = ziskos::io::read::<u64>();
let b = ziskos::io::read::<u64>();
// Compute the value we want to prove.
let result = gcd(a, b);
// Commit the result as a public output so a verifier can inspect it
// without re-executing the program.
ziskos::io::commit(&result);
println!("gcd({a}, {b}) => {result}");
}
Build the guest once before generating proofs in the next section:
cargo-zisk build --release
Selecting a proof type
STARK is the default in both the CLI and the SDK. Use the sections below to switch to a different type depending on where you are working from. No other changes to your code are required.
Using cargo-zisk
Pass a flag to cargo-zisk prove to select a different proof type.
No flag means STARK:
# STARK (default)
cargo-zisk prove --release -i samples/example-input.bin
# Minimal STARK
cargo-zisk prove --release -i samples/example-input.bin --minimal
# PLONK
cargo-zisk prove --release -i samples/example-input.bin --plonk
Using zisk-sdk
From the SDK the proof type is selected with .wrap(ProofKind::...) on
the prove builder before .run(); omitting the call leaves the proof at
the default STARK output. We'll build one host binary per format under
host/src/. The first time, build the STARK host step by step; the
other two are near-identical copies of it, shown in full for clarity.
Generate a STARK proof
First, declare the stark binary in host/Cargo.toml:
[[bin]]
name = "stark-host"
path = "src/stark.rs"
Then create host/src/stark.rs, import the types, and load the compiled
guest ELF:
use zisk_sdk::{load_program, GuestProgram, ProverClient, ZiskStdin};
/// Guest ELF binary, embedded into the host at build time.
static PROGRAM: GuestProgram = load_program!("gcd-guest");
Inside main, build the embedded prover, write the two u64 operands
the guest reads, and run the one-time setup. None of this depends on the
proof format:
let client = ProverClient::embedded().build()?;
let stdin = ZiskStdin::new();
stdin.write::<u64>(&12);
stdin.write::<u64>(&18);
// One-time setup for this guest binary.
client.setup(&PROGRAM).run_sync()?;
Now comes the step that selects the proof format. For the STARK baseline, prove without a wrap, then verify, verification is identical for every format:
// STARK (default): no wrap.
let proof = client.prove(&PROGRAM, stdin).run_sync()?;
if proof.with_program_vk(&PROGRAM.vk()?).verify().is_ok() {
println!("Proof was verified successfully.");
}
Putting every step together, the complete host/src/stark.rs is:
use zisk_sdk::{load_program, GuestProgram, ProverClient, ZiskStdin};
/// Guest ELF binary, embedded into the host at build time.
static PROGRAM: GuestProgram = load_program!("gcd-guest");
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ProverClient::embedded().build()?;
// Write the two GCD operands the guest reads.
let stdin = ZiskStdin::new();
stdin.write::<u64>(&12);
stdin.write::<u64>(&18);
// One-time setup for this guest binary.
client.setup(&PROGRAM).run_sync()?;
// STARK (default): no wrap.
let proof = client.prove(&PROGRAM, stdin).run_sync()?;
if proof.with_program_vk(&PROGRAM.vk()?).verify().is_ok() {
println!("Proof was verified successfully.");
}
Ok(())
}
Build and run the stark binary:
cd ../host
cargo run --release --bin stark-host
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 --bin stark-host -- <a> <b> --gpu --asm
<a> and <b> set the GCD 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.
Generate a minimal STARK proof
The Minimal STARK host is the STARK host with a single extra call on the
prove builder. .wrap(ProofKind::VadcopFinalMinimal) runs an additional
recursive proving pass that verifies the STARK final proof and emits a
smaller one in its place. You trade a little extra proving time for a
more compact output that is cheaper to store and transmit off-chain. The
client, input, and setup stay exactly as they were, only the prove call
changes.
Register a second binary:
[[bin]]
name = "minimal-host"
path = "src/minimal.rs"
Then create host/src/minimal.rs: the STARK host with ProofKind added
to the imports and the .wrap(...) appended to the prove call:
use zisk_sdk::{load_program, GuestProgram, ProofKind, ProverClient, ZiskStdin};
/// Guest ELF binary, embedded into the host at build time.
static PROGRAM: GuestProgram = load_program!("gcd-guest");
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ProverClient::embedded().build()?;
let stdin = ZiskStdin::new();
stdin.write::<u64>(&12);
stdin.write::<u64>(&18);
client.setup(&PROGRAM).run_sync()?;
// Minimal STARK: wrap the STARK into a smaller recursive proof.
let proof = client
.prove(&PROGRAM, stdin)
.wrap(ProofKind::VadcopFinalMinimal)
.run_sync()?;
if proof.with_program_vk(&PROGRAM.vk()?).verify().is_ok() {
println!("Proof was verified successfully.");
}
Ok(())
}
Run the minimal binary:
cargo run --release --bin minimal-host
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 --bin minimal-host -- <a> <b> --gpu --asm
<a> and <b> set the GCD 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.
Generate a PLONK proof
The PLONK host wraps the STARK in a SNARK verifier, producing the constant-size proof that the on-chain Solidity verifier accepts. It is the most expensive of the three to generate. Unlike Minimal STARK, it takes two changes from the STARK host:
- The client is built with
.plonk(), which preloads the PLONK (SNARK) proving key. That key is large and not loaded by default, so without this call the prover has nothing to build the PLONK wrapper with and proving fails. - The prove builder wraps with
.wrap(ProofKind::Plonk)instead of leaving the STARK output unwrapped.
Register a third binary:
[[bin]]
name = "plonk-host"
path = "src/plonk.rs"
Then create host/src/plonk.rs with both changes in place:
use zisk_sdk::{load_program, GuestProgram, ProofKind, ProverClient, ZiskStdin};
/// Guest ELF binary, embedded into the host at build time.
static PROGRAM: GuestProgram = load_program!("gcd-guest");
fn main() -> Result<(), Box<dyn std::error::Error>> {
// `.plonk()` preloads the PLONK proving key.
let client = ProverClient::embedded().plonk().build()?;
let stdin = ZiskStdin::new();
stdin.write::<u64>(&12);
stdin.write::<u64>(&18);
client.setup(&PROGRAM).run_sync()?;
// PLONK: wrap the STARK into a constant-size, on-chain-verifiable proof.
let proof = client
.prove(&PROGRAM, stdin)
.wrap(ProofKind::Plonk)
.run_sync()?;
if proof.with_program_vk(&PROGRAM.vk()?).verify().is_ok() {
println!("Proof was verified successfully.");
}
Ok(())
}
Run the plonk binary:
cargo run --release --bin plonk-host
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 --bin plonk-host -- <a> <b> --gpu --asm
<a> and <b> set the GCD 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 now know the three output formats ZisK can produce and exactly when each one makes sense. The default gets you a proof faster so, just reach for Minimal STARK or PLONK only when your target requires it.
Next steps
With proof format covered, the remaining pieces of the pipeline are how inputs flow in and what to do with the result once it is out:
- Managing guest I/O: learn how inputs and outputs flow between host and guest.
- Verifying a proof: load an existing proof from disk and verify it programmatically.