Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Prover

The zkm_sdk crate provides the tools for proof generation. Its ProverClient lets you:

  • generate the proving and verifying keys with setup();
  • execute a program without proving it with execute();
  • generate proofs with prove();
  • verify proofs with verify().

When generating Groth16 or PLONK proofs, the ProverClient downloads the circuit artifacts of the current release (the proving key from the trusted setup, the verifying key and the Solidity verifier) on first use with try_install_circuit_artifacts(), and caches them in ~/.zkm/circuits/{groth16,plonk}/<version> (for example v2.0.0).

Example: Fibonacci

The following code uses zkm_sdk in a host program.

use zkm_sdk::{include_elf, utils, ProverClient, ZKMProofWithPublicValues, ZKMStdin};

/// The ELF we want to execute inside the zkVM.
const ELF: &[u8] = include_elf!("fibonacci");

fn main() {
    utils::setup_logger();

    let n = 1000u32;

    let mut stdin = ZKMStdin::new();
    stdin.write(&n);

    let client = ProverClient::new();

    let (_, report) = client.execute(ELF, &stdin).run().unwrap();
    println!("executed program with {} cycles", report.total_instruction_count());

    let (pk, vk) = client.setup(ELF);
    let mut proof = client.prove(&pk, stdin).run().unwrap();

    println!("generated proof");

    let _ = proof.public_values.read::<u32>();
    let a = proof.public_values.read::<u32>();
    let b = proof.public_values.read::<u32>();

    println!("a: {}", a);
    println!("b: {}", b);

    client.verify(&proof, &vk).expect("verification failed");

    proof.save("proof-with-pis.bin").expect("saving proof failed");
    let deserialized_proof =
        ZKMProofWithPublicValues::load("proof-with-pis.bin").expect("loading proof failed");

    client.verify(&deserialized_proof, &vk).expect("verification failed");

    println!("successfully generated and verified proof for the program!")
}

Proof Types

The proof mode is chosen on the prove() builder. The proof itself is a ZKMProof:

#![allow(unused)]
fn main() {
pub enum ZKMProof {
    /// A proof generated by the core proof mode.
    ///
    /// The proof size scales linearly with the number of cycles.
    Core(Vec<ShardProof<CoreSC>>),
    /// A proof generated by the compress proof mode.
    ///
    /// The proof size is constant, regardless of the number of cycles.
    Compressed(Box<ZKMReduceProof<InnerSC>>),
    /// A proof generated by the Plonk proof mode.
    Plonk(PlonkBn254Proof),
    /// A proof generated by the Groth16 proof mode.
    Groth16(Groth16Bn254Proof),
    /// A proof generated by the DV-SNARK proof mode.
    DvSnark(DvSnarkBn254Proof),
    /// Compressed-proof-to-Groth16 conversion.
    CompressToGroth16,
}
}

A proof is returned as a ZKMProofWithPublicValues, which bundles the proof, the committed public values and the Ziren version, and can be written and read back with save() and load().

Core Proof (Default)

The default mode produces one STARK proof per shard; the total size grows linearly with the length of the execution.

#![allow(unused)]
fn main() {
let client = ProverClient::new();
client.prove(&pk, stdin).run().unwrap();
}

Compressed Proof

The compressed mode aggregates the shard proofs by recursion into a single STARK proof of constant size. It is verified natively (for example with zkm_verifier::StarkVerifier), but is too large for on-chain verification.

#![allow(unused)]
fn main() {
let client = ProverClient::new();
client.prove(&pk, stdin).compressed().run().unwrap();
}

The Groth16 mode wraps the compressed proof into a Groth16 proof over BN254 of 260 bytes (a 4-byte verifier selector and 8 field elements), verifiable on-chain.

#![allow(unused)]
fn main() {
let client = ProverClient::new();
client.prove(&pk, stdin).groth16().run().unwrap();
}

PLONK Proof

The PLONK mode wraps the compressed proof into a PLONK proof over BN254 of about 868 bytes, also verifiable on-chain. PLONK uses a universal setup (the Aztec Ignition SRS) instead of a circuit-specific trusted setup ceremony.

#![allow(unused)]
fn main() {
let client = ProverClient::new();
client.prove(&pk, stdin).plonk().run().unwrap();
}

Other Modes and Options

  • compress_to_groth16() converts an existing compressed proof into a Groth16 proof. The input stream must hold exactly one compressed proof (written with stdin.write_proof) and the bincode-encoded public values as its only input.
  • The builder also accepts shard_size(), shard_batch_size(), cycle_limit(), with_hook(), and timeout() (network prover only).

Immutable Wrap Verifying Key

By default the Groth16 circuit is specific to one Ziren release. In the immutable-wrap-vk mode (ZKM_IMM_WRAP_VK=1 when building the guest and proving), a single Groth16 verifying key serves all releases: the guest hashes its public values with BLAKE3 instead of SHA-256, and the commitment and start pc of the release's partial STARK verifying key are hashed into the program key hash. zkm-build then builds the guest with its imm-wrap-vk feature, so the guest's Cargo.toml must forward it:

[features]
imm-wrap-vk = ["zkm-zkvm/imm-wrap-vk"]

Such proofs are verified with Groth16Verifier::verify_by_imm_groth16_vk, and their artifacts live in ~/.zkm/circuits/groth16/imm-wrap-vk/<version>. See the imm-wrap-vk-add example.

Hardware Acceleration

GPU Acceleration

Ziren provides a CUDA-based GPU prover, which proves with much lower latency and better cost-performance than the CPU prover.

Software Requirements

Hardware Requirements

  • Processor: 4-core CPU or higher
  • System memory: 16 GB RAM or higher
  • Graphics card: 24 GB VRAM or higher

The NVIDIA GPU must have a compute capability of at least 8.6. You can check yours in the official NVIDIA documentation.

Usage

Select the GPU prover in one of two ways:

  • Option A (environment variable): use ProverClient::new() with ZKM_PROVER=cuda.
  • Option B (direct method): use ProverClient::cuda().

The client sends each proving step over RPC to a GPU prover server. It either starts that server itself in a Docker container (the default) or connects to a server that is already running. Either way, generate proofs with the standard methods; nothing else in the host program changes.

Starting the server in Docker (default)

The client pulls the image, runs it with docker run --rm --gpus <devices> -p <port>:3000, waits for it to come up and removes the container when the program exits or is interrupted. The images are published on the GitHub Container Registry as ghcr.io/projectzkm/ziren-gpu, which lists each tag with its digest. Use the image tagged with the SDK's version, v2.0.0 for Ziren 2.0.0: the server must come from the same release (see below). Because the container receives the private input stream, the image must also be pinned by its digest:

export ZKM_GPU_IMAGE=ghcr.io/projectzkm/ziren-gpu:v2.0.0@sha256:<digest>   # the digest of the published v2.0.0 image
VariableDefaultMeaning
ZKM_GPU_IMAGEnoneThe image, as repo:tag@sha256:<digest>, with the tag of the SDK's version: ghcr.io/projectzkm/ziren-gpu:v2.0.0@sha256:<digest> for Ziren 2.0.0.
ZKM_ALLOW_MUTABLE_GPU_IMAGEunset1 accepts a tag without a digest, such as :v2.0.0 alone; for local development only.
CUDA_VISIBLE_DEVICE_INDEXall GPUsThe one GPU the container uses (--gpus device=<index>).
CUDA_PORT3000Host port the container's server is published on. Give each concurrent client its own port.
RUST_LOGnoneLog level passed to the server.

For example, to prove the Fibonacci example on GPU 0:

export ZKM_PROVER=cuda
export ZKM_GPU_IMAGE=ghcr.io/projectzkm/ziren-gpu:v2.0.0@sha256:<digest>
export CUDA_VISIBLE_DEVICE_INDEX=0
cd examples/fibonacci/host && cargo run --release
Connecting to a running server

To use a server started separately, on this machine or another, turn the container off and give its endpoint:

export ZKM_PROVER=cuda
export CUDA_RUN_DOCKER=false
export CUDA_ENDPOINT=http://<host>:<port>/twirp/   # default: http://localhost:3000/twirp/

The client then starts nothing; CUDA_VISIBLE_DEVICE_INDEX, CUDA_PORT and ZKM_GPU_IMAGE are ignored, and which GPUs are used is the server's configuration. The endpoint must end in /twirp/.

Proof types and GPUs
  • Core and compressed proofs run on all the GPUs the server drives.
  • Groth16 and PLONK proofs, including compress_to_groth16(), add a shrink step that runs only on a server driving a single GPU; on a server driving several, it stops with an error. In Docker mode set CUDA_VISIBLE_DEVICE_INDEX, or run one server per GPU.
  • The server must come from the same Ziren release as the SDK. Compressed and wrapped proofs are checked against that release's set of recursion verifying keys (VERIFY_VK, on by default), and a server from another release produces keys outside the set.

CPU Acceleration

Ziren supports AVX2/AVX512 acceleration on x86 CPUs through Plonky3.

Check your CPU's AVX support with:

grep avx /proc/cpuinfo

and look for avx2 or avx512 in the output.

To enable AVX2, add these flags to your RUSTFLAGS environment variable:

RUSTFLAGS="-C target-cpu=native" cargo run --release

To enable AVX512, add these flags to your RUSTFLAGS environment variable:

RUSTFLAGS="-C target-cpu=native -C target-feature=+avx512f" cargo run --release

Network Prover

The ZKM Proof Network proves programs remotely. The SDK's network prover (the network feature of zkm-sdk) produces compressed and Groth16 proofs, and converts compressed proofs to Groth16 (compress_to_groth16()); it does not produce core or PLONK proofs.

A network proof passes through several stages (queuing, splitting, proving and finalizing), and each stage may take a different amount of time.

Requirements

  • Access: open a proof network access request issue in the Ziren repository. Never post a secret in the issue; the user token is delivered to you privately.
  • The user token, in a file only you can read. It is the only credential: the SDK sends it as Authorization: Bearer on every proof request, over TLS that authenticates the server.
  • SDK dependency: add zkm-sdk with the network feature to your Cargo.toml:
zkm-sdk = { git = "https://github.com/ProjectZKM/Ziren", features = ["network"] }

Environment Variable Setup

Before running your application, export the following environment variables:

export ZKM_PROVER=network
export ENDPOINT=<proof_network_endpoint>        # The network's proof endpoint, e.g. https://<host>:<port>
export FLEET_TOKEN_FILE=<path_to_token_file>    # Your user token
export CA_CERT_PATH=<path_to_ca_certificate>    # Only when the system trust store does not hold the network's CA
export DOMAIN_NAME=<tls_server_name>            # Only when the server's name differs from the endpoint's host

A token bundle issued with fleetctl token issue already sets these.

Optional: customize the network prover's behavior:

export SHARD_SIZE=<shard_size>              # Shard (segment) size requested from the network
export MAX_PROVER_NUM=<max_prover_num>      # Maximum number of provers to use in parallel
export SINGLE_NODE=<true|false>             # Whether to use a single node for proving (default: false)
export ZKM_PROOF_POLL_INTERVAL=<ms>         # How often to poll for the proof status (default: 3000, minimum 100)

Example

The following host uses the network prover:

use zkm_sdk::{include_elf, utils, ProverClient, ZKMStdin};

const FIBONACCI_ELF: &[u8] = include_elf!("fibonacci");

fn main() {
    utils::setup_logger();

    let mut stdin = ZKMStdin::new();
    stdin.write(&10u32);

    // Create a network client (or set ZKM_PROVER=network and use ProverClient::new()).
    let client = ProverClient::network();
    let (pk, vk) = client.setup(FIBONACCI_ELF);
    let proof = client.prove(&pk, stdin).groth16().run().unwrap();
    client.verify(&proof, &vk).unwrap();
}