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:
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:
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:
| Field | Description |
|---|---|
| Proof | The cryptographic object that attests to correct execution. The verifier checks this without re-running the program. |
| Public values | The outputs the guest committed — for the Fibonacci example, the u32 result fibonacci(n). |
| Verification key | A 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:
[dependencies]
zisk-sdk = { workspace = true }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Then import the types you will use:
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:
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:
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:
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:
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:
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:
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:
| Check | Why it matters |
|---|---|
| The proof is cryptographically valid | verify() 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 expect | The 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 expect | A 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:
- On-chain verification: submit and verify PLONK proofs on a blockchain.
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.