use crate::error::{CommonError, Result};
use proofman::{verify_snark_proof, SnarkProof, SnarkProtocol};
use proofman_verifier::VadcopFinalProof;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fs::File;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
pub use zisk_verifier::{
program_publics, GOLDILOCKS_ORDER, IS_VADCOP_FINAL_PROOF, PROGRAM_VK_LEN,
VADCOP_FINAL_FLAG_LEN, ZISK_PUBLICS,
};
use crate::HashMode;
#[inline]
fn canonical(word: u64) -> u64 {
if word >= GOLDILOCKS_ORDER {
word - GOLDILOCKS_ORDER
} else {
word
}
}
fn ensure_stored_publics(body: &ProofBody) -> Result<()> {
let flag_free = PROGRAM_VK_LEN + ZISK_PUBLICS;
let (publics_full, flagged_ok) = match body {
ProofBody::Vadcop { publics_full, .. } => (publics_full.as_slice(), false),
ProofBody::Plonk { publics_full, .. } => (publics_full.as_slice(), true),
};
let ok = publics_full.len() == flag_free
|| (flagged_ok && publics_full.len() == VADCOP_FINAL_FLAG_LEN + flag_free);
if !ok {
return Err(CommonError::InvalidProof(format!(
"stored publics have {} field elements, expected {flag_free}",
publics_full.len()
)));
}
ensure_canonical_publics(publics_full)
}
fn ensure_canonical_publics(publics_full: &[u64]) -> Result<()> {
let normalized = program_publics(publics_full);
if normalized.len() != PROGRAM_VK_LEN + ZISK_PUBLICS {
return Err(CommonError::InvalidProof(format!(
"committed publics have {} field elements (after flag strip), expected {}",
normalized.len(),
PROGRAM_VK_LEN + ZISK_PUBLICS
)));
}
if let Some(i) = publics_full.iter().position(|&w| w >= GOLDILOCKS_ORDER) {
return Err(CommonError::InvalidProof(format!(
"public {i} is not a canonical Goldilocks element: {} >= {GOLDILOCKS_ORDER}",
publics_full[i]
)));
}
Ok(())
}
fn splice_program_vk(publics_full: &[u64], vk: &[u64]) -> Result<Vec<u64>> {
if publics_full.len() < PROGRAM_VK_LEN {
return Err(CommonError::InvalidProof(format!(
"committed publics too short to hold a program VK: {} < {PROGRAM_VK_LEN}",
publics_full.len()
)));
}
let mut out = program_publics(publics_full).to_vec();
out[..PROGRAM_VK_LEN].copy_from_slice(vk);
Ok(out)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SetupKey {
pub hash_id: String,
pub with_hints: bool,
pub emulator_only: bool,
}
impl SetupKey {
pub fn new(hash_id: impl Into<String>, with_hints: bool, emulator_only: bool) -> Self {
Self { hash_id: hash_id.into(), with_hints, emulator_only }
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct ProgramVK {
pub vk: Vec<u64>,
pub hash_mode: HashMode,
}
impl ProgramVK {
pub fn new_from_publics_with_mode(publics: &[u64], hash_mode: HashMode) -> Self {
let publics = program_publics(publics);
assert!(
publics.len() >= PROGRAM_VK_LEN,
"Not enough u64 publics to extract program VK (expected at least {})",
PROGRAM_VK_LEN
);
Self { vk: publics[..PROGRAM_VK_LEN].to_vec(), hash_mode }
}
pub fn new_from_publics(publics: &[u64]) -> Self {
Self::new_from_publics_with_mode(publics, HashMode::default())
}
pub fn new_empty() -> Self {
Self { vk: vec![0u64; PROGRAM_VK_LEN], hash_mode: HashMode::default() }
}
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum VadcopKind {
#[default]
Final,
Recurser,
Minimal,
}
impl VadcopKind {
pub fn flag(self) -> Option<u64> {
match self {
VadcopKind::Final => Some(IS_VADCOP_FINAL_PROOF),
VadcopKind::Recurser => Some(0),
VadcopKind::Minimal => None,
}
}
pub fn is_minimal(self) -> bool {
matches!(self, VadcopKind::Minimal)
}
pub fn from_publics_full(publics_full: &[u64]) -> Self {
if publics_full.len() == VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN + ZISK_PUBLICS {
if publics_full[0] == 0 {
VadcopKind::Recurser
} else {
VadcopKind::Final
}
} else if publics_full.len() == PROGRAM_VK_LEN + ZISK_PUBLICS {
VadcopKind::Minimal
} else {
VadcopKind::Final
}
}
pub fn stark_publics(self, publics_full: &[u64]) -> Vec<u64> {
match self.flag() {
Some(flag) => {
let mut v = Vec::with_capacity(VADCOP_FINAL_FLAG_LEN + publics_full.len());
v.push(flag);
v.extend_from_slice(publics_full);
v
}
None => publics_full.to_vec(),
}
}
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProofKind {
#[default]
VadcopFinal,
VadcopFinalMinimal,
Plonk,
}
impl From<i32> for ProofKind {
fn from(v: i32) -> Self {
match v {
1 => ProofKind::VadcopFinalMinimal,
2 => ProofKind::Plonk,
_ => ProofKind::VadcopFinal,
}
}
}
impl From<ProofKind> for i32 {
fn from(k: ProofKind) -> Self {
match k {
ProofKind::VadcopFinal => 0,
ProofKind::VadcopFinalMinimal => 1,
ProofKind::Plonk => 2,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlonkVkey {
pub protocol: String,
pub curve: String,
#[serde(rename = "nPublic")]
pub n_public: u32,
pub power: u32,
pub k1: String,
pub k2: String,
#[serde(rename = "Qm")]
pub qm: [String; 3],
#[serde(rename = "Ql")]
pub ql: [String; 3],
#[serde(rename = "Qr")]
pub qr: [String; 3],
#[serde(rename = "Qo")]
pub qo: [String; 3],
#[serde(rename = "Qc")]
pub qc: [String; 3],
#[serde(rename = "S1")]
pub s1: [String; 3],
#[serde(rename = "S2")]
pub s2: [String; 3],
#[serde(rename = "S3")]
pub s3: [String; 3],
#[serde(rename = "X_2")]
pub x_2: [[String; 2]; 3],
pub w: String,
}
impl PlonkVkey {
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let file = File::open(path.as_ref()).map_err(|e| {
CommonError::Io(format!(
"failed to open file for loading PlonkVkey: {}: {e}",
path.as_ref().display()
))
})?;
let vkey: PlonkVkey = serde_json::from_reader(file).map_err(|e| {
CommonError::Deserialization(format!(
"failed to parse PlonkVkey JSON from {}: {e}",
path.as_ref().display()
))
})?;
Ok(vkey)
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
CommonError::Io(format!(
"failed to create parent directory {}: {e}",
parent.display()
))
})?;
}
let file = File::create(path).map_err(|e| {
CommonError::Io(format!(
"failed to create file for saving PlonkVkey: {}: {e}",
path.display()
))
})?;
serde_json::to_writer_pretty(file, self).map_err(|e| {
CommonError::Serialization(format!("PlonkVkey JSON to {}: {e}", path.display()))
})?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlonkVkBlob {
pub vadcop_vk: Vec<u64>,
pub plonk_vkey: PlonkVkey,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct PublicValues {
data: Vec<u8>,
#[serde(skip)]
ptr: AtomicUsize,
}
impl Clone for PublicValues {
fn clone(&self) -> Self {
Self { data: self.data.clone(), ptr: AtomicUsize::new(self.ptr.load(Ordering::Relaxed)) }
}
}
impl PublicValues {
pub fn new(publics_bytes: &[u8]) -> Self {
assert!(
publics_bytes.len() == ZISK_PUBLICS * 8 + 32,
"Not enough bytes to fill PublicValues"
);
let mut data = [0u8; ZISK_PUBLICS * 4];
for (i, chunk) in publics_bytes[32..].chunks_exact(8).enumerate() {
let v32 = u32::from_le_bytes(chunk[0..4].try_into().unwrap());
data[i * 4..(i + 1) * 4].copy_from_slice(&v32.to_le_bytes());
}
Self { data: data.to_vec(), ptr: AtomicUsize::new(0) }
}
pub fn new_from_u64(publics: &[u64]) -> Self {
let publics = program_publics(publics);
assert!(
publics.len() == ZISK_PUBLICS + PROGRAM_VK_LEN,
"Expected {} u64 publics, got {}",
ZISK_PUBLICS + PROGRAM_VK_LEN,
publics.len()
);
let mut data = [0u8; ZISK_PUBLICS * 4];
for (i, &val) in publics[PROGRAM_VK_LEN..].iter().enumerate() {
data[i * 4..(i + 1) * 4].copy_from_slice(&(canonical(val) as u32).to_le_bytes());
}
Self { data: data.to_vec(), ptr: AtomicUsize::new(0) }
}
pub fn new_empty() -> Self {
Self { data: [0u8; ZISK_PUBLICS * 4].to_vec(), ptr: AtomicUsize::new(0) }
}
pub fn write<T: serde::Serialize>(value: &T) -> Result<Self> {
let serialized = bincode::serde::encode_to_vec(value, bincode::config::standard())
.map_err(|e| CommonError::Serialization(e.to_string()))?;
if serialized.len() > ZISK_PUBLICS * 4 {
return Err(CommonError::Invalid(format!(
"Serialized data too large: {} bytes (max {} bytes)",
serialized.len(),
ZISK_PUBLICS * 4
)));
}
let mut data = [0u8; ZISK_PUBLICS * 4];
for (i, chunk) in serialized.chunks(4).enumerate() {
let mut buf = [0u8; 4];
buf[..chunk.len()].copy_from_slice(chunk);
data[i * 4..(i + 1) * 4].copy_from_slice(&buf);
}
Ok(Self { data: data.to_vec(), ptr: AtomicUsize::new(0) })
}
pub fn write_abi<T: alloy_sol_types::SolValue>(value: &T) -> Result<Self> {
let encoded = value.abi_encode();
if encoded.len() > ZISK_PUBLICS * 4 {
return Err(CommonError::Invalid(format!(
"ABI encoded data too large: {} bytes (max {} bytes)",
encoded.len(),
ZISK_PUBLICS * 4
)));
}
let mut data = [0u8; ZISK_PUBLICS * 4];
for (i, chunk) in encoded.chunks(4).enumerate() {
let mut buf = [0u8; 4];
buf[..chunk.len()].copy_from_slice(chunk);
data[i * 4..(i + 1) * 4].copy_from_slice(&buf);
}
Ok(Self { data: data.to_vec(), ptr: AtomicUsize::new(0) })
}
pub fn head(&self) {
self.ptr.store(0, Ordering::Relaxed);
}
pub fn read_slice(&self, slice: &mut [u8]) {
let ptr = self.ptr.load(Ordering::Relaxed);
slice.copy_from_slice(&self.data[ptr..ptr + slice.len()]);
self.ptr.store(ptr + slice.len(), Ordering::Relaxed);
}
pub fn read<T: serde::Serialize + serde::de::DeserializeOwned>(&self) -> Result<T> {
let ptr = self.ptr.load(Ordering::Relaxed);
let (result, nb_bytes): (T, usize) =
bincode::serde::decode_from_slice(&self.data[ptr..], bincode::config::standard())
.map_err(|e| CommonError::Deserialization(e.to_string()))?;
self.ptr.store(ptr + nb_bytes, Ordering::Relaxed);
Ok(result)
}
pub fn read_abi<T>(&self) -> Result<T>
where
T: alloy_sol_types::SolValue + From<<T::SolType as alloy_sol_types::SolType>::RustType>,
{
let ptr = self.ptr.load(Ordering::Relaxed);
let decoded = T::abi_decode(&self.data[ptr..])
.map_err(|e| CommonError::AbiDecoding(e.to_string()))?;
let encoded_size = decoded.abi_encode().len();
self.ptr.store(ptr + encoded_size, Ordering::Relaxed);
Ok(decoded)
}
pub fn public_u64(&self) -> Vec<u64> {
(0..ZISK_PUBLICS)
.map(|i| {
let start = i * 4;
u32::from_le_bytes([
self.data[start],
self.data[start + 1],
self.data[start + 2],
self.data[start + 3],
]) as u64
})
.collect()
}
pub fn hash_solidity(&self, program_vk: &ProgramVK, vadcop_verkey: &[u64]) -> Vec<u8> {
let bytes = self.bytes_solidity(program_vk, vadcop_verkey);
let hash = Sha256::digest(&bytes);
hash.to_vec()
}
}
impl PublicValues {
pub fn bytes_solidity(&self, program_vk: &ProgramVK, vadcop_verkey: &[u64]) -> Vec<u8> {
let mut prefix = [0u8; PROGRAM_VK_LEN * 8];
for (i, val) in program_vk.vk.iter().enumerate() {
prefix[i * 8..(i + 1) * 8].copy_from_slice(&val.to_be_bytes());
}
let mut bytes = prefix.to_vec();
bytes.extend_from_slice(&self.data);
let mut suffix = [0u8; PROGRAM_VK_LEN * 8];
for (i, val) in vadcop_verkey.iter().enumerate() {
suffix[i * 8..(i + 1) * 8].copy_from_slice(&val.to_be_bytes());
}
bytes.extend(&suffix);
bytes
}
}
pub fn snark_inputs_bytes(publics_full: &[u64]) -> Vec<u8> {
let publics_full = program_publics(publics_full);
assert!(
publics_full.len() >= PROGRAM_VK_LEN + ZISK_PUBLICS,
"publics_full too short for snark inputs"
);
publics_full[PROGRAM_VK_LEN..PROGRAM_VK_LEN + ZISK_PUBLICS]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect()
}
pub fn snark_publics_hash(publics_full: &[u64], rootc: &[u64]) -> Vec<u8> {
assert!(rootc.len() >= PROGRAM_VK_LEN, "rootc too short for snark hash");
let publics_full = program_publics(publics_full);
let program_vk = &publics_full[..PROGRAM_VK_LEN];
let mut preimage = Vec::with_capacity((2 * PROGRAM_VK_LEN + ZISK_PUBLICS) * 8);
preimage.extend(program_vk.iter().flat_map(|v| v.to_be_bytes())); preimage.extend(snark_inputs_bytes(publics_full)); preimage.extend(rootc[..PROGRAM_VK_LEN].iter().flat_map(|v| v.to_be_bytes())); let digest = Sha256::digest(&preimage);
let bn254 = num_bigint::BigUint::parse_bytes(
b"21888242871839275222246405745257275088548364400416034343698204186575808495617",
10,
)
.expect("valid BN254 modulus");
let reduced = num_bigint::BigUint::from_bytes_be(&digest) % bn254;
let mut out = reduced.to_bytes_be();
out.splice(0..0, std::iter::repeat(0u8).take(32 - out.len())); out
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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>,
},
}
impl Default for ProofBody {
fn default() -> Self {
ProofBody::Vadcop {
proof: Vec::new(),
zisk_vk: vec![0u64; PROGRAM_VK_LEN],
kind: VadcopKind::Final,
hash: String::new(),
publics_full: vec![0u64; PROGRAM_VK_LEN + ZISK_PUBLICS],
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct Proof {
pub body: ProofBody,
pub program_vk: ProgramVK,
}
pub struct ZiskVerifyBuilder<'a> {
proof_with_values: &'a Proof,
override_publics: Option<&'a PublicValues>,
override_program_vk: Option<&'a ProgramVK>,
trusted_plonk_vk: Option<&'a PlonkVkey>,
trusted_setup_vk: Option<&'a [u64]>,
}
impl<'a> ZiskVerifyBuilder<'a> {
fn new(proof_with_values: &'a Proof) -> Self {
Self {
proof_with_values,
override_publics: None,
override_program_vk: None,
trusted_plonk_vk: None,
trusted_setup_vk: None,
}
}
pub fn with_publics(mut self, publics: &'a PublicValues) -> Self {
self.override_publics = Some(publics);
self
}
pub fn with_program_vk(mut self, program_vk: &'a ProgramVK) -> Self {
self.override_program_vk = Some(program_vk);
self
}
pub fn with_plonk_vk(mut self, plonk_vkey: &'a PlonkVkey) -> Self {
self.trusted_plonk_vk = Some(plonk_vkey);
self
}
pub fn with_setup_vk(mut self, setup_vk: &'a [u64]) -> Self {
self.trusted_setup_vk = Some(setup_vk);
self
}
pub fn verify(self) -> Result<()> {
ensure_stored_publics(&self.proof_with_values.body)?;
let derived_publics = self.proof_with_values.publics();
let publics = self.override_publics.unwrap_or(&derived_publics);
let program_vk = self.override_program_vk.unwrap_or(&self.proof_with_values.program_vk);
if let Some(pv) = self.override_program_vk {
if pv.vk.len() != PROGRAM_VK_LEN {
return Err(CommonError::InvalidProof(format!(
"program vk override must have exactly {PROGRAM_VK_LEN} u64 limbs, got {}",
pv.vk.len()
)));
}
if !zisk_verifier::publics_are_canonical(&pv.vk) {
return Err(CommonError::InvalidProof(
"program vk override has a non-canonical Goldilocks limb".to_string(),
));
}
}
if let Some(vk) = self.trusted_setup_vk {
if !zisk_verifier::publics_are_canonical(vk) {
return Err(CommonError::InvalidProof(
"setup vk override has a non-canonical Goldilocks limb".to_string(),
));
}
}
if let Some(pv) = self.override_publics {
if pv.data.len() != ZISK_PUBLICS * 4 {
return Err(CommonError::InvalidProof(format!(
"publics override must be {} bytes ({ZISK_PUBLICS} u32 values), got {}",
ZISK_PUBLICS * 4,
pv.data.len()
)));
}
}
match &self.proof_with_values.body {
ProofBody::Plonk { proof_bytes, plonk_vk, publics_full, rootc, .. } => {
let plonk_vkey = self.trusted_plonk_vk.unwrap_or(&plonk_vk.plonk_vkey);
let rootc = self.trusted_setup_vk.unwrap_or(rootc.as_slice());
if rootc.len() != PROGRAM_VK_LEN {
return Err(CommonError::InvalidProof(format!(
"setup vk (`rootc`) must have exactly {PROGRAM_VK_LEN} u64 limbs, got {}",
rootc.len()
)));
}
let public_snark_bytes = match (self.override_publics, self.override_program_vk) {
(None, None) => snark_publics_hash(publics_full, rootc),
(None, Some(pv)) => {
snark_publics_hash(&splice_program_vk(publics_full, &pv.vk)?, rootc)
}
(Some(_), _) => {
let committed = program_publics(publics_full);
let vk_limbs: &[u64] = match self.override_program_vk {
Some(pv) => &pv.vk,
None => &committed[..PROGRAM_VK_LEN],
};
let mut pf = vk_limbs.to_vec();
pf.extend(publics.public_u64());
snark_publics_hash(&pf, rootc)
}
};
let snark_proof = SnarkProof {
proof_bytes: proof_bytes.clone(),
public_bytes: Vec::new(),
public_snark_bytes,
protocol_id: SnarkProtocol::Plonk.protocol_id(),
};
let temp_dir = std::env::temp_dir();
let unique_id = format!(
"{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let temp_file = temp_dir.join(format!("plonk_vkey_{}.json", unique_id));
let plonk_vkey_json = serde_json::to_vec(plonk_vkey)
.map_err(|e| CommonError::Serialization(format!("PlonkVkey to JSON: {e}")))?;
std::fs::write(&temp_file, &plonk_vkey_json).map_err(|e| {
CommonError::Io(format!(
"Failed to write PlonkVkey to temporary file: {}: {e}",
temp_file.display()
))
})?;
let result = verify_snark_proof(&snark_proof, &temp_file);
if temp_file.exists() {
std::fs::remove_file(&temp_file).map_err(|e| {
CommonError::Io(format!(
"Failed to delete temporary file: {}: {e}",
temp_file.display()
))
})?;
}
result.map_err(|e| {
CommonError::Invalid(format!("snark proof verification failed: {e}"))
})?;
Ok(())
}
ProofBody::Vadcop { proof, zisk_vk, kind, hash, publics_full } => {
let kind = *kind;
if self.trusted_plonk_vk.is_some() {
return Err(CommonError::InvalidProof(
"a PLONK verification key was pinned (with_plonk_vk) for a non-PLONK proof"
.to_string(),
));
}
if program_vk.hash_mode.as_str() != hash {
return Err(CommonError::InvalidProof(format!(
"verkey hash mode {} does not match proof hash family {hash:?}",
program_vk.hash_mode.as_str()
)));
}
let setup_vk = self.trusted_setup_vk.unwrap_or(zisk_vk.as_slice());
if setup_vk.len() != PROGRAM_VK_LEN {
return Err(CommonError::InvalidProof(format!(
"setup vk must have exactly {PROGRAM_VK_LEN} u64 limbs, got {}",
setup_vk.len()
)));
}
if kind == VadcopKind::Recurser {
let declared: &[u64] = match self.override_program_vk {
Some(pv) => &pv.vk,
None => &program_publics(publics_full)[..PROGRAM_VK_LEN],
};
if declared != setup_vk {
return Err(CommonError::InvalidProof(format!(
"recurser proof declares recursion domain {declared:?} but verifies \
under {setup_vk:?}; its subtree was not produced by this recurser"
)));
}
}
let Some(expected_len) =
zisk_verifier::expected_proof_bytes(hash, kind.is_minimal())
else {
return Err(CommonError::InvalidProof(format!(
"no {:?} stage exists for hash family {hash:?}",
self.proof_with_values.kind()
)));
};
if proof.len() * 8 != expected_len {
return Err(CommonError::InvalidProof(format!(
"Malformed proof: expected {} bytes for {:?}, got {}",
expected_len,
self.proof_with_values.kind(),
proof.len() * 8
)));
}
let pubs_u64 = match (self.override_publics, self.override_program_vk) {
(None, None) => kind.stark_publics(publics_full),
(None, Some(pv)) => {
kind.stark_publics(&splice_program_vk(publics_full, &pv.vk)?)
}
(Some(_), _) => {
let committed = program_publics(publics_full);
let vk_limbs: &[u64] = match self.override_program_vk {
Some(pv) => &pv.vk,
None => &committed[..PROGRAM_VK_LEN],
};
let mut v = Vec::with_capacity(
kind.flag().map_or(0, |_| VADCOP_FINAL_FLAG_LEN)
+ PROGRAM_VK_LEN
+ ZISK_PUBLICS,
);
if let Some(flag) = kind.flag() {
v.push(flag);
}
v.extend_from_slice(vk_limbs);
v.extend(publics.public_u64());
v
}
};
let vadcop_final_proof =
VadcopFinalProof::new(proof.clone(), pubs_u64, kind.is_minimal(), hash.clone());
let is_valid = zisk_verifier::verify_vadcop_final(&vadcop_final_proof, setup_vk);
if !is_valid {
Err(CommonError::NotVerified)
} else {
Ok(())
}
}
}
}
}
impl Proof {
pub fn new(body: ProofBody, program_vk: ProgramVK) -> Self {
Self { body, program_vk }
}
pub fn publics(&self) -> PublicValues {
let committed = program_publics(self.committed_publics());
if committed.len() == PROGRAM_VK_LEN + ZISK_PUBLICS {
return PublicValues::new_from_u64(committed);
}
let mut data = [0u8; ZISK_PUBLICS * 4];
for (i, &val) in committed.iter().skip(PROGRAM_VK_LEN).take(ZISK_PUBLICS).enumerate() {
data[i * 4..(i + 1) * 4].copy_from_slice(&(canonical(val) as u32).to_le_bytes());
}
PublicValues { data: data.to_vec(), ptr: AtomicUsize::new(0) }
}
pub fn try_publics(&self) -> Result<PublicValues> {
let committed = self.committed_publics();
ensure_canonical_publics(committed)?;
Ok(PublicValues::new_from_u64(committed))
}
pub fn publics_full(&self) -> Option<&[u64]> {
match &self.body {
ProofBody::Vadcop { publics_full, .. } => Some(publics_full),
ProofBody::Plonk { .. } => None,
}
}
pub fn kind(&self) -> ProofKind {
match &self.body {
ProofBody::Vadcop { kind: VadcopKind::Minimal, .. } => ProofKind::VadcopFinalMinimal,
ProofBody::Vadcop { .. } => ProofKind::VadcopFinal,
ProofBody::Plonk { .. } => ProofKind::Plonk,
}
}
pub fn is_empty(&self) -> bool {
match &self.body {
ProofBody::Vadcop { proof, .. } => proof.is_empty(),
ProofBody::Plonk { proof_bytes, .. } => proof_bytes.is_empty(),
}
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
CommonError::Io(format!(
"failed to create parent directory {}: {e}",
parent.display()
))
})?;
}
let mut file = File::create(path).map_err(|e| {
CommonError::Io(format!(
"failed to create file for saving proof: {}: {e}",
path.display()
))
})?;
bincode::serde::encode_into_std_write(self, &mut file, bincode::config::standard())
.map(|_| ())
.map_err(|e| CommonError::Io(format!("Failed to save proof: {}", e)))
}
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let mut file = File::open(path.as_ref()).map_err(|e| {
CommonError::Io(format!(
"failed to open file for loading proof: {}: {e}",
path.as_ref().display()
))
})?;
let proof: Proof =
bincode::serde::decode_from_std_read(&mut file, bincode::config::standard())
.map_err(|e| CommonError::Io(format!("Failed to load proof: {}", e)))?;
ensure_stored_publics(&proof.body)?;
Ok(proof)
}
fn committed_publics(&self) -> &[u64] {
match &self.body {
ProofBody::Vadcop { publics_full, .. } | ProofBody::Plonk { publics_full, .. } => {
publics_full
}
}
}
pub fn get_vadcop_final_proof(&self) -> Result<VadcopFinalProof> {
match &self.body {
ProofBody::Vadcop { proof, kind, hash, publics_full, .. } => {
ensure_stored_publics(&self.body)?;
Ok(VadcopFinalProof::new(
proof.clone(),
kind.stark_publics(publics_full),
kind.is_minimal(),
hash.clone(),
))
}
ProofBody::Plonk { .. } => {
Err(CommonError::InvalidProof("Proof is not a Vadcop final proof".to_string()))
}
}
}
pub fn get_vadcop_final_proof_to_aggregate(&self) -> Result<VadcopFinalProof> {
if let ProofBody::Vadcop { kind, .. } = &self.body {
if kind.is_minimal() {
return Err(CommonError::InvalidProof(
"a compressed (minimal) proof cannot be aggregated: compression strips \
the is_vadcop_final_proof flag the aggregator reads at public slot 0, \
and it cannot be recovered. Produce the leaf uncompressed \
(ProofKind::VadcopFinal) if you intend to fold it — note the embedded \
client defaults to ProofKind::VadcopFinalMinimal"
.to_string(),
));
}
}
self.get_vadcop_final_proof()
}
pub fn get_proof_u64(&self) -> Result<Vec<u64>> {
match &self.body {
ProofBody::Vadcop { proof, zisk_vk, kind, hash, publics_full } => {
if self.program_vk.vk.len() != PROGRAM_VK_LEN {
return Err(CommonError::InvalidProof(format!(
"Invalid program_vk length: expected {}, got {}",
PROGRAM_VK_LEN,
self.program_vk.vk.len()
)));
}
if zisk_vk.len() != PROGRAM_VK_LEN {
return Err(CommonError::InvalidProof(format!(
"Invalid zisk_vk length: expected {}, got {}",
PROGRAM_VK_LEN,
zisk_vk.len()
)));
}
let stark_publics = kind.stark_publics(publics_full);
let n_publics = stark_publics.len();
let tag = zisk_verifier::hash_tag(hash).ok_or_else(|| {
CommonError::InvalidProof(format!("unrecognized proof hash family {hash:?}"))
})?;
let mut words =
Vec::with_capacity(2 + n_publics + proof.len() + zisk_vk.len() + 1);
words.push(kind.is_minimal() as u64);
words.push(n_publics as u64);
words.extend_from_slice(&stark_publics);
words.extend_from_slice(proof);
words.extend_from_slice(zisk_vk);
words.push(tag);
Ok(words)
}
ProofBody::Plonk { .. } => Err(CommonError::InvalidProof(
"Proof not suitable for get_proof_u64. Only VadcopFinal and VadcopFinalMinimal proofs are supported.".to_string()
)),
}
}
pub fn get_proof_bytes(&self) -> Result<Vec<u8>> {
let words = self.get_proof_u64()?;
let mut bytes = Vec::with_capacity(words.len() * 8);
for w in &words {
bytes.extend_from_slice(&w.to_le_bytes());
}
Ok(bytes)
}
pub fn get_publics(&self) -> PublicValues {
self.publics()
}
pub fn get_program_vk(&self) -> ProgramVK {
ProgramVK {
vk: program_publics(self.committed_publics())
.iter()
.take(PROGRAM_VK_LEN)
.map(|&w| canonical(w))
.collect(),
hash_mode: self.program_vk.hash_mode,
}
}
pub fn new_from_vadcop_proof(
proof: &[u64],
minimal: bool,
zisk_vk: Vec<u64>,
hash: String,
) -> Result<Self> {
if zisk_vk.len() != PROGRAM_VK_LEN {
return Err(CommonError::InvalidProof(format!(
"Invalid zisk_vk length: expected {}, got {}",
PROGRAM_VK_LEN,
zisk_vk.len()
)));
}
let hash_mode = hash.parse::<HashMode>().map_err(|e| {
CommonError::Invalid(format!("unrecognized proof hash family {hash:?}: {e}"))
})?;
let hash = hash_mode.as_str().to_string();
let expected_n_publics = zisk_verifier::expected_n_publics(minimal);
match proof.first() {
Some(&n) if n == expected_n_publics as u64 => {}
Some(&n) => {
return Err(CommonError::InvalidProof(format!(
"proof declares {n} publics, expected {expected_n_publics} for this stage"
)))
}
None => {
return Err(CommonError::InvalidProof(
"Vadcop proof is empty, cannot read its public count".to_string(),
))
}
}
let vadcop_proof =
VadcopFinalProof::new_from_proof(proof, minimal, hash.clone()).map_err(|e| {
CommonError::InvalidProof(format!("Failed to parse Vadcop proof: {}", e))
})?;
ensure_canonical_publics(&vadcop_proof.public_values)?;
let program_vk =
ProgramVK::new_from_publics_with_mode(&vadcop_proof.public_values, hash_mode);
let kind = if minimal {
VadcopKind::Minimal
} else {
match vadcop_proof.public_values[0] {
0 => VadcopKind::Recurser,
IS_VADCOP_FINAL_PROOF => VadcopKind::Final,
other => {
return Err(CommonError::InvalidProof(format!(
"is_vadcop_final_proof must be 0 or {IS_VADCOP_FINAL_PROOF}, got {other}"
)))
}
}
};
let publics_full = program_publics(&vadcop_proof.public_values).to_vec();
Ok(Self {
body: ProofBody::Vadcop {
proof: vadcop_proof.proof,
zisk_vk,
kind,
hash,
publics_full,
},
program_vk,
})
}
pub fn verify(&self) -> Result<()> {
ZiskVerifyBuilder::new(self).verify()
}
pub fn verify_builder(&self) -> ZiskVerifyBuilder<'_> {
ZiskVerifyBuilder::new(self)
}
pub fn with_publics<'a>(&'a self, publics: &'a PublicValues) -> ZiskVerifyBuilder<'a> {
ZiskVerifyBuilder::new(self).with_publics(publics)
}
pub fn with_program_vk<'a>(&'a self, program_vk: &'a ProgramVK) -> ZiskVerifyBuilder<'a> {
ZiskVerifyBuilder::new(self).with_program_vk(program_vk)
}
pub fn with_plonk_vk<'a>(&'a self, plonk_vkey: &'a PlonkVkey) -> ZiskVerifyBuilder<'a> {
ZiskVerifyBuilder::new(self).with_plonk_vk(plonk_vkey)
}
pub fn with_setup_vk<'a>(&'a self, setup_vk: &'a [u64]) -> ZiskVerifyBuilder<'a> {
ZiskVerifyBuilder::new(self).with_setup_vk(setup_vk)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_returns_err_for_malformed_vadcop_final_minimal() {
let result = Proof::new(
ProofBody::Vadcop {
proof: vec![],
zisk_vk: vec![0u64; PROGRAM_VK_LEN],
kind: VadcopKind::Minimal,
hash: "Poseidon2".to_string(),
publics_full: vec![0u64; PROGRAM_VK_LEN + ZISK_PUBLICS],
},
ProgramVK::new_empty(),
)
.verify();
assert!(result.is_err(), "expected Err for malformed proof, got {:?}", result);
}
#[test]
fn verify_returns_err_for_malformed_vadcop_final() {
let result = Proof::new(
ProofBody::Vadcop {
proof: vec![],
zisk_vk: vec![0u64; PROGRAM_VK_LEN],
kind: VadcopKind::Final,
hash: "Poseidon2".to_string(),
publics_full: vec![0u64; PROGRAM_VK_LEN + ZISK_PUBLICS],
},
ProgramVK::new_empty(),
)
.verify();
assert!(result.is_err(), "expected Err for malformed proof, got {:?}", result);
}
fn serialized_vadcop(publics: &[u64]) -> Vec<u64> {
let mut v = vec![publics.len() as u64];
v.extend_from_slice(publics);
v.extend_from_slice(&[0u64; 8]);
v
}
#[test]
fn new_from_vadcop_proof_rejects_a_wrong_public_count() {
let err = Proof::new_from_vadcop_proof(
&serialized_vadcop(&[1, 2]),
false,
vec![1, 2, 3, 4],
"Poseidon2".to_string(),
)
.unwrap_err();
assert!(err.to_string().contains("declares 2 publics"), "got: {err}");
}
#[test]
fn new_from_vadcop_proof_rejects_an_out_of_range_flag() {
let mut publics = vec![0u64; VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN + ZISK_PUBLICS];
publics[0] = 2;
let err = Proof::new_from_vadcop_proof(
&serialized_vadcop(&publics),
false,
vec![1, 2, 3, 4],
"Poseidon2".to_string(),
)
.unwrap_err();
assert!(err.to_string().contains("is_vadcop_final_proof"), "got: {err}");
}
#[test]
fn new_from_vadcop_proof_rejects_a_non_canonical_public() {
let mut publics = vec![0u64; VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN + ZISK_PUBLICS];
publics[0] = IS_VADCOP_FINAL_PROOF;
publics[VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN] = GOLDILOCKS_ORDER;
let err = Proof::new_from_vadcop_proof(
&serialized_vadcop(&publics),
false,
vec![1, 2, 3, 4],
"Poseidon2".to_string(),
)
.unwrap_err();
assert!(err.to_string().contains("canonical"), "got: {err}");
}
#[test]
fn plonk_publics_come_from_the_committed_vector() {
let mut publics_full = vec![0u64; PROGRAM_VK_LEN + ZISK_PUBLICS];
publics_full[PROGRAM_VK_LEN] = 0xAABB;
let mut lying = PublicValues::new_empty();
lying.data[0..4].copy_from_slice(&0xDEADu32.to_le_bytes());
let proof = Proof::new(
ProofBody::Plonk {
proof_bytes: vec![],
plonk_vk: Box::new(PlonkVkBlob {
vadcop_vk: vec![0u64; PROGRAM_VK_LEN],
plonk_vkey: dummy_plonk_vkey(),
}),
publics: lying,
publics_full,
rootc: vec![0u64; PROGRAM_VK_LEN],
},
ProgramVK::new_empty(),
);
assert_eq!(&proof.publics().data[0..4], &0xAABBu32.to_le_bytes());
}
#[test]
fn recurser_domain_check_follows_the_program_vk_override() {
let proof = vadcop_proof(VadcopKind::Recurser, flag_free_publics([1, 2, 3, 4]));
let foreign = ProgramVK { vk: vec![9, 9, 9, 9], hash_mode: HashMode::Poseidon2 };
let err =
proof.with_program_vk(&foreign).with_setup_vk(&[1, 2, 3, 4]).verify().unwrap_err();
assert!(err.to_string().contains("recursion domain"), "got: {err}");
}
#[test]
fn a_compressed_proof_cannot_be_aggregated() {
let minimal = vadcop_proof(VadcopKind::Minimal, flag_free_publics([1, 2, 3, 4]));
let err = minimal.get_vadcop_final_proof_to_aggregate().unwrap_err();
assert!(err.to_string().contains("cannot be aggregated"), "got: {err}");
for kind in [VadcopKind::Final, VadcopKind::Recurser] {
let p = vadcop_proof(kind, flag_free_publics([1, 2, 3, 4]));
let vfp = p.get_vadcop_final_proof_to_aggregate().unwrap();
assert_eq!(
vfp.public_values.len(),
VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN + ZISK_PUBLICS
);
}
}
#[test]
fn try_publics_rejects_what_publics_can_only_guess() {
let misshapen = Proof::new(
ProofBody::Plonk {
proof_bytes: vec![],
plonk_vk: Box::new(PlonkVkBlob {
vadcop_vk: vec![0u64; PROGRAM_VK_LEN],
plonk_vkey: dummy_plonk_vkey(),
}),
publics: PublicValues::new_empty(),
publics_full: vec![1, 2],
rootc: vec![0u64; PROGRAM_VK_LEN],
},
ProgramVK::new_empty(),
);
assert!(misshapen.try_publics().is_err());
assert_eq!(misshapen.publics().public_u64().len(), ZISK_PUBLICS);
let ok = vadcop_proof(VadcopKind::Final, flag_free_publics([1, 2, 3, 4]));
assert_eq!(ok.try_publics().unwrap().data, ok.publics().data);
}
#[test]
fn verify_reports_a_nonexistent_stage_as_malformed() {
let mut proof = vadcop_proof(VadcopKind::Minimal, flag_free_publics([1, 2, 3, 4]));
if let ProofBody::Vadcop { hash, .. } = &mut proof.body {
*hash = "blake3".to_string();
}
proof.program_vk.hash_mode = HashMode::Blake3;
let err = proof.verify().unwrap_err();
assert!(matches!(err, CommonError::InvalidProof(_)), "got: {err:?}");
assert!(err.to_string().contains("no "), "got: {err}");
}
#[test]
fn get_vadcop_final_proof_rejects_an_already_flagged_body() {
let mut flagged = vec![0u64; VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN + ZISK_PUBLICS];
flagged[0] = IS_VADCOP_FINAL_PROOF;
let proof = vadcop_proof(VadcopKind::Final, flagged);
let err = proof.get_vadcop_final_proof().unwrap_err();
assert!(matches!(err, CommonError::InvalidProof(_)), "got: {err:?}");
let short = vadcop_proof(VadcopKind::Final, vec![0u64; 3]);
assert!(short.get_vadcop_final_proof().is_err());
let ok = vadcop_proof(VadcopKind::Final, flag_free_publics([1, 2, 3, 4]));
let vfp = ok.get_vadcop_final_proof().unwrap();
assert_eq!(vfp.public_values.len(), VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN + ZISK_PUBLICS);
assert_eq!(vfp.public_values[0], IS_VADCOP_FINAL_PROOF);
}
#[test]
fn a_flagged_plonk_body_is_not_shifted_by_a_word() {
let mut flagged = vec![0u64; VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN + ZISK_PUBLICS];
flagged[0] = IS_VADCOP_FINAL_PROOF;
flagged[VADCOP_FINAL_FLAG_LEN..VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN]
.copy_from_slice(&[11, 12, 13, 14]);
flagged[VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN] = 0xAABB;
let proof = Proof::new(
ProofBody::Plonk {
proof_bytes: vec![],
plonk_vk: Box::new(PlonkVkBlob {
vadcop_vk: vec![0u64; PROGRAM_VK_LEN],
plonk_vkey: dummy_plonk_vkey(),
}),
publics: PublicValues::new_empty(),
publics_full: flagged,
rootc: vec![0u64; PROGRAM_VK_LEN],
},
ProgramVK::new_empty(),
);
assert_eq!(&proof.publics().data[0..4], &0xAABBu32.to_le_bytes());
assert_eq!(&proof.try_publics().unwrap().data[0..4], &0xAABBu32.to_le_bytes());
assert_eq!(proof.get_program_vk().vk, vec![11, 12, 13, 14]);
}
fn dummy_plonk_vkey() -> PlonkVkey {
let g1 = || ["0".to_string(), "0".to_string(), "1".to_string()];
let g2 = || {
[
["0".to_string(), "0".to_string()],
["0".to_string(), "0".to_string()],
["1".to_string(), "0".to_string()],
]
};
PlonkVkey {
protocol: "plonk".to_string(),
curve: "bn128".to_string(),
n_public: 1,
power: 1,
k1: "2".to_string(),
k2: "3".to_string(),
qm: g1(),
ql: g1(),
qr: g1(),
qo: g1(),
qc: g1(),
s1: g1(),
s2: g1(),
s3: g1(),
x_2: g2(),
w: "1".to_string(),
}
}
fn vadcop_proof(kind: VadcopKind, publics_full: Vec<u64>) -> Proof {
Proof::new(
ProofBody::Vadcop {
proof: vec![0u64; 8],
zisk_vk: vec![1, 2, 3, 4],
kind,
hash: "Poseidon2".to_string(),
publics_full,
},
ProgramVK::new_from_publics_with_mode(&[1, 2, 3, 4], HashMode::Poseidon2),
)
}
fn flag_free_publics(vk: [u64; PROGRAM_VK_LEN]) -> Vec<u64> {
let mut p = vec![0u64; PROGRAM_VK_LEN + ZISK_PUBLICS];
p[..PROGRAM_VK_LEN].copy_from_slice(&vk);
p
}
#[test]
fn verify_rejects_a_plonk_key_pinned_on_a_vadcop_proof() {
let proof = vadcop_proof(VadcopKind::Final, flag_free_publics([1, 2, 3, 4]));
let vkey = dummy_plonk_vkey();
let err = proof.with_plonk_vk(&vkey).verify().unwrap_err();
assert!(err.to_string().contains("PLONK verification key was pinned"), "got: {err}");
}
#[test]
fn verify_rejects_non_canonical_program_vk_override() {
let proof = vadcop_proof(VadcopKind::Final, flag_free_publics([1, 2, 3, 4]));
let shifted =
ProgramVK { vk: vec![1 + GOLDILOCKS_ORDER, 2, 3, 4], hash_mode: HashMode::Poseidon2 };
let err = proof.with_program_vk(&shifted).verify().unwrap_err();
assert!(err.to_string().contains("non-canonical"), "got: {err}");
}
#[test]
fn verify_rejects_non_canonical_setup_vk_override() {
let proof = vadcop_proof(VadcopKind::Final, flag_free_publics([1, 2, 3, 4]));
let err = proof.with_setup_vk(&[1 + GOLDILOCKS_ORDER, 2, 3, 4]).verify().unwrap_err();
assert!(err.to_string().contains("non-canonical"), "got: {err}");
}
#[test]
fn verify_rejects_a_recurser_proof_verifying_outside_its_domain() {
let proof = vadcop_proof(VadcopKind::Recurser, flag_free_publics([9, 9, 9, 9]));
let err = proof.with_setup_vk(&[1, 2, 3, 4]).verify().unwrap_err();
assert!(err.to_string().contains("recursion domain"), "got: {err}");
}
#[test]
fn load_rejects_a_non_canonical_stored_public() {
let tmp = std::env::temp_dir().join(format!("proof_noncanon_{}.bin", std::process::id()));
let mut publics = flag_free_publics([1, 2, 3, 4]);
publics[PROGRAM_VK_LEN] = GOLDILOCKS_ORDER;
vadcop_proof(VadcopKind::Final, publics).save(&tmp).unwrap();
let err = Proof::load(&tmp).unwrap_err();
std::fs::remove_file(&tmp).ok();
assert!(err.to_string().contains("canonical"), "got: {err}");
}
#[test]
fn serialized_proof_carries_the_hash_tag_last() {
let proof = vadcop_proof(VadcopKind::Final, flag_free_publics([1, 2, 3, 4]));
let words = proof.get_proof_u64().unwrap();
assert_eq!(words.last().copied(), zisk_verifier::hash_tag("Poseidon2"));
}
#[test]
fn splice_program_vk_preserves_wide_publics() {
let mut publics = flag_free_publics([1, 2, 3, 4]);
publics[PROGRAM_VK_LEN] = 1 << 40;
let spliced = splice_program_vk(&publics, &[9, 9, 9, 9]).unwrap();
assert_eq!(&spliced[..PROGRAM_VK_LEN], &[9, 9, 9, 9]);
assert_eq!(spliced[PROGRAM_VK_LEN], 1 << 40, "wide public must not be truncated");
}
#[test]
fn splice_program_vk_normalizes_a_flagged_publics_vector() {
let mut flagged = vec![0u64; VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN + ZISK_PUBLICS];
flagged[0] = IS_VADCOP_FINAL_PROOF;
flagged[VADCOP_FINAL_FLAG_LEN..VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN]
.copy_from_slice(&[11, 12, 13, 14]);
flagged[VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN] = 77;
let spliced = splice_program_vk(&flagged, &[91, 92, 93, 94]).unwrap();
assert_eq!(spliced.len(), PROGRAM_VK_LEN + ZISK_PUBLICS, "flag must be stripped");
assert_eq!(&spliced[..PROGRAM_VK_LEN], &[91, 92, 93, 94], "vk must be replaced");
assert_eq!(spliced[PROGRAM_VK_LEN], 77, "inputs must not shift");
}
#[test]
fn plonk_verify_rejects_wrong_len_setup_vk() {
let vkey = dummy_plonk_vkey();
let proof = Proof::new(
ProofBody::Plonk {
proof_bytes: vec![],
plonk_vk: Box::new(PlonkVkBlob {
vadcop_vk: vec![0u64; PROGRAM_VK_LEN],
plonk_vkey: vkey,
}),
publics: PublicValues::new_empty(),
publics_full: vec![0u64; PROGRAM_VK_LEN + ZISK_PUBLICS],
rootc: vec![0u64; PROGRAM_VK_LEN],
},
ProgramVK::new_empty(),
);
let err = proof.with_setup_vk(&[1u64, 2, 3]).verify().unwrap_err();
assert!(
matches!(err, CommonError::InvalidProof(_)),
"expected InvalidProof for wrong-length setup vk, got {err:?}"
);
}
#[test]
fn proof_save_load_roundtrip_vadcop() {
let tmp = std::env::temp_dir().join(format!("proof_roundtrip_{}.bin", std::process::id()));
let original = Proof::new(
ProofBody::Vadcop {
proof: vec![1, 2, 3, 4],
zisk_vk: vec![10, 20, 30, 40],
kind: VadcopKind::Minimal,
hash: "Poseidon2".to_string(),
publics_full: vec![0u64; PROGRAM_VK_LEN + ZISK_PUBLICS],
},
ProgramVK::new_from_publics(&[7, 8, 9, 10]),
);
original.save(&tmp).unwrap();
let loaded = Proof::load(&tmp).unwrap();
std::fs::remove_file(&tmp).ok();
assert_eq!(loaded.kind(), ProofKind::VadcopFinalMinimal);
match loaded.body {
ProofBody::Vadcop { proof, zisk_vk, kind, hash, .. } => {
assert_eq!(proof, vec![1, 2, 3, 4]);
assert_eq!(zisk_vk, vec![10, 20, 30, 40]);
assert_eq!(kind, VadcopKind::Minimal);
assert_eq!(hash, "Poseidon2");
}
ProofBody::Plonk { .. } => panic!("expected Vadcop body after roundtrip"),
}
assert_eq!(loaded.program_vk.vk, vec![7, 8, 9, 10]);
}
#[test]
fn proof_kind_derivation() {
let vadcop = Proof::new(
ProofBody::Vadcop {
proof: vec![],
zisk_vk: vec![],
kind: VadcopKind::Final,
hash: "Poseidon2".to_string(),
publics_full: vec![0u64; PROGRAM_VK_LEN + ZISK_PUBLICS],
},
ProgramVK::new_empty(),
);
assert_eq!(vadcop.kind(), ProofKind::VadcopFinal);
assert!(vadcop.is_empty());
let minimal = Proof::new(
ProofBody::Vadcop {
proof: vec![1],
zisk_vk: vec![],
kind: VadcopKind::Minimal,
hash: "Poseidon2".to_string(),
publics_full: vec![0u64; PROGRAM_VK_LEN + ZISK_PUBLICS],
},
ProgramVK::new_empty(),
);
assert_eq!(minimal.kind(), ProofKind::VadcopFinalMinimal);
assert!(!minimal.is_empty());
}
}