use proofman_verifier::VadcopFinalProof;
pub const VADCOP_VK_LEN_WORDS: usize = 4;
pub const HASH_TAG_LEN_WORDS: usize = 1;
const HASH_TAGS: [&str; 3] = ["Poseidon1", "Poseidon2", "blake3"];
pub fn hash_tag(hash_id: &str) -> Option<u64> {
HASH_TAGS.iter().position(|&f| f == hash_id).map(|i| i as u64)
}
pub fn hash_id_from_tag(tag: u64) -> Option<&'static str> {
HASH_TAGS.get(tag as usize).copied()
}
pub const ZISK_PUBLICS: usize = 64;
pub const PROGRAM_VK_LEN: usize = 4;
pub const PROGRAM_N_PUBLICS: usize = PROGRAM_VK_LEN + ZISK_PUBLICS;
pub const VADCOP_FINAL_FLAG_LEN: usize = 1;
pub const IS_VADCOP_FINAL_PROOF: u64 = 1;
pub const GOLDILOCKS_ORDER: u64 = 0xFFFF_FFFF_0000_0001;
pub fn publics_are_canonical(publics: &[u64]) -> bool {
publics.iter().all(|&w| w < GOLDILOCKS_ORDER)
}
const EXPECTED_N_PUBLICS_FINAL: u64 = (VADCOP_FINAL_FLAG_LEN + PROGRAM_N_PUBLICS) as u64;
const EXPECTED_N_PUBLICS_COMPRESSED: u64 = PROGRAM_N_PUBLICS as u64;
pub const fn expected_n_publics(minimal: bool) -> usize {
if minimal {
EXPECTED_N_PUBLICS_COMPRESSED as usize
} else {
EXPECTED_N_PUBLICS_FINAL as usize
}
}
fn minimal_marker(zisk_proof: &[u64]) -> Option<bool> {
match *zisk_proof.first()? {
0 => Some(false),
1 => Some(true),
_ => None,
}
}
pub fn committed_is_aggregate(zisk_proof: &[u64]) -> Option<bool> {
if zisk_proof.len() < 2 || minimal_marker(zisk_proof)? {
return None;
}
if zisk_proof[1] != EXPECTED_N_PUBLICS_FINAL {
return None;
}
if zisk_proof.len() < 2 + EXPECTED_N_PUBLICS_FINAL as usize {
return None;
}
is_aggregate_flag(zisk_proof[2])
}
fn is_aggregate_flag(flag: u64) -> Option<bool> {
match flag {
IS_VADCOP_FINAL_PROOF => Some(false),
0 => Some(true),
_ => None,
}
}
pub fn committed_program_vk(zisk_proof: &[u64]) -> Option<&[u64]> {
if zisk_proof.len() < 2 {
return None;
}
let minimal = minimal_marker(zisk_proof)?;
let expected_n_publics =
if minimal { EXPECTED_N_PUBLICS_COMPRESSED } else { EXPECTED_N_PUBLICS_FINAL };
if zisk_proof[1] != expected_n_publics {
return None;
}
let n = expected_n_publics as usize;
if zisk_proof.len() < 2 + n {
return None;
}
Some(&program_publics(&zisk_proof[2..2 + n])[..PROGRAM_VK_LEN])
}
pub fn verify_vadcop_final_proof(zisk_proof: &[u64], vadcop_final_vk: &[u64], hash: &str) -> bool {
if zisk_proof.len() < 2 {
return false;
}
if vadcop_final_vk.len() != PROGRAM_VK_LEN {
return false;
}
let Some(minimal) = minimal_marker(zisk_proof) else {
return false;
};
let vadcop_proof = &zisk_proof[1..];
let expected_n_publics = expected_n_publics(minimal);
if zisk_proof.len() < 2 + expected_n_publics {
return false;
}
if vadcop_proof[0] != expected_n_publics as u64 {
return false;
}
if !publics_are_canonical(&vadcop_proof[1..1 + expected_n_publics]) {
return false;
}
if !minimal && is_aggregate_flag(vadcop_proof[1]).is_none() {
return false;
}
let Some(expected_bytes) = expected_proof_bytes(hash, minimal) else {
return false;
};
if vadcop_proof.len() != 1 + expected_n_publics + expected_bytes / 8 {
return false;
}
verify_by_family(hash, minimal, vadcop_proof, vadcop_final_vk)
}
struct StageVerifier {
verify_u64: fn(&[u64], &[u64]) -> bool,
verify: fn(&VadcopFinalProof, &[u64]) -> bool,
expected_proof_bytes: fn() -> usize,
}
fn stage_verifier(hash: &str, minimal: bool) -> Option<StageVerifier> {
macro_rules! stage {
($module:path) => {{
use $module as m;
StageVerifier {
verify_u64: m::verify_u64,
verify: m::verify,
expected_proof_bytes: m::expected_proof_bytes,
}
}};
}
Some(match (hash, minimal) {
("blake3", false) => stage!(crate::blake3::vadcop_final),
("Poseidon1", false) => stage!(crate::poseidon1::vadcop_final),
("Poseidon1", true) => stage!(crate::poseidon1::vadcop_final_compressed),
("Poseidon2", false) => stage!(crate::poseidon2::vadcop_final),
("Poseidon2", true) => stage!(crate::poseidon2::vadcop_final_compressed),
_ => return None,
})
}
fn verify_by_family(hash: &str, minimal: bool, vadcop_proof: &[u64], vk: &[u64]) -> bool {
stage_verifier(hash, minimal).is_some_and(|s| (s.verify_u64)(vadcop_proof, vk))
}
pub fn verify_vadcop_final(proof: &VadcopFinalProof, vk: &[u64]) -> bool {
if vk.len() != PROGRAM_VK_LEN {
return false;
}
if proof.public_values.len() != expected_n_publics(proof.compressed) {
return false;
}
if !publics_are_canonical(&proof.public_values) {
return false;
}
if !proof.compressed && is_aggregate_flag(proof.public_values[0]).is_none() {
return false;
}
stage_verifier(&proof.hash, proof.compressed).is_some_and(|s| {
proof.proof.len() * 8 == (s.expected_proof_bytes)() && (s.verify)(proof, vk)
})
}
pub fn expected_proof_bytes(hash: &str, minimal: bool) -> Option<usize> {
stage_verifier(hash, minimal).map(|s| (s.expected_proof_bytes)())
}
pub fn program_publics(publics_full: &[u64]) -> &[u64] {
if publics_full.len() == VADCOP_FINAL_FLAG_LEN + PROGRAM_N_PUBLICS {
&publics_full[VADCOP_FINAL_FLAG_LEN..]
} else {
publics_full
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
const HDR: usize = 2;
const LEAF_LEN: usize = HDR + EXPECTED_N_PUBLICS_FINAL as usize;
fn leaf(flag: u64, vk: [u64; PROGRAM_VK_LEN]) -> [u64; LEAF_LEN] {
let mut v = [7u64; LEAF_LEN];
v[0] = 0;
v[1] = EXPECTED_N_PUBLICS_FINAL;
v[2] = flag;
v[3..3 + PROGRAM_VK_LEN].copy_from_slice(&vk);
v
}
#[test]
fn a_leaf_is_not_an_aggregate() {
let p = leaf(IS_VADCOP_FINAL_PROOF, [1, 2, 3, 4]);
assert_eq!(committed_is_aggregate(&p), Some(false));
assert_eq!(committed_program_vk(&p), Some(&[1u64, 2, 3, 4][..]));
}
#[test]
fn a_flag_zero_proof_is_an_aggregate() {
let p = leaf(0, [9, 9, 9, 9]);
assert_eq!(committed_is_aggregate(&p), Some(true));
assert_eq!(committed_program_vk(&p), Some(&[9u64, 9, 9, 9][..]));
}
#[test]
fn a_minimal_proof_cannot_be_classified() {
let mut p = leaf(0, [1, 2, 3, 4]);
p[0] = 1;
p[1] = EXPECTED_N_PUBLICS_COMPRESSED;
assert_eq!(committed_is_aggregate(&p), None);
}
#[test]
fn a_truncated_proof_classifies_as_nothing() {
assert_eq!(committed_is_aggregate(&[]), None);
assert_eq!(committed_is_aggregate(&[0]), None);
assert_eq!(committed_is_aggregate(&[0, EXPECTED_N_PUBLICS_FINAL]), None);
assert_eq!(committed_is_aggregate(&[0, 12, 0, 1, 2, 3, 4]), None);
}
#[test]
fn every_tag_round_trips() {
for (i, f) in HASH_TAGS.iter().enumerate() {
assert_eq!(hash_tag(f), Some(i as u64));
assert_eq!(hash_id_from_tag(i as u64), Some(*f));
}
assert_eq!(hash_id_from_tag(HASH_TAGS.len() as u64), None);
assert_eq!(hash_tag("poseidon3"), None);
}
fn final_proof(publics: Vec<u64>, compressed: bool) -> VadcopFinalProof {
VadcopFinalProof::new(vec![0u64; 8], publics, compressed, "Poseidon2".to_string())
}
#[test]
fn a_short_publics_vector_is_refused_before_dispatch() {
let short = final_proof(vec![0; 10], false);
assert!(!verify_vadcop_final(&short, &[1, 2, 3, 4]));
let flagged = final_proof(vec![0; EXPECTED_N_PUBLICS_FINAL as usize], true);
assert!(!verify_vadcop_final(&flagged, &[1, 2, 3, 4]));
}
#[test]
fn a_wrong_length_setup_key_is_refused() {
let publics = vec![0; EXPECTED_N_PUBLICS_FINAL as usize];
assert!(!verify_vadcop_final(&final_proof(publics.clone(), false), &[1, 2, 3]));
assert!(!verify_vadcop_final(&final_proof(publics, false), &[1, 2, 3, 4, 5]));
}
#[test]
fn a_non_canonical_public_is_refused_on_the_host_path() {
let mut publics = vec![0u64; EXPECTED_N_PUBLICS_FINAL as usize];
publics[VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN] = GOLDILOCKS_ORDER;
assert!(!verify_vadcop_final(&final_proof(publics, false), &[1, 2, 3, 4]));
}
#[test]
fn a_non_boolean_minimal_marker_is_refused() {
let mut p = leaf(IS_VADCOP_FINAL_PROOF, [1, 2, 3, 4]).to_vec();
p[0] = 2;
assert!(!verify_vadcop_final_proof(&p, &[1, 2, 3, 4], "Poseidon2"));
assert_eq!(committed_program_vk(&p), None);
assert_eq!(committed_is_aggregate(&p), None);
}
#[test]
fn every_built_stage_has_a_whole_word_proof_size() {
for (hash, minimal) in [
("blake3", false),
("Poseidon1", false),
("Poseidon1", true),
("Poseidon2", false),
("Poseidon2", true),
] {
let bytes = expected_proof_bytes(hash, minimal)
.unwrap_or_else(|| panic!("{hash} minimal={minimal} should have a stage"));
assert_eq!(bytes % 8, 0, "{hash} minimal={minimal}");
assert!(bytes > 0, "{hash} minimal={minimal}");
}
assert_eq!(expected_proof_bytes("blake3", true), None);
assert_eq!(expected_proof_bytes("poseidon3", false), None);
}
#[test]
fn a_wrong_length_stark_payload_is_refused() {
let words = 1
+ EXPECTED_N_PUBLICS_FINAL as usize
+ expected_proof_bytes("Poseidon2", false).unwrap() / 8;
let mut exact = vec![0u64; 1 + words];
exact[0] = 0;
exact[1] = EXPECTED_N_PUBLICS_FINAL;
exact[2] = IS_VADCOP_FINAL_PROOF;
let mut short = exact.clone();
short.pop();
let mut long = exact.clone();
long.push(0);
assert!(!verify_vadcop_final_proof(&short, &[1, 2, 3, 4], "Poseidon2"));
assert!(!verify_vadcop_final_proof(&long, &[1, 2, 3, 4], "Poseidon2"));
assert!(!verify_vadcop_final_proof(&exact, &[1, 2, 3, 4], "Poseidon2"));
}
#[test]
fn an_out_of_range_leaf_flag_is_refused_everywhere() {
let mut p = leaf(2, [1, 2, 3, 4]).to_vec();
assert_eq!(committed_is_aggregate(&p), None, "flag 2 is not a fold");
assert!(!verify_vadcop_final_proof(&p, &[1, 2, 3, 4], "Poseidon2"));
let mut publics = vec![0u64; EXPECTED_N_PUBLICS_FINAL as usize];
publics[0] = 2;
assert!(!verify_vadcop_final(&final_proof(publics, false), &[1, 2, 3, 4]));
p[2] = IS_VADCOP_FINAL_PROOF;
assert_eq!(committed_is_aggregate(&p), Some(false));
p[2] = 0;
assert_eq!(committed_is_aggregate(&p), Some(true));
}
#[test]
fn a_non_canonical_public_is_refused() {
let mut p = leaf(IS_VADCOP_FINAL_PROOF, [1, 2, 3, 4]).to_vec();
p[2 + VADCOP_FINAL_FLAG_LEN + PROGRAM_VK_LEN] = GOLDILOCKS_ORDER;
assert!(!verify_vadcop_final_proof(&p, &[1, 2, 3, 4], "Poseidon2"));
}
}