Choosing a prover client
This guide covers the two prover backends ZisK supports: an embedded prover that runs inside your process, and a remote prover that offloads proof generation to an external endpoint. Both are configured at runtime and can produce any proof type.
Understanding prover types
ZisK supports two prover types. Choose based on where you want proof generation to run:
Embedded prover
The embedded prover runs proof generation in the same process as your host program or cli command. It is configured through two independent choices: the execution backend and the compute device.
Executor controls how the guest program is run:
| Option | Platform | Description |
|---|---|---|
| Emulator | All | Software emulator. The right default while developing. |
| Assembly | Linux x86_64 | Native execution engine. Significantly faster for large programs. |
Compute device controls where proving is executed:
| Option | Device | Description |
|---|---|---|
| CPU | Any | Default. Works everywhere. |
| GPU | CUDA-compatible device | Offloads witness generation for faster proving on large programs. |
The two choices combine freely. Emulator with CPU is the default for development; assembly with GPU gives maximum throughput on supported hardware.
Remote prover
The remote prover delegates proof generation to a remote endpoint. Use this when you need more compute than your local machine can provide.
Working on a real example
To try each prover configuration 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.
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 prover
Here is how to configure the prover depending on whether you are
working from the terminal (cargo-zisk) or from Rust (zisk-sdk).
Using cargo-zisk
The CLI proves locally with the embedded prover by default, or offloads the workto a remote coordinator. Both reuse the guest you built above, and the proof they produce is identical.
Proving locally (embedded)
cargo-zisk prove runs the embedded prover in-process. The simplest
invocation uses the default backend, emulator + CPU, which works on
every platform:
cargo-zisk prove --release -i samples/example-input.bin -o proof.bin
The backend executor and device are two independent choices, each toggled by one flag:
--asmpicks the executor, how the guest is run while proving. Without it, ZisK uses the portable software emulator; with it, it uses the native Assembly engine, which is significantly faster on large programs but is only available on Linux x86_64.--gpupicks the device, where witness generation runs. Without it, proving runs on the CPU (works everywhere); with it, it is offloaded to a CUDA-capable GPU for a substantial speedup.
The two flags are orthogonal, so any combination is valid. Passing neither gives the default emulator + CPU, which is the right starting point while developing; adding both gives the fastest configuration on supported hardware:
| Backend | Flag | Requirements |
|---|---|---|
| CPU + Emulator | (default, no flags needed) | Any platform |
| GPU + Emulator | --gpu | CUDA-capable GPU |
| CPU + Assembly | --asm | Linux x86_64 |
| GPU + Assembly | --asm --gpu | Linux x86_64 + CUDA-capable GPU |
# GPU with emulator
cargo-zisk prove --release -i samples/example-input.bin --gpu
# Assembly with CPU (Linux x86_64)
cargo-zisk prove --release -i samples/example-input.bin --asm
# Assembly with GPU (maximum throughput)
cargo-zisk prove --release -i samples/example-input.bin --asm --gpu
A few extra flags enables you to tune the prover by trading speed for a smaller memory footprint:
| Flag | Short | Description |
|---|---|---|
--minimal-memory | -m | Reduce the memory footprint during proving at the cost of speed. |
--max-witness-stored <BYTES> | -x | Maximum memory (in bytes) for witness storage during proving. |
--unlock-mapped-memory | -u | Unlock the memory map for the ROM file. Only applies with --asm. |
For example, to cap witness storage and trim the footprint on a memory-constrained machine:
cargo-zisk prove --release -i samples/example-input.bin --minimal-memory --max-witness-stored 4294967296
Whichever flags you choose, verify the result locally:
cargo-zisk verify -p proof.bin
Proving on a remote host
To offload proving to a remote machine, use the cargo-zisk remote
subcommands, which send work to a coordinator instead of running it
in-process. Point it at your coordinator with --coordinator <URL> on the
remote command, or the ZISK_COORDINATOR_URL environment variable
(default http://localhost:7000). The available subcommands are:
| Subcommand | Purpose |
|---|---|
cargo-zisk remote upload | Upload the guest program to the coordinator. |
cargo-zisk remote setup | Generate the program setup on the remote service. |
cargo-zisk remote prove | Generate a proof on the remote service. |
cargo-zisk remote execute | Execute the guest on the remote service. |
cargo-zisk remote wrap | Wrap an existing proof on the remote service. |
Walk through the remote flow with the gcd example you built above.
You will need a running coordinator; if you don't have one, spin up a
local prover by following the Prover Quickstart.
Point the CLI at it, either with --coordinator <URL> on each command
or once through the environment:
export ZISK_COORDINATOR_URL=http://localhost:7000
Next upload the guest program to send the compiled ELF to the coordinator so the remote service has the program to run and prove:
cargo-zisk remote upload --release
INFO: --- UPLOAD SUMMARY ------------
INFO: Program registered. Hash ID: cdd5ada31316f08206fc99bc289742cea250fe8e87bcaae9a8aae3922c8abf67
Then generate the guest program setup to derive the program key on the remote
service. This is the same one-time preprocessing as the local
cargo-zisk setup, but performed where the proof will be generated, so
you only repeat it when the guest changes:
cargo-zisk remote setup --release
INFO: --- SETUP SUMMARY -------------
INFO: Setup completed for Hash ID: cdd5ada31316f08206fc99bc289742cea250fe8e87bcaae9a8aae3922c8abf67
Once the guest is set, send the input and generate the proof remotely.
remote prove takes the same -i / -o as the local command, and downloads the
finished proof to the -o path:
cargo-zisk remote prove --release -i samples/example-input.bin -o proof.bin
INFO: --- PROVE SUMMARY -------------
INFO: Proof generated in 3.482s, steps: 12843
INFO: Proof saved to proof.bin
Finally verify the proof locally as it is cheap and does not re-execute the guest, so there is no need to do it remotely:
cargo-zisk verify -p proof.bin
INFO: ✓ STARK proof was verified
INFO: --- VERIFICATION SUMMARY ---
INFO: time: 19 milliseconds
INFO: ----------------------------
Using zisk-sdk
From Rust, both backends are selected on the ProverClient builder:
ProverClient::embedded() proves in-process, while
ProverClient::remote(url) offloads to a coordinator. The rest of the
host, input, setup, prove, and verify, is identical either way.
Proving locally (embedded)
We'll build the host as its own binary, so rename host/src/main.rs to
host/src/embedded.rs and declare it in host/Cargo.toml. With the
default main.rs gone, the binary target has to be listed explicitly:
[[bin]]
name = "embedded-host"
path = "src/embedded.rs"
Load the guest program
Import the types and load the compiled guest ELF:
use gcd_common::gcd;
use zisk_sdk::{load_program, EmbeddedOpts, GuestProgram, ProverClient, ZiskStdin};
/// Guest ELF binary, embedded into the host at build time.
static PROGRAM: GuestProgram = load_program!("gcd-guest");
Configure the prover client
This is the step this guide is about. ProverClient::embedded() builds
the in-process prover; the builder methods pick the executor and device,
and EmbeddedOpts tunes how proving runs.
First select the executor and the device. The default is emulator and CPU.
Chain the matching methods to change it, the SDK equivalents of the CLI's
--asm / --gpu flags:
let client = ProverClient::embedded().build()?; // Emulator + CPU (default)
let client = ProverClient::embedded().gpu().build()?; // Emulator + GPU
let client = ProverClient::embedded().assembly().build()?; // Assembly + CPU (Linux x86_64)
let client = ProverClient::embedded().assembly().gpu().build()?; // Assembly + GPU (max throughput)
Now, optionally, tune your prover by passing an EmbeddedOpts to .with_embedded_opts()
for fine-grained control over memory and parallelism. Every field has a
default, so set only the ones you need:
| Field | Type | Description |
|---|---|---|
minimal_memory | bool | Reduce memory footprint during proving at the cost of speed. |
proving_key | Option<PathBuf> | Path to the proving key directory. |
proving_key_snark | Option<PathBuf> | Path to the PLONK proving key directory. |
preload_plonk | bool | Eagerly preload PLONK/SNARK proving keys at startup. |
max_witness_stored | Option<usize> | Maximum memory (bytes) for witness storage during proving. |
number_threads_witness | Option<usize> | Number of threads for witness generation thread pools. |
max_streams | Option<usize> | Maximum number of parallel streams during proving. |
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Default executor (emulator + CPU) with a trimmed memory footprint.
let opts = EmbeddedOpts::default()
.minimal_memory();
let client = ProverClient::embedded().with_embedded_opts(opts).build()?;
Ok(())
}
Prepare the input, prove, and verify
From here the host is exactly the same as in the
Your first proof tutorial: write the inputs into
ZiskStdin, run client.setup(&PROGRAM) and
client.prove(&PROGRAM, stdin), then verify with
proof.with_program_vk(&PROGRAM.vk()?).verify(). The only thing this
guide changes is the client you built in the previous step.
The complete host
Putting every step together:
use gcd_common::gcd;
use zisk_sdk::{load_program, EmbeddedOpts, 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>> {
// Configure the embedded prover. Default is emulator + CPU; chain
// `.assembly()` and/or `.gpu()` to change the backend.
let opts = EmbeddedOpts::default().minimal_memory();
let client = ProverClient::embedded().with_embedded_opts(opts).build()?;
// Write the two GCD operands the guest reads, in order.
let stdin = ZiskStdin::new();
stdin.write::<u64>(&12);
stdin.write::<u64>(&18);
// One-time setup, then prove.
client.setup(&PROGRAM).run_sync()?;
let proof = client.prove(&PROGRAM, stdin).run_sync()?;
// Verify against this guest's verification key and check the output.
if proof.with_program_vk(&PROGRAM.vk()?).verify().is_ok() {
println!("Proof was verified successfully.");
}
let expected = gcd(12, 18);
assert_eq!(proof.get_publics().read::<u64>()?, expected);
println!("gcd(12, 18) => {expected}");
Ok(())
}
Run the host
cd ../host
cargo run --release --bin embedded-host
The configuration is fixed in the source, so changing the backend means
editing the ProverClient line and rebuilding.
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 embedded-host -- <a> <b> --gpu --asm
<a> and <b> sets 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.
Proving on a remote host
To prove on a remote coordinator instead of the embedded prover, copy
host/src/embedded.rs to host/src/remote.rs and register it as a
second binary in host/Cargo.toml:
[[bin]]
name = "remote-host"
path = "src/remote.rs"
Everything in the host stays the same except the client builder, swap
ProverClient::embedded() for ProverClient::remote(url):
// Replace the embedded client with a remote one pointed at your coordinator:
let client = ProverClient::remote("http://localhost:7000").build()?;
To tune the connection settings, chain these builder methods before .build()
to control timeouts:
| Builder method | Description |
|---|---|
connect_timeout(Duration) | Timeout for establishing the connection (default 10 s). |
request_timeout(Duration) | Timeout for the full proving request (default 300 s). |
use std::time::Duration;
let client = ProverClient::remote("http://localhost:7000")
.connect_timeout(Duration::from_secs(10))
.request_timeout(Duration::from_secs(600))
.build()?;
Once you've applied the changes, run it like the embedded binary, selecting the target with --bin:
cd ../host
cargo run --release --bin remote-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 remote-host -- <a> <b> --gpu --asm
<a> and <b> set the GCD input. The coordinator endpoint, however, is not a
command-line argument: it is hardcoded in the ProverClient::remote("...") call.
To point at a different coordinator you must edit the URL in host/src/remote.rs
and rebuild.
Summary
You now know how to choose and configure a prover client. The
embedded prover runs proving in-process, with the executor
(emulator or Assembly) and device (CPU or GPU) selected by the
--asm / --gpu flags from cargo-zisk, or the .assembly() /
.gpu() builder methods and EmbeddedOpts from zisk-sdk. The
remote prover offloads the same work to a coordinator, through the
cargo-zisk remote subcommands or ProverClient::remote(url).
Whichever path you take, the guest program and the proof it produces are identical, only where and how proving runs changes, so you can start on the embedded emulator while developing and move to assembly, GPU, or a remote coordinator as your proving needs grow.
Next steps
With the prover client configured, the next decision is what kind of proof it should produce:
- Choosing a proof format: choose the right proof type for your use case.
- Managing guest I/O: learn how inputs and outputs flow between host and guest.