use cyber_hemera::hash as hemera_hash;
use lens::brakedown::Brakedown;
use nebu::Goldilocks;
use zheng::ccs::{CONST_IDX, Z_LEN, build_step_ccs, reg_t, reg_t1};
use zheng::spartan::SpartanVerifier;
use zheng::types::{Accumulator, CCSWitness, Proof, ProofParams, Statement};
use zheng::{Transcript, decide, fold};
use bbg::Particle;
const PROGRAM: [u8; 32] = *b"foculus-pay-proof-v0...........\0";
#[derive(Clone, Debug)]
pub struct PayStatement {
pub content_id: Particle,
pub total_out: u64,
pub leg_count: u32,
}
impl PayStatement {
pub fn to_zheng(&self) -> Statement {
let mut input = [0u8; 32];
let mut buf = [0u8; 32 + 8 + 4];
buf[..32].copy_from_slice(&self.content_id);
buf[32..40].copy_from_slice(&self.total_out.to_le_bytes());
buf[40..44].copy_from_slice(&self.leg_count.to_le_bytes());
let h = hemera_hash(&buf);
let b = h.as_bytes();
input[..b.len().min(32)].copy_from_slice(&b[..b.len().min(32)]);
Statement {
program_hash: PROGRAM,
input_hash: input,
output_hash: self.content_id,
focus_bound: self.total_out,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum PayProofError {
FoldFailed,
DecideFailed,
VerifyFailed,
Empty,
}
pub fn prove_pay(stmt: &PayStatement) -> Result<Proof, PayProofError> {
if stmt.leg_count == 0 {
return Err(PayProofError::Empty);
}
let instance = build_step_ccs(5); let witness = pay_witness(stmt);
let zero = vec![Goldilocks::ZERO; Z_LEN];
let mut acc = Accumulator {
committed_instance: instance.clone(),
folded_witness: CCSWitness { z: zero.clone() },
witness_commitment: Brakedown::commit_raw(&zero),
error_evals: vec![Goldilocks::ZERO; instance.num_rows],
step_count: 0,
};
let mut t = Transcript::new();
fold(&mut acc, &instance, &witness, &mut t).map_err(|_| PayProofError::FoldFailed)?;
let zheng_stmt = stmt.to_zheng();
decide(&acc, &zheng_stmt, &ProofParams::default()).map_err(|_| PayProofError::DecideFailed)
}
pub fn verify_pay(proof: &Proof, stmt: &PayStatement) -> bool {
let Ok(expected) = prove_pay(stmt) else {
return false;
};
proof.commitment.as_bytes() == expected.commitment.as_bytes()
&& proof.eval_value == expected.eval_value
&& verify_decide_structure(proof, stmt)
}
fn verify_decide_structure(proof: &Proof, stmt: &PayStatement) -> bool {
let instance = build_step_ccs(5);
let witness = pay_witness(stmt);
let zero = vec![Goldilocks::ZERO; Z_LEN];
let mut acc = Accumulator {
committed_instance: instance.clone(),
folded_witness: CCSWitness { z: zero.clone() },
witness_commitment: Brakedown::commit_raw(&zero),
error_evals: vec![Goldilocks::ZERO; instance.num_rows],
step_count: 0,
};
let mut t = Transcript::new();
if fold(&mut acc, &instance, &witness, &mut t).is_err() {
return false;
}
let zheng_stmt = stmt.to_zheng();
let mut vt = Transcript::new_recursive();
vt.absorb_statement(&zheng_stmt);
vt.absorb(acc.witness_commitment.as_bytes());
for &e in &acc.error_evals {
vt.absorb(&e.as_u64().to_le_bytes());
}
vt.absorb(&acc.step_count.to_le_bytes());
SpartanVerifier::verify(&acc.committed_instance, proof, &acc.error_evals, &mut vt).is_ok()
}
fn pay_witness(stmt: &PayStatement) -> CCSWitness {
let mut buf = [0u8; 44];
buf[..32].copy_from_slice(&stmt.content_id);
buf[32..40].copy_from_slice(&stmt.total_out.to_le_bytes());
buf[40..44].copy_from_slice(&stmt.leg_count.to_le_bytes());
let h = hemera_hash(&buf);
let b = h.as_bytes();
let a = u64::from_le_bytes(b[0..8].try_into().unwrap()) % 1_000_000 + 1;
let b_ = u64::from_le_bytes(b[8..16].try_into().unwrap()) % 1_000_000 + 1;
let c = a + b_;
let mut z = vec![Goldilocks::ZERO; Z_LEN];
z[CONST_IDX] = Goldilocks::ONE;
z[reg_t(3)] = Goldilocks::new(a);
z[reg_t(4)] = Goldilocks::new(b_);
z[reg_t1(5)] = Goldilocks::new(c);
CCSWitness { z }
}
pub fn nullifier_set_hash(nullifiers: &[Particle]) -> Particle {
let mut buf = Vec::with_capacity(nullifiers.len() * 32 + 8);
buf.extend_from_slice(&(nullifiers.len() as u64).to_le_bytes());
for n in nullifiers {
buf.extend_from_slice(n);
}
let h = hemera_hash(&buf);
*h.as_bytes().first_chunk::<32>().unwrap_or(&[0u8; 32])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prove_and_verify_pay() {
let stmt = PayStatement {
content_id: [3u8; 32],
total_out: 150,
leg_count: 2,
};
let proof = prove_pay(&stmt).unwrap();
assert!(verify_pay(&proof, &stmt));
}
#[test]
fn verify_rejects_wrong_content() {
let stmt = PayStatement {
content_id: [3u8; 32],
total_out: 150,
leg_count: 2,
};
let proof = prove_pay(&stmt).unwrap();
let bad = PayStatement {
content_id: [4u8; 32],
total_out: 150,
leg_count: 2,
};
assert!(!verify_pay(&proof, &bad));
}
#[test]
fn empty_pay_errors() {
let stmt = PayStatement {
content_id: [1u8; 32],
total_out: 0,
leg_count: 0,
};
assert_eq!(prove_pay(&stmt).unwrap_err(), PayProofError::Empty);
}
}