use super::proof::{DirectError, validate};
use crate::types::{CCSInstance, CCSWitness};
use nebu::Goldilocks;
use nebu::field::P;
pub const FORMAT: &str = "zheng-nox-public-execution-v3";
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Certificate {
pub free: Vec<u64>,
}
fn pins(public: &[(usize, Goldilocks)]) -> Vec<(usize, Goldilocks)> {
core::iter::once((0, Goldilocks::ONE))
.chain(public.iter().copied())
.collect()
}
pub fn certify(
instance: &CCSInstance,
witness: &CCSWitness,
public: &[(usize, Goldilocks)],
) -> Result<Certificate, DirectError> {
validate(instance, public)?;
let pins = pins(public);
if witness.z.len() != instance.num_cols
|| pins.iter().any(|&(i, v)| witness.z[i] != v)
|| !instance.is_satisfied_by(witness)
{
return Err(DirectError::InvalidWitness);
}
let mut pinned = pins.iter().map(|&(i, _)| i).peekable();
let mut free = Vec::with_capacity(instance.num_cols);
for (i, value) in witness.z.iter().enumerate() {
if pinned.peek() == Some(&i) {
pinned.next();
continue;
}
free.push(value.as_u64());
}
while free.last() == Some(&0) {
free.pop();
}
Ok(Certificate { free })
}
fn assemble(
instance: &CCSInstance,
certificate: &Certificate,
public: &[(usize, Goldilocks)],
) -> Result<CCSWitness, DirectError> {
let pins = pins(public);
let referenced = instance
.matrices
.iter()
.flat_map(|m| m.entries.iter().flatten())
.filter(|&&(_, v)| v != Goldilocks::ZERO)
.map(|&(c, _)| c + 1)
.max()
.unwrap_or(0);
let pinned_below = pins.iter().filter(|&&(i, _)| i < referenced).count();
let free_positions = referenced.saturating_sub(pinned_below);
if certificate.free.len() > free_positions
|| certificate.free.last() == Some(&0)
|| certificate.free.iter().any(|&v| v >= P)
{
return Err(DirectError::InvalidProof);
}
let mut z = vec![Goldilocks::ZERO; instance.num_cols];
let mut pinned = pins.iter().peekable();
let mut values = certificate.free.iter();
for (i, slot) in z.iter_mut().enumerate() {
if let Some(&&(index, value)) = pinned.peek()
&& index == i
{
*slot = value;
pinned.next();
continue;
}
match values.next() {
Some(&v) => *slot = Goldilocks::new(v),
None => break,
}
}
for &(index, value) in pinned {
z[index] = value;
}
Ok(CCSWitness { z })
}
pub fn verify(
instance: &CCSInstance,
certificate: &Certificate,
public: &[(usize, Goldilocks)],
) -> Result<(), DirectError> {
validate(instance, public)?;
let witness = assemble(instance, certificate, public)?;
if !instance.is_satisfied_by(&witness) {
return Err(DirectError::InvalidProof);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{CCSInstance, CCSWitness, SparseMatrix};
fn product() -> (CCSInstance, Vec<(usize, Goldilocks)>, CCSWitness) {
let n = 64;
let m = 1;
let row = |col: usize| SparseMatrix {
rows: m,
cols: n,
entries: vec![vec![(col, Goldilocks::ONE)]],
};
let instance = CCSInstance {
num_rows: m,
num_cols: n,
matrices: vec![row(2), row(3), row(1)],
multisets: vec![vec![0, 1], vec![2]],
coeffs: vec![Goldilocks::ONE, -Goldilocks::ONE],
};
let mut z = vec![Goldilocks::ZERO; n];
z[0] = Goldilocks::ONE;
z[1] = Goldilocks::new(42);
z[2] = Goldilocks::new(6);
z[3] = Goldilocks::new(7);
(instance, vec![(1, Goldilocks::new(42))], CCSWitness { z })
}
#[test]
fn honest_certificate_round_trips_and_trims_padding() {
let (instance, public, witness) = product();
let cert = certify(&instance, &witness, &public).unwrap();
assert_eq!(cert.free, vec![6, 7]);
verify(&instance, &cert, &public).unwrap();
}
#[test]
fn wrong_public_output_is_rejected() {
let (instance, _, witness) = product();
let cert = certify(&instance, &witness, &[(1, Goldilocks::new(42))]).unwrap();
assert!(verify(&instance, &cert, &[(1, Goldilocks::new(43))]).is_err());
}
#[test]
fn tampered_free_value_is_rejected() {
let (instance, public, witness) = product();
let mut cert = certify(&instance, &witness, &public).unwrap();
cert.free[1] = 8;
assert!(verify(&instance, &cert, &public).is_err());
}
#[test]
fn every_single_value_flip_is_rejected() {
let (instance, public, witness) = product();
let cert = certify(&instance, &witness, &public).unwrap();
for i in 0..cert.free.len() {
for delta in [1u64, 2, P - 1] {
let mut bad = cert.clone();
bad.free[i] = (bad.free[i] + delta) % P;
assert!(
verify(&instance, &bad, &public).is_err(),
"flip {i}+{delta}"
);
}
}
}
#[test]
fn noncanonical_and_overlong_certificates_are_rejected() {
let (instance, public, witness) = product();
let cert = certify(&instance, &witness, &public).unwrap();
let mut trailing_zero = cert.clone();
trailing_zero.free.push(0);
assert!(verify(&instance, &trailing_zero, &public).is_err());
let mut padding = cert.clone();
padding.free.push(5);
assert!(verify(&instance, &padding, &public).is_err());
let mut noncanonical = cert.clone();
noncanonical.free[0] += P;
assert!(verify(&instance, &noncanonical, &public).is_err());
let overlong = Certificate {
free: vec![0; instance.num_cols],
};
assert!(verify(&instance, &overlong, &public).is_err());
}
#[test]
fn zeroed_constant_cannot_be_expressed() {
let (instance, public, _) = product();
let empty = Certificate { free: vec![] };
assert!(verify(&instance, &empty, &public).is_err());
}
#[test]
fn unsatisfying_witness_cannot_be_certified() {
let (instance, public, mut witness) = product();
witness.z[3] = Goldilocks::new(8);
assert!(certify(&instance, &witness, &public).is_err());
}
}
#[cfg(test)]
pub(crate) mod malleability {
use super::super::{ExecutionNoun, certify_execution};
use nebu::Goldilocks;
pub(crate) fn parse(text: &str) -> ExecutionNoun {
fn go(b: &[u8], p: &mut usize) -> ExecutionNoun {
while b[*p].is_ascii_whitespace() {
*p += 1;
}
if b[*p] == b'[' {
*p += 1;
let a = go(b, p);
let c = go(b, p);
while b[*p].is_ascii_whitespace() {
*p += 1;
}
*p += 1;
ExecutionNoun::Pair(Box::new(a), Box::new(c))
} else {
let s = *p;
while b[*p].is_ascii_digit() {
*p += 1;
}
ExecutionNoun::Atom(std::str::from_utf8(&b[s..*p]).unwrap().parse().unwrap())
}
}
go(text.as_bytes(), &mut 0)
}
const HASH: &str = "[2 [[3 [[4 [[9 [[0 3] [1 0]]] [[1 0] [8 [1 0]]]]] [0 1]]] [1 [2 [[0 3] [1 [2 [[3 [[5 [[0 2] [1 0]]] [1 0]]] [1 [15 [3 [[0 2] [3 [[1 0] [3 [[1 0] [3 [[1 0] [3 [[1 0] [3 [[1 0] [3 [[1 0] [3 [[1 0] [1 0]]]]]]]]]]]]]]]]]]]]]]]]]]]";
const ADD: &str = "[2 [[3 [[4 [[9 [[0 7] [1 0]]] [[1 0] [8 [1 0]]]]] [0 1]]] [1 [2 [[0 3] [1 [2 [[3 [[5 [[0 2] [1 0]]] [3 [[5 [[0 6] [1 0]]] [1 0]]]]] [1 [2 [[3 [[5 [[0 6] [0 2]]] [0 1]]] [1 [7 [[0 2] [0 14]]]]]]]]]]]]]]]";
const LOOP: &str = "[2 [[3 [[0 2] [0 1]]] [1 [2 [[2 [[3 [[1 0] [0 1]]] [1 [2 [[3 [[0 2] [3 [[2 [[3 [[0 6] [1 0]]] [1 [5 [[0 2] [1 3]]]]]] [0 7]]]]] [1 [3 [[0 6] [3 [[0 14] [0 15]]]]]]]]]]] [1 [2 [[2 [[3 [[1 1] [0 1]]] [1 [2 [[3 [[0 2] [3 [[2 [[3 [[0 6] [1 0]]] [1 [5 [[0 2] [1 3]]]]]] [0 7]]]]] [1 [3 [[0 6] [3 [[0 14] [0 15]]]]]]]]]]] [1 [2 [[2 [[3 [[1 2] [0 1]]] [1 [2 [[3 [[0 2] [3 [[2 [[3 [[0 6] [1 0]]] [1 [5 [[0 2] [1 3]]]]]] [0 7]]]]] [1 [3 [[0 6] [3 [[0 14] [0 15]]]]]]]]]]] [1 [2 [[4 [[9 [[0 2] [1 13]]] [[2 [[3 [[5 [[0 2] [1 100]]] [0 3]]] [1 [0 1]]]] [2 [[3 [[7 [[0 2] [1 2]]] [0 3]]] [1 [0 1]]]]]]] [1 [0 2]]]]]]]]]]]]]]]]";
fn row_value(m: &crate::types::SparseMatrix, r: usize, z: &[Goldilocks]) -> Goldilocks {
m.entries[r]
.iter()
.fold(Goldilocks::ZERO, |acc, &(c, v)| acc + v * z[c])
}
pub(crate) fn movable_wires_only_multiply_zero(
name: &str,
instance: &crate::types::CCSInstance,
cert: &super::Certificate,
public: &[(usize, Goldilocks)],
) -> usize {
let z = super::assemble(instance, cert, public).unwrap().z;
let pins: Vec<usize> = core::iter::once(0)
.chain(public.iter().map(|p| p.0))
.collect();
let free_index: Vec<usize> = (0..instance.num_cols)
.filter(|i| !pins.contains(i))
.collect();
let mut movable = 0;
for (k, &idx) in free_index.iter().enumerate().take(cert.free.len()) {
let mut bad = cert.clone();
bad.free[k] = (bad.free[k] + 1) % nebu::field::P;
if super::verify(instance, &bad, public).is_err() {
continue;
}
movable += 1;
for r in 0..instance.num_rows {
for set in &instance.multisets {
let reads = set.iter().any(|&mi| {
instance.matrices[mi].entries[r]
.iter()
.any(|&(c, v)| c == idx && v != Goldilocks::ZERO)
});
if !reads {
continue;
}
let zero_partner = set.iter().any(|&mi| {
!instance.matrices[mi].entries[r]
.iter()
.any(|&(c, _)| c == idx)
&& row_value(&instance.matrices[mi], r, &z) == Goldilocks::ZERO
});
assert!(
zero_partner,
"{name}: z[{idx}] is movable and reaches row {r} through a nonzero product"
);
}
}
}
movable
}
#[test]
fn every_movable_wire_only_multiplies_zero() {
for (name, program, inputs) in [
(
"hash",
HASH,
vec![vec![7u64], vec![0], vec![nebu::field::P - 1]],
),
(
"add",
ADD,
vec![vec![7, 5], vec![0, 0], vec![1, nebu::field::P - 1]],
),
("loop", LOOP, vec![vec![4], vec![0], vec![10]]),
] {
let program = parse(program);
for input in inputs {
let (statement, cert) = certify_execution(&program, &input, 1_000_000).unwrap();
let relation = statement.relation().unwrap();
let public = statement.bindings(&relation).unwrap();
let movable =
movable_wires_only_multiply_zero(name, &relation.instance, &cert, &public);
println!(
"{name} {input:?}: free {} movable {movable}",
cert.free.len()
);
}
}
}
}