Skip to main content

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:

OptionPlatformDescription
EmulatorAllSoftware emulator. The right default while developing.
AssemblyLinux x86_64Native execution engine. Significantly faster for large programs.

Compute device controls where proving is executed:

OptionDeviceDescription
CPUAnyDefault. Works everywhere.
GPUCUDA-compatible deviceOffloads 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:

bash
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:

bash
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:

bash
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:

common/src/lib.rs
/// 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:

guest/src/main.rs
#![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:

bash
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:

bash
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:

  • --asm picks 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.
  • --gpu picks 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:

BackendFlagRequirements
CPU + Emulator(default, no flags needed)Any platform
GPU + Emulator--gpuCUDA-capable GPU
CPU + Assembly--asmLinux x86_64
GPU + Assembly--asm --gpuLinux x86_64 + CUDA-capable GPU
bash
# 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:

FlagShortDescription
--minimal-memory-mReduce the memory footprint during proving at the cost of speed.
--max-witness-stored <BYTES>-xMaximum memory (in bytes) for witness storage during proving.
--unlock-mapped-memory-uUnlock 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:

bash
cargo-zisk prove --release -i samples/example-input.bin --minimal-memory --max-witness-stored 4294967296

Whichever flags you choose, verify the result locally:

bash
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:

SubcommandPurpose
cargo-zisk remote uploadUpload the guest program to the coordinator.
cargo-zisk remote setupGenerate the program setup on the remote service.
cargo-zisk remote proveGenerate a proof on the remote service.
cargo-zisk remote executeExecute the guest on the remote service.
cargo-zisk remote wrapWrap 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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

host/Cargo.toml
[[bin]]
name = "embedded-host"
path = "src/embedded.rs"
Load the guest program

Import the types and load the compiled guest ELF:

host/src/embedded.rs
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:

host/src/embedded.rs
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:

FieldTypeDescription
minimal_memoryboolReduce memory footprint during proving at the cost of speed.
proving_keyOption<PathBuf>Path to the proving key directory.
proving_key_snarkOption<PathBuf>Path to the PLONK proving key directory.
preload_plonkboolEagerly preload PLONK/SNARK proving keys at startup.
max_witness_storedOption<usize>Maximum memory (bytes) for witness storage during proving.
number_threads_witnessOption<usize>Number of threads for witness generation thread pools.
max_streamsOption<usize>Maximum number of parallel streams during proving.
host/src/embedded.rs
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:

host/src/embedded.rs
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
bash
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.

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 --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:

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):

host/src/remote.rs
// 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 methodDescription
connect_timeout(Duration)Timeout for establishing the connection (default 10 s).
request_timeout(Duration)Timeout for the full proving request (default 300 s).
host/src/remote.rs
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:

bash
cd ../host
cargo run --release --bin remote-host
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 --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: