Proof
Proof bundles a cryptographic proof body, the public values the
guest committed, and the program verification key that pins the proof
to a specific program. Use it to verify, inspect outputs, and persist
for later use.
Overview
A Proof is what a proving run produces. It carries everything an
independent process needs to verify the run — neither the guest ELF
nor the original inputs are required. ProveResult derefs to Proof,
so every method documented below is reachable directly on the result
returned by client.prove(...).
You usually do not construct a Proof yourself. The prover hands you
one through ProveResult, or you load a previously saved one with
Proof::load(...).
Types
Proof
The top-level proof container.
pub struct Proof {
pub body: ProofBody,
pub program_vk: ProgramVK,
}
| Field | Type | Description |
|---|---|---|
body | ProofBody | Kind-tagged proof payload, including the publics. |
program_vk | ProgramVK | Program verification key that pins the proof to a program. |
Publics are not a field on Proof. They live inside the body at
full u64 width, and the PublicValues view is derived on demand by
publics(). This keeps the u32 view from
ever disagreeing with the elements the proof actually committed to.
ProofKind
Identifies the proof format. Derived from ProofBody via
Proof::kind(), not stored on the proof.
pub enum ProofKind {
#[default]
VadcopFinal,
VadcopFinalMinimal,
Plonk,
}
| Variant | Description |
|---|---|
VadcopFinal | Default STARK output. Largest proof, fastest to generate. |
VadcopFinalMinimal | Smaller STARK obtained by wrapping VadcopFinal. Off-chain friendly. |
Plonk | SNARK-wrapped proof, constant size, EVM-verifiable on-chain. |
ProofBody
The kind-tagged proof payload stored in Proof::body. Both variants
carry publics_full — the untruncated u64 field elements the proof
committed to — because a recurser proof's publics exceed 32 bits and
the recursion round-trip must use the exact committed values.
pub enum ProofBody {
Vadcop {
proof: Vec<u64>,
zisk_vk: Vec<u64>,
kind: VadcopKind,
hash: String,
publics_full: Vec<u64>,
},
Plonk {
proof_bytes: Vec<u8>,
plonk_vk: Box<PlonkVkBlob>,
publics: PublicValues,
publics_full: Vec<u64>,
rootc: Vec<u64>,
},
}
Vadcop — produced by VadcopFinal and VadcopFinalMinimal.
| Field | Type | Description |
|---|---|---|
proof | Vec<u64> | Proof, as a flat vector of field elements. |
zisk_vk | Vec<u64> | ZisK verification key. |
kind | VadcopKind | Which vadcop flavor this is, and the is_vadcop_final_proof flag value it implies. |
hash | String | Hash family the proof was generated with. |
publics_full | Vec<u64> | Flag-free program publics [program_vk(4) | inputs(64)], always 68, at full u64 width. |
Plonk — produced by Plonk, for on-chain verification.
| Field | Type | Description |
|---|---|---|
proof_bytes | Vec<u8> | Serialized SNARK proof bytes. |
plonk_vk | Box<PlonkVkBlob> | PLONK verification key blob. Boxed so the common Vadcop variant doesn't carry it inline. |
publics | PublicValues | u32 view of the publics, matching the Solidity calldata layout. |
publics_full | Vec<u64> | Full-width publics. The SNARK's publicsHash is computed over these, not the u32 view. |
rootc | Vec<u64> | The stamped rootCVadcopFinal committed into publicsHash. |
rootc is what the on-chain publicsHash commits to, and it is
not derivable from publics_full or from the verification key
blob: it is the vadcop_final verkey for a plain proof, but the
recurser verkey for an aggregated one. Read it from the Plonk
body rather than reconstructing it. The hash preimage is
program_vk (big-endian) ++ inputs (little-endian) ++ rootc
(big-endian), SHA-256'd and then reduced modulo the BN254 scalar
field.
VadcopKind
Tags which flavor of vadcop proof a Vadcop body holds. It owns the
is_vadcop_final_proof flag value, which is deliberately not
stored in publics_full — STARK paths re-add it via
VadcopKind::stark_publics.
pub enum VadcopKind {
#[default]
Final,
Recurser,
Minimal,
}
| Variant | Publics layout | Flag at index 0 | Meaning |
|---|---|---|---|
Final | [flag | vk(4) | inputs(64)] (69) | 1 | Raw ZisK vadcop_final proof — a recursion leaf. |
Recurser | [flag | vk(4) | inputs(64)] (69) | 0 | Output of a recurser fold (an aggregated proof). |
Minimal | [vk(4) | inputs(64)] (68) | none | Minimal proof; the compressed circuit strips it. |
| Method | Description |
|---|---|
.flag() -> Option<u64> | The flag value at public index 0, or None for Minimal. |
.is_minimal() -> bool | Whether this is the minimal (compressed) proof. |
.from_publics_full(&[u64]) | Classify a raw publics vector as it arrives from the prover, before normalization. |
.stark_publics(&[u64]) -> Vec<u64> | Rebuild the STARK public vector, re-adding the flag for Final / Recurser. |
VadcopKind is defined in zisk_common and is not re-exported by
zisk_sdk. The SDK re-exports Proof, ProofBody, ProofKind,
PublicValues, ProgramVK, PlonkVkBlob, and PlonkVkey; reach for
zisk_common directly if you need to name this type. Most callers
don't — use Proof::kind(), which maps Minimal to
ProofKind::VadcopFinalMinimal and both other variants to
ProofKind::VadcopFinal.
PublicValues
Holds the public outputs the guest committed. Internally a byte
buffer plus an atomic read cursor: each read advances the cursor
by the number of bytes consumed. Reads take &self because the
cursor is atomic.
pub struct PublicValues {
/* private */
}
The encoding follows serde rules: read::<T>() uses bincode for
typed reads; read_slice operates on raw bytes; read_abi::<T>()
decodes Solidity ABI–encoded values.
ProgramVK
The program verification key. Re-exported from zisk_common. It is
derived from the guest ELF (see GuestProgram::vk)
and is what an independent verifier pins a proof to.
pub struct ProgramVK { /* private */ }
Constructors
Proof::load
Load a previously saved proof from disk.
pub fn load(path: impl AsRef<Path>) -> Result<Proof>
| Name | Type | Description |
|---|---|---|
path | impl AsRef<Path> | Path to the proof file on disk. |
The file must have been written by Proof::save (or by tooling
using the same encoding).
use zisk_sdk::Proof;
let proof = Proof::load("my-path/proof.bin")?;
proof.verify()?;
Methods
save
Write the proof to disk. The parent directory must already exist; existing files are overwritten.
pub fn save(&self, path: impl AsRef<Path>) -> Result<()>
| Name | Type | Description |
|---|---|---|
path | impl AsRef<Path> | Destination path for the proof file. |
proof.save("my-path/proof.bin")?;
kind
Return the proof's ProofKind, derived from the body discriminant: a
Vadcop body with VadcopKind::Minimal maps to
VadcopFinalMinimal, any other Vadcop body to VadcopFinal, and a
Plonk body to Plonk.
pub fn kind(&self) -> ProofKind
match proof.kind() {
ProofKind::VadcopFinal => println!("Vadcop final proof"),
ProofKind::VadcopFinalMinimal => println!("Vadcop minimal proof"),
ProofKind::Plonk => println!("Plonk SNARK proof"),
}
is_empty
Return true if the proof carries no proof body (a default /
placeholder proof).
pub fn is_empty(&self) -> bool
publics / publics_full
publics() derives the u32 PublicValues view from whatever the body
holds: for Vadcop it truncates each full-width public to its low 32
bits (the guest / Solidity ABI width), and for Plonk it returns the
stored u32 publics unchanged. Because it is the single derivation
point, the view can never disagree with the underlying source of
truth.
publics_full() returns the untruncated u64 publics
[program_vk(4) | inputs(64)] — what the proof actually committed to,
and what the recursion round-trip needs. It returns None for a
Plonk proof, which has no u64 form of its own.
pub fn publics(&self) -> PublicValues
pub fn publics_full(&self) -> Option<&[u64]>
// u32 view — what the guest wrote and the ABI expects
let publics = proof.publics();
// Full-width elements, when they matter (recurser proofs exceed 32 bits)
if let Some(full) = proof.publics_full() {
println!("committed {} field elements", full.len());
}
get_publics
Alias for publics(). Returns the derived
PublicValues by value — it is a fresh view, not a borrow of
something stored on the proof.
pub fn get_publics(&self) -> PublicValues
let publics = proof.get_publics();
let nonce: u64 = publics.read::<u64>()?;
let digest: [u8; 32] = {
let mut out = [0u8; 32];
publics.read_slice(&mut out);
out
};
get_program_vk
Return the program verification key that pins this proof to its program. Use it to compare against a trusted key held out-of-band.
pub fn get_program_vk(&self) -> &ProgramVK
let vk = proof.get_program_vk();
assert_eq!(vk, &trusted_vk);
get_proof_bytes / get_proof_u64
Return the serialized proof payload, either as raw bytes or as u64
words. The encoding varies by ProofKind. Use these to inspect or
forward the proof body; for the typed structure use the public
body field directly.
pub fn get_proof_bytes(&self) -> Result<Vec<u8>>
pub fn get_proof_u64(&self) -> Result<Vec<u64>>
let bytes = proof.get_proof_bytes()?;
println!("proof bytes: {}", bytes.len());
match &proof.body {
ProofBody::Vadcop { proof, .. } => println!("vadcop words: {}", proof.len()),
ProofBody::Plonk { proof_bytes, .. } => println!("plonk bytes: {}", proof_bytes.len()),
}
get_vadcop_final_proof
Return the VADCOP final proof structure. Errors if the proof is a PLONK proof rather than a VADCOP one.
pub fn get_vadcop_final_proof(&self) -> Result<VadcopFinalProof>
verify
Verify the proof against the public values and program verification key already stored on it.
pub fn verify(&self) -> Result<()>
proof.verify()?;
println!("proof verified");
with_publics / with_program_vk
Begin a verify chain that overrides the embedded public values or
program verification key. Both return a ZiskVerifyBuilder you can
keep chaining before finishing with .verify().
pub fn with_publics<'a>(&'a self, publics: &'a PublicValues) -> ZiskVerifyBuilder<'a>
pub fn with_program_vk<'a>(&'a self, program_vk: &'a ProgramVK) -> ZiskVerifyBuilder<'a>
// Pin verification to an externally-held program VK
let vk = PROGRAM.vk()?;
proof.with_program_vk(&vk).verify()?;
PublicValues methods
The cursor is shared via an AtomicUsize, so reads take &self
even though they advance the cursor. Call head to rewind
before re-reading.
read
Decode the next bincode-encoded value from the buffer and advance the cursor by the bytes consumed.
pub fn read<T>(&self) -> Result<T>
where
T: serde::Serialize + serde::de::DeserializeOwned,
let count: u64 = publics.read::<u64>()?;
let digest: [u8; 32] = publics.read::<[u8; 32]>()?;
read_slice
Copy the next slice.len() bytes from the buffer into slice. The
cursor advances by slice.len().
pub fn read_slice(&self, slice: &mut [u8])
| Name | Type | Description |
|---|---|---|
slice | &mut [u8] | Destination buffer; its length is the count. |
let mut tag = [0u8; 32];
publics.read_slice(&mut tag);
read_abi
Decode the next value using Solidity ABI rules instead of bincode.
Use this when the guest committed its publics with write_abi for
on-chain consumption.
pub fn read_abi<T>(&self) -> Result<T>
where
T: alloy_sol_types::SolValue
+ From<<T::SolType as alloy_sol_types::SolType>::RustType>,
head
Reset the read cursor back to the beginning of the buffer so the next read starts from the first value again.
pub fn head(&self)
let a: u64 = publics.read()?;
publics.head(); // rewind
let a_again: u64 = publics.read()?;