Skip to main content

Verifying a proof

This guide walks through verifying a proof's cryptographic structure, pinning it to the trusted guest verification key, and reading the committed public values. It uses the same Fibonacci example from Your first proof and covers both ways to verify — from the CLI with cargo-zisk and from Rust with zisk-sdk.

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. If you want the finished version of the program this guide verifies, clone the companion examples repo and move into the fibonacci directory:

bash
git clone https://github.com/0xPolygonHermez/zisk-examples.git
cd zisk-examples/fibonacci

To start from an empty project and build it yourself, scaffold a new workspace with cargo-zisk and fill in the guest exactly as described in Your first proof:

bash
cargo-zisk new fibonacci
cd fibonacci

Either path lands you in a project with a guest/ crate that computes Fibonacci and commits its u32 result, a common/ crate, and a host/ crate where you will extend the prover host to also verify the proof it just produced.


What a proof contains

A ZisK proof is not just a cryptographic blob. Proof bundles three things together:

FieldDescription
ProofThe cryptographic object that attests to correct execution. The verifier checks this without re-running the program.
Public valuesThe outputs the guest committed — for the Fibonacci example, the u32 result fibonacci(n).
Verification keyA cryptographic commitment to the guest program itself, derived from the ELF binary. The verifier uses it to confirm the proof corresponds to that exact ELF.

From proof to verification

With a proof in hand, there are two ways to drive verification. The zisk-sdk is the right choice when verification is part of a larger Rust application — checking a proof against expected public values, asserting the program verification key matches a trusted one, or integrating verification into your own logic. cargo-zisk is the right choice when you want a quick terminal check that a file on disk verifies, without writing any code. Both paths perform the same cryptographic check.


Verifying with zisk-sdk

In this section you'll extend the Fibonacci host from Your first proof: right after producing the proof, verify it against the trusted vk and assert the committed public values.

All the code in this section lives in host/src/main.rs. The host is an ordinary Rust program that runs on your machine outside the zkVM.

Add the dependency

Add zisk-sdk to the host crate:

host/Cargo.toml
[dependencies]
zisk-sdk = { workspace = true }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Then import the types you will use:

host/src/main.rs
use zisk_sdk::{GuestProgram, ProverClient, ZiskStdin, load_program};

Verify the proof

A bare proof.verify() only checks that the proof is internally consistent — it does not prove anything about which guest program produced it. To bind the proof to a guest you trust, pin the verification key derived from the trusted ELF and pass it via .with_program_vk(...). In this single-binary example the trusted vk is just the one the in-process setup already produced for PROGRAM:

host/src/main.rs
use zisk_sdk::{GuestProgram, ProverClient, ZiskStdin, load_program};

static PROGRAM: GuestProgram = load_program!("guest");

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

let client = ProverClient::embedded().build()?;
client.setup(&PROGRAM).run()?.await?;

let mut stdin = ZiskStdin::new();
stdin.write::<u32>(&10);
let proof = client.prove(&PROGRAM, stdin).run()?.await?;
println!("Proof generated successfully!");

// Pinning the verification key for the guest we trust
let trusted_vk = client.program_vk(&PROGRAM)?;

// Cryptographic verification, bound to that vk
proof
.with_program_vk(&trusted_vk)
.verify()?;
println!("Proof verified against trusted guest vk!");

Ok(())
}

If the proof was produced by a different guest ELF than the one PROGRAM points at, the chained verify() call returns an error even when the underlying cryptography is well-formed.

Read the public values

Once the proof has been verified against the trusted vk, read the outputs the guest committed. proof.get_publics() returns a &PublicValues that acts as a cursor over the bytes the guest wrote with io::commit. For the Fibonacci example that's a single u32, so one read::<u32>() call drains the cursor:

host/src/main.rs
use zisk_sdk::{GuestProgram, ProverClient, ZiskStdin, load_program};

static PROGRAM: GuestProgram = load_program!("guest");

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

let client = ProverClient::embedded().build()?;
client.setup(&PROGRAM).run()?.await?;

let mut stdin = ZiskStdin::new();
stdin.write::<u32>(&10);
let proof = client.prove(&PROGRAM, stdin).run()?.await?;
println!("Proof generated successfully!");

let trusted_vk = client.program_vk(&PROGRAM)?;
proof
.with_program_vk(&trusted_vk)
.verify()?;
println!("Proof verified against trusted guest vk!");

// Reading the committed result
let result: u32 = proof.get_publics().read::<u32>()?;
println!("fibonacci(10) => {result}");

Ok(())
}

Verify the public values

Verifying the cryptographic proof against the trusted vk might still not be enough on its own. You may also need to confirm that the value the guest committed matches what your application expects. For Fibonacci, that means asserting against the known result for the input you passed:

host/src/main.rs
use zisk_sdk::{GuestProgram, ProverClient, ZiskStdin, load_program};

static PROGRAM: GuestProgram = load_program!("guest");

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

let client = ProverClient::embedded().build()?;
client.setup(&PROGRAM).run()?.await?;

let mut stdin = ZiskStdin::new();
stdin.write::<u32>(&10);
let proof = client.prove(&PROGRAM, stdin).run()?.await?;
println!("Proof generated successfully!");

// Cryptographic check, bound to the trusted vk
let trusted_vk = client.program_vk(&PROGRAM)?;
proof
.with_program_vk(&trusted_vk)
.verify()?;
println!("Proof verified against trusted guest vk!");

// Application-level check
let result: u32 = proof.get_publics().read::<u32>()?;
assert_eq!(result, 55, "fibonacci(10) mismatch");
println!("fibonacci(10) => {result} (matches expected)");

Ok(())
}

The trusted vk here came from the in-process setup, but in many deployments the verifier receives the proof and the canonical verification key from different sources — for example, the vk is published alongside a trusted ELF and the proof arrives over the network. You can also pin a PublicValues value the same way:

host/src/main.rs
proof
.with_publics(&expected_publics)
.with_program_vk(&trusted_vk)
.verify()?;

Run the host

Put it all together — prove, pin the vk, verify, read, assert — and run the host:

bash
cargo run --release
Proof generated successfully!
Proof verified against trusted guest vk!
fibonacci(10) => 55 (matches expected)

The proof was generated, bound to the trusted vk, verified, and its committed result asserted against the expected value — all in a single binary.


Verifying with cargo-zisk

The cargo-zisk CLI exposes the same cryptographic check as a single command. This is the fastest way to confirm that a proof file on disk is well-formed without writing any code, ideal for development, smoke tests, and one-off checks.

Verify the proof

Pass the proof file to cargo-zisk verify with -p and the CLI will check the cryptographic structure without re-executing the guest:

bash
cargo-zisk build --release
cargo-zisk prove -i ../example-input.bin -o ../proof.bin
cargo-zisk verify -p ../proof.bin
INFO: ✓ STARK proof was verified
INFO: --- VERIFICATION SUMMARY ---
INFO: time: 23 milliseconds
INFO: ----------------------------

Anyone with the proof file can run this command and be cryptographically certain that the proof is valid, without seeing the input or repeating the computation.


Important considerations

Verifying the cryptographic proof is necessary but not sufficient. A complete verification strategy covers three things:

CheckWhy it matters
The proof is cryptographically validverify() confirms the proof is well-formed and consistent with the verification key. This is the baseline: it tells you the computation ran correctly inside the zkVM.
The guest program is the one you expectThe verification key is derived from the guest ELF. Without access to the source of that program, the proof says nothing about what was proven — a malicious prover could submit a proof of a trivial guest that reads the desired output as input and commits it directly. The proof would verify, but the computation is meaningless. Audit the guest source.
The committed output matches what you expectA valid proof is not enough on its own for state transitions, cross-chain messages, and similar cases. A proof of a valid but irrelevant state transition is useless. Assert the public values explicitly against your application's expectation, as shown in Verify the public values.

Summary

You now know that verifying a proof means more than checking the cryptography. A trustworthy verification confirms the proof is valid, that the guest program is the one you audited, and that the committed output is the one your application requires — all driven either from a single cargo-zisk verify invocation or from a Rust host program using zisk-sdk that proves, pins the trusted vk, verifies, and asserts the committed values in a single pass.


Next steps

With local verification covered, the natural next step is taking the proof on-chain:

If you want to explore the full set of available APIs and options, head to the SDK reference for a complete picture of what zisk-sdk exposes.