pub mod ccs;
pub mod folding;
pub mod multilinear;
pub mod phi;
pub mod spartan;
pub mod sumcheck;
pub mod transcript;
pub mod types;
pub use crate::ccs::{
AxisOpening, HashAux, LookOpening, RootLeaves, build_axis_transcript_steps,
build_look_transcript_steps, look_openings_from_provider, root_from_leaves, standalone_root,
};
pub use phi::{
PhiError, PhiProof, PhiStatement, SparseGraph, SpmvError, SpmvProof, SpmvStatement,
TriKernelParams, prove_phi_star, prove_spmv, spmv_native, verify_phi_star, verify_spmv,
};
pub use transcript::Transcript;
pub use types::{
Accumulator, CCSInstance, CCSWitness, CommitError, DecideError, FoldError, LensBackend,
OpenError, Proof, ProofParams, SecurityLevel, SparseMatrix, Statement, SumcheckPoly,
TraceProof, VerifyError,
};
use nebu::Goldilocks;
use nox::VecTrace;
use lens::brakedown::Brakedown;
use lens::{Commitment, Lens, MultilinearPoly, Opening};
use crate::ccs::{
build_axis_steps_from_trace, build_ccs_from_trace, build_hash_binding_steps_from_trace,
build_hash_steps_from_trace, build_look_steps_from_trace,
};
use crate::folding::{decide as run_decide, fold_step};
use crate::spartan::verifier::SpartanVerifier;
fn hash_row(row: &nox::TraceRow) -> [u8; 32] {
let mut bytes = Vec::with_capacity(128);
for &v in row.r().iter() {
bytes.extend_from_slice(&v.to_le_bytes());
}
*hemera::hash(&bytes).as_bytes()
}
pub(crate) fn linkage_digest(commitments: &[&Commitment]) -> [u8; 32] {
let mut bytes = Vec::with_capacity(8 + commitments.len() * 32);
bytes.extend_from_slice(&(commitments.len() as u64).to_le_bytes());
for c in commitments {
bytes.extend_from_slice(c.as_bytes());
}
*hemera::hash(&bytes).as_bytes()
}
fn blank_acc(instance: &CCSInstance) -> Accumulator {
let init_z = vec![Goldilocks::ZERO; 64];
Accumulator {
committed_instance: instance.clone(),
folded_witness: CCSWitness { z: init_z.clone() },
witness_commitment: Brakedown::commit_raw(&init_z),
error_evals: vec![Goldilocks::ZERO; instance.num_rows],
step_count: 0,
}
}
pub fn commit(
trace: &VecTrace,
hash_aux: &[HashAux],
axis_openings: &[AxisOpening],
look_openings: &[LookOpening],
statement: &Statement,
params: &ProofParams,
) -> Result<TraceProof, CommitError> {
if statement.focus_bound > 0 && trace.0.len() as u64 > statement.focus_bound {
return Err(CommitError::FocusExhausted);
}
if statement.input_hash != [0u8; 32]
&& let Some(first) = trace.0.first()
&& hash_row(first) != statement.input_hash
{
return Err(CommitError::StatementMismatch);
}
if statement.output_hash != [0u8; 32]
&& let Some(last) = trace.0.last()
&& hash_row(last) != statement.output_hash
{
return Err(CommitError::StatementMismatch);
}
let main_steps = build_ccs_from_trace(&trace.0);
let hash_steps = build_hash_steps_from_trace(&trace.0, hash_aux)?;
let hash_binding = build_hash_binding_steps_from_trace(&trace.0, hash_aux)?;
let axis_steps = build_axis_steps_from_trace(&trace.0, axis_openings)?;
let axis_transcript = build_axis_transcript_steps(&trace.0, axis_openings)?;
let look_steps = build_look_steps_from_trace(&trace.0, look_openings)?;
let look_transcript = build_look_transcript_steps(&trace.0, look_openings)?;
let all_steps: Vec<(CCSInstance, CCSWitness)> = main_steps
.into_iter()
.chain(hash_steps)
.chain(hash_binding)
.chain(axis_steps)
.chain(axis_transcript)
.chain(look_steps)
.chain(look_transcript)
.collect();
if all_steps.is_empty() {
return Err(CommitError::TraceOverflow);
}
let mut folded: Vec<Accumulator> = Vec::new();
let mut cur_instance: Option<CCSInstance> = None;
let mut cur_acc: Option<Accumulator> = None;
let mut cur_transcript = Transcript::new();
for (instance, witness) in &all_steps {
let same = cur_instance.as_ref() == Some(instance);
if !same {
if let Some(acc) = cur_acc.take() {
folded.push(acc);
}
cur_instance = Some(instance.clone());
cur_acc = Some(blank_acc(instance));
cur_transcript = Transcript::new();
}
fold_step(
cur_acc.as_mut().unwrap(),
instance,
witness,
&mut cur_transcript,
)
.map_err(|_| CommitError::TraceOverflow)?;
}
if let Some(acc) = cur_acc.take() {
folded.push(acc);
}
let commitments: Vec<&Commitment> = folded.iter().map(|a| &a.witness_commitment).collect();
let linkage = linkage_digest(&commitments);
let mut groups: Vec<(Proof, Accumulator)> = Vec::with_capacity(folded.len());
for acc in folded {
let proof =
run_decide(&acc, statement, &linkage, params).map_err(CommitError::DecideFailed)?;
groups.push((proof, acc));
}
Ok(TraceProof { groups })
}
pub fn open(
poly: &[Goldilocks],
point: &[Goldilocks],
_params: &ProofParams,
) -> Result<(Commitment, Opening), OpenError> {
let num_vars = point.len();
if num_vars == 0 {
return Err(OpenError::InvalidPoint);
}
let target_len = 1usize << num_vars;
if poly.len() > target_len {
return Err(OpenError::InvalidPoint);
}
let mut padded = poly.to_vec();
while padded.len() < target_len {
padded.push(Goldilocks::ZERO);
}
let mp = MultilinearPoly::new(padded);
let commitment = Brakedown::commit(&mp);
let mut lt = lens::Transcript::new(b"zheng-open");
let opening = Brakedown::open(&mp, point, &mut lt);
Ok((commitment, opening))
}
pub fn verify_eval(
commitment: &Commitment,
point: &[Goldilocks],
value: Goldilocks,
opening: &Opening,
_params: &ProofParams,
) -> Result<(), OpenError> {
let mut lt = lens::Transcript::new(b"zheng-open");
if Brakedown::verify(commitment, point, value, opening, &mut lt) {
Ok(())
} else {
Err(OpenError::LensFailed)
}
}
pub fn verify(
proof: &TraceProof,
statement: &Statement,
_params: &ProofParams,
) -> Result<(), VerifyError> {
let commitments: Vec<&Commitment> = proof
.groups
.iter()
.map(|(_, acc)| &acc.witness_commitment)
.collect();
let linkage = linkage_digest(&commitments);
for (group_proof, acc) in &proof.groups {
let linear = acc
.committed_instance
.multisets
.iter()
.all(|multiset| multiset.len() <= 1);
if linear && acc.error_evals.iter().any(|&e| e != Goldilocks::ZERO) {
return Err(VerifyError::LinearErrorNonzero);
}
let mut transcript = Transcript::new_recursive();
transcript.absorb_statement(statement);
transcript.absorb_linkage(&linkage);
transcript.absorb(acc.witness_commitment.as_bytes());
for &e in &acc.error_evals {
transcript.absorb(&e.as_u64().to_le_bytes());
}
transcript.absorb(&acc.step_count.to_le_bytes());
SpartanVerifier::verify(
&acc.committed_instance,
group_proof,
&acc.error_evals,
&mut transcript,
)?;
}
Ok(())
}
pub fn fold(
acc: &mut Accumulator,
instance: &CCSInstance,
witness: &CCSWitness,
transcript: &mut Transcript,
) -> Result<(), FoldError> {
fold_step(acc, instance, witness, transcript)
}
pub fn decide(
acc: &Accumulator,
statement: &Statement,
params: &ProofParams,
) -> Result<Proof, DecideError> {
let linkage = linkage_digest(&[&acc.witness_commitment]);
run_decide(acc, statement, &linkage, params)
}
#[cfg(test)]
mod tests {
use super::*;
use lens::brakedown::Brakedown;
use lens::{Lens, MultilinearPoly, Transcript as LensTranscript};
use nox::{NullCalls, Reduction, VecTrace};
fn malformed_trace() -> VecTrace {
let mut order = Reduction::<1024>::new();
let obj = order.atom(Goldilocks::new(0)).unwrap();
let tag_255 = order.atom(Goldilocks::new(255)).unwrap();
let body = order.atom(Goldilocks::new(0)).unwrap();
let formula = order.pair(tag_255, body).unwrap();
let mut trace = VecTrace::default();
nox::reduce(&mut order, obj, formula, 10, &NullCalls, &mut trace);
nox::reduce(&mut order, obj, formula, 10, &NullCalls, &mut trace);
trace
}
fn zero_statement() -> Statement {
Statement {
program_hash: [0u8; 32],
input_hash: [0u8; 32],
output_hash: [0u8; 32],
focus_bound: 0,
bbg_root: [0u8; 32],
}
}
#[test]
fn commit_verify_roundtrip() {
let trace = malformed_trace();
let stmt = zero_statement();
let params = ProofParams::default();
let trace_proof = commit(&trace, &[], &[], &[], &stmt, ¶ms).unwrap();
assert!(verify(&trace_proof, &stmt, ¶ms).is_ok());
}
fn make_z_33(vals: &[(usize, u64)]) -> CCSWitness {
use crate::ccs::{CONST_IDX, Z_LEN};
let mut z = vec![Goldilocks::ZERO; Z_LEN];
z[CONST_IDX] = Goldilocks::ONE;
for &(idx, v) in vals {
z[idx] = Goldilocks::new(v);
}
CCSWitness { z }
}
#[test]
fn fold_add_multi_step_commit_verify() {
use crate::ccs::patterns::build_step_ccs;
use crate::ccs::{reg_t, reg_t1};
use crate::folding::fold::fold_step;
let instance = build_step_ccs(5); let witnesses = [
make_z_33(&[(reg_t(3), 3), (reg_t(4), 4), (reg_t1(5), 7)]),
make_z_33(&[(reg_t(3), 10), (reg_t(4), 20), (reg_t1(5), 30)]),
make_z_33(&[(reg_t(3), 1), (reg_t(4), 1), (reg_t1(5), 2)]),
];
for w in &witnesses {
assert!(instance.is_satisfied_by(w));
}
let mut acc = blank_acc(&instance);
let mut transcript = Transcript::new();
for w in &witnesses {
fold_step(&mut acc, &instance, w, &mut transcript).unwrap();
}
assert_eq!(acc.step_count, 3);
assert!(acc.error_evals.iter().all(|&e| e == Goldilocks::ZERO));
let stmt = zero_statement();
let proof = decide(&acc, &stmt, &ProofParams::default()).unwrap();
let trace_proof = TraceProof {
groups: vec![(proof, acc)],
};
assert!(verify(&trace_proof, &stmt, &ProofParams::default()).is_ok());
}
#[test]
fn fold_mul_multi_step_commit_verify() {
use crate::ccs::patterns::build_step_ccs;
use crate::ccs::{reg_t, reg_t1};
use crate::folding::fold::fold_step;
let instance = build_step_ccs(7); let witnesses = [
make_z_33(&[(reg_t(3), 6), (reg_t(4), 7), (reg_t1(5), 42)]),
make_z_33(&[(reg_t(3), 2), (reg_t(4), 5), (reg_t1(5), 10)]),
make_z_33(&[(reg_t(3), 3), (reg_t(4), 3), (reg_t1(5), 9)]),
];
for w in &witnesses {
assert!(instance.is_satisfied_by(w));
}
let mut acc = blank_acc(&instance);
let mut transcript = Transcript::new();
for w in &witnesses {
fold_step(&mut acc, &instance, w, &mut transcript).unwrap();
}
assert_eq!(acc.step_count, 3);
let stmt = zero_statement();
let proof = decide(&acc, &stmt, &ProofParams::default()).unwrap();
let trace_proof = TraceProof {
groups: vec![(proof, acc)],
};
assert!(verify(&trace_proof, &stmt, &ProofParams::default()).is_ok());
}
fn make_poly(values: &[u64]) -> Vec<Goldilocks> {
values.iter().map(|&v| Goldilocks::new(v)).collect()
}
#[test]
fn open_verify_eval_roundtrip_small() {
let poly = make_poly(&[3, 7, 11, 19]);
let point = vec![Goldilocks::new(2), Goldilocks::new(5)];
let params = ProofParams::default();
let (commitment, opening) = open(&poly, &point, ¶ms).unwrap();
let mp = MultilinearPoly::new(poly.clone());
let expected = mp.evaluate(&point);
verify_eval(&commitment, &point, expected, &opening, ¶ms).unwrap();
}
#[test]
fn open_verify_eval_roundtrip_six_vars() {
let poly: Vec<Goldilocks> = (0u64..64).map(Goldilocks::new).collect();
let point: Vec<Goldilocks> = (1u64..=6).map(Goldilocks::new).collect();
let params = ProofParams::default();
let (commitment, opening) = open(&poly, &point, ¶ms).unwrap();
let mp = MultilinearPoly::new(poly);
let expected = mp.evaluate(&point);
verify_eval(&commitment, &point, expected, &opening, ¶ms).unwrap();
}
#[test]
fn open_pads_short_poly_to_point_size() {
let poly = make_poly(&[5, 13]);
let point = vec![Goldilocks::ZERO, Goldilocks::ZERO, Goldilocks::ZERO];
let params = ProofParams::default();
let (commitment, opening) = open(&poly, &point, ¶ms).unwrap();
let expected = Goldilocks::new(5);
verify_eval(&commitment, &point, expected, &opening, ¶ms).unwrap();
}
#[test]
fn open_larger_than_witness_size() {
let poly: Vec<Goldilocks> = (0u64..256).map(|v| Goldilocks::new(v * 3 + 1)).collect();
let point: Vec<Goldilocks> = (0u64..8).map(|v| Goldilocks::new(v + 2)).collect();
let params = ProofParams::default();
let (commitment, opening) = open(&poly, &point, ¶ms).unwrap();
let mp = MultilinearPoly::new(poly);
let expected = mp.evaluate(&point);
verify_eval(&commitment, &point, expected, &opening, ¶ms).unwrap();
}
#[test]
fn verify_eval_wrong_value_rejected() {
let poly = make_poly(&[1, 2, 3, 4]);
let point = vec![Goldilocks::ZERO, Goldilocks::ZERO];
let params = ProofParams::default();
let (commitment, opening) = open(&poly, &point, ¶ms).unwrap();
let wrong = Goldilocks::new(999);
assert!(verify_eval(&commitment, &point, wrong, &opening, ¶ms).is_err());
}
#[test]
fn open_zero_vars_rejected() {
let poly = make_poly(&[42]);
let params = ProofParams::default();
assert!(open(&poly, &[], ¶ms).is_err());
}
#[test]
fn open_poly_longer_than_point_rejected() {
let poly = make_poly(&[1, 2, 3, 4, 5, 6, 7, 8]);
let point = vec![Goldilocks::ZERO, Goldilocks::ZERO];
let params = ProofParams::default();
assert!(open(&poly, &point, ¶ms).is_err());
}
fn make_axis_opening() -> AxisOpening {
let evals: Vec<Goldilocks> = (1u64..=4).map(Goldilocks::new).collect();
let poly = MultilinearPoly::new(evals);
let commitment = Brakedown::commit(&poly);
let point = vec![Goldilocks::ZERO, Goldilocks::ZERO];
let value = Goldilocks::new(1);
let opening = {
let mut lt = LensTranscript::new(b"e2e-axis-open");
Brakedown::open(&poly, &point, &mut lt)
};
AxisOpening {
commitment,
point,
value,
opening,
transcript_seed: b"e2e-axis-open".to_vec(),
}
}
#[test]
fn e2e_hash_accumulator_roundtrip() {
let mut order = Reduction::<1024>::new();
let s = order.atom(Goldilocks::new(42)).unwrap();
let tag1 = order.atom(Goldilocks::new(1)).unwrap();
let tag15 = order.atom(Goldilocks::new(15)).unwrap();
let quote_f = order.pair(tag1, s).unwrap();
let hash_f = order.pair(tag15, quote_f).unwrap();
let mut trace = VecTrace::default();
nox::reduce(&mut order, s, hash_f, 100, &NullCalls, &mut trace);
assert_eq!(trace.0.len(), 26);
let in_digest = *order.digest(s).unwrap();
let rate = [
in_digest[0],
in_digest[1],
in_digest[2],
in_digest[3],
Goldilocks::ZERO,
Goldilocks::ZERO,
Goldilocks::ZERO,
Goldilocks::ZERO,
];
let hash_aux = HashAux { rate };
let stmt = zero_statement();
let params = ProofParams::default();
let trace_proof = commit(&trace, &[hash_aux], &[], &[], &stmt, ¶ms).unwrap();
assert!(verify(&trace_proof, &stmt, ¶ms).is_ok());
}
#[test]
fn e2e_axis_accumulator_and_statement_binding_roundtrip() {
let mut order = Reduction::<1024>::new();
let s = order.atom(Goldilocks::new(7)).unwrap();
let tag0 = order.atom(Goldilocks::new(0)).unwrap();
let addr1 = order.atom(Goldilocks::new(1)).unwrap();
let axis_f = order.pair(tag0, addr1).unwrap();
let mut trace = VecTrace::default();
nox::reduce(&mut order, s, axis_f, 100, &NullCalls, &mut trace);
nox::reduce(&mut order, s, axis_f, 99, &NullCalls, &mut trace);
assert_eq!(trace.0.len(), 2);
let ao1 = make_axis_opening();
let ao2 = make_axis_opening();
let input_hash = super::hash_row(&trace.0[0]);
let output_hash = super::hash_row(&trace.0[trace.0.len() - 1]);
let stmt = Statement {
program_hash: [0u8; 32],
input_hash,
output_hash,
focus_bound: 10,
bbg_root: [0u8; 32],
};
let params = ProofParams::default();
let trace_proof = commit(&trace, &[], &[ao1, ao2], &[], &stmt, ¶ms).unwrap();
assert!(verify(&trace_proof, &stmt, ¶ms).is_ok());
}
#[test]
fn e2e_statement_binding_rejects_wrong_input_hash() {
let mut order = Reduction::<1024>::new();
let s = order.atom(Goldilocks::new(7)).unwrap();
let tag0 = order.atom(Goldilocks::new(0)).unwrap();
let addr = order.atom(Goldilocks::new(1)).unwrap();
let axis_f = order.pair(tag0, addr).unwrap();
let mut trace = VecTrace::default();
nox::reduce(&mut order, s, axis_f, 100, &NullCalls, &mut trace);
nox::reduce(&mut order, s, axis_f, 99, &NullCalls, &mut trace);
let mut wrong_hash = [0u8; 32];
wrong_hash[0] = 0xff;
let stmt = Statement {
program_hash: [0u8; 32],
input_hash: wrong_hash,
output_hash: [0u8; 32],
focus_bound: 0,
bbg_root: [0u8; 32],
};
let params = ProofParams::default();
let err = commit(
&trace,
&[],
&[make_axis_opening(), make_axis_opening()],
&[],
&stmt,
¶ms,
);
assert!(matches!(err, Err(CommitError::StatementMismatch)));
}
#[test]
fn e2e_statement_binding_rejects_focus_exceeded() {
let trace = malformed_trace(); let stmt = Statement {
program_hash: [0u8; 32],
input_hash: [0u8; 32],
output_hash: [0u8; 32],
focus_bound: 1, };
let params = ProofParams::default();
let err = commit(&trace, &[], &[], &[], &stmt, ¶ms);
assert!(matches!(err, Err(CommitError::FocusExhausted)));
}
struct AxisProver {
commitment: [u8; 32],
}
impl nox::LookProvider for AxisProver {
fn look(
&self,
_commitment: Goldilocks,
_namespace: Goldilocks,
_key: Goldilocks,
) -> Option<Goldilocks> {
None
}
}
impl<const N: usize> nox::CallProvider<N> for AxisProver {
fn provide(
&self,
_reduction: &mut Reduction<N>,
_tag: Goldilocks,
_object: nox::Order,
) -> Option<nox::Order> {
None
}
fn axis_commitment(&self, _object_id: u64) -> Option<[u8; 32]> {
Some(self.commitment)
}
}
fn prover_active_axis_setup(tweak: u64) -> (VecTrace, Vec<AxisOpening>) {
use lens::Transcript as LensTranscript;
let g = Goldilocks::new;
let mut order = Reduction::<1024>::new();
let a = order.atom(g(11)).unwrap();
let b = order.atom(g(22)).unwrap();
let c = order.atom(g(33)).unwrap();
let d = order.atom(g(44)).unwrap();
let left = order.pair(a, b).unwrap();
let right = order.pair(c, d).unwrap();
let s = order.pair(left, right).unwrap();
let tag0 = order.atom(g(0)).unwrap();
let addr = order.atom(g(5)).unwrap();
let axis_f = order.pair(tag0, addr).unwrap();
let ids = [a as u64, b as u64, c as u64, d as u64];
let evals: Vec<Goldilocks> = ids
.iter()
.enumerate()
.map(|(i, &v)| if i == 1 { g(v) } else { g(v + tweak) })
.collect();
let poly = MultilinearPoly::new(evals);
let commitment = Brakedown::commit(&poly);
let prover = AxisProver {
commitment: commitment.as_bytes().try_into().unwrap(),
};
let mut trace = VecTrace::default();
nox::reduce(&mut order, s, axis_f, 100, &prover, &mut trace);
nox::reduce(&mut order, s, axis_f, 99, &prover, &mut trace);
assert_eq!(trace.0.len(), 2);
assert_eq!(trace.0[0].r()[7], b as u64, "axis 5 = tail(head(s)) = b");
assert_ne!(trace.0[0].r()[11], 0, "prover-active row carries the commitment");
let point = crate::ccs::axis_eval_point(5);
let value = poly.evaluate(&point);
assert_eq!(
value,
g(b as u64),
"noun polynomial at the address point is the result particle"
);
let openings = (0..2)
.map(|_| {
let mut lt = LensTranscript::new(b"e2e-axis-prover");
AxisOpening {
commitment,
point: point.clone(),
value,
opening: Brakedown::open(&poly, &point, &mut lt),
transcript_seed: b"e2e-axis-prover".to_vec(),
}
})
.collect();
(trace, openings)
}
#[test]
fn e2e_prover_active_axis_binding_roundtrip() {
let (trace, openings) = prover_active_axis_setup(0);
let stmt = zero_statement();
let params = ProofParams::default();
let trace_proof = commit(&trace, &[], &openings, &[], &stmt, ¶ms).unwrap();
assert!(verify(&trace_proof, &stmt, ¶ms).is_ok());
}
#[test]
fn commit_rejects_swapped_axis_commitment() {
let (trace_p, _) = prover_active_axis_setup(0);
let (_, openings_q) = prover_active_axis_setup(7);
let stmt = zero_statement();
let params = ProofParams::default();
let err = commit(&trace_p, &[], &openings_q, &[], &stmt, ¶ms);
assert!(matches!(err, Err(CommitError::AxisBinding)));
}
#[test]
fn commit_rejects_forged_axis_result() {
let (trace, mut openings) = prover_active_axis_setup(0);
openings[0].value += Goldilocks::ONE;
let stmt = zero_statement();
let params = ProofParams::default();
let err = commit(&trace, &[], &openings, &[], &stmt, ¶ms);
assert!(matches!(err, Err(CommitError::AxisBinding)));
}
#[test]
fn commit_rejects_tampered_axis_opening() {
let (trace, mut openings) = prover_active_axis_setup(0);
if let Opening::Tensor { final_poly, .. } = &mut openings[0].opening {
final_poly[0] ^= 1;
} else {
panic!("Brakedown opening is Tensor");
}
let stmt = zero_statement();
let params = ProofParams::default();
let err = commit(&trace, &[], &openings, &[], &stmt, ¶ms);
assert!(matches!(err, Err(CommitError::AxisBinding)));
}
#[test]
fn verify_rejects_spliced_axis_group() {
let stmt = zero_statement();
let params = ProofParams::default();
let (t1, o1) = prover_active_axis_setup(0);
let (t2, o2) = prover_active_axis_setup(7);
let p1 = commit(&t1, &[], &o1, &[], &stmt, ¶ms).unwrap();
let p2 = commit(&t2, &[], &o2, &[], &stmt, ¶ms).unwrap();
assert!(verify(&p1, &stmt, ¶ms).is_ok());
assert!(verify(&p2, &stmt, ¶ms).is_ok());
let axis_group = |p: &TraceProof| {
let idxs: Vec<usize> = p
.groups
.iter()
.enumerate()
.filter(|(_, (_, a))| a.committed_instance.num_cols == 3)
.map(|(i, _)| i)
.collect();
assert_eq!(idxs.len(), 1, "exactly one axis eq-step group");
idxs[0]
};
let i1 = axis_group(&p1);
let i2 = axis_group(&p2);
assert_ne!(
p1.groups[i1].1.witness_commitment.as_bytes(),
p2.groups[i2].1.witness_commitment.as_bytes(),
"the two axis groups differ (different noun commitments)"
);
let mut spliced = p1.clone();
spliced.groups[i1] = p2.groups[i2].clone();
assert!(
verify(&spliced, &stmt, ¶ms).is_err(),
"axis group spliced from another proof must not verify"
);
}
fn prove_raw_linear_steps(steps: &[(CCSInstance, CCSWitness)]) -> TraceProof {
let mut acc = blank_acc(&steps[0].0);
let mut transcript = Transcript::new();
for (instance, witness) in steps {
crate::folding::fold_step(&mut acc, instance, witness, &mut transcript).unwrap();
}
let proof = decide(&acc, &zero_statement(), &ProofParams::default()).unwrap();
TraceProof {
groups: vec![(proof, acc)],
}
}
#[test]
fn verify_rejects_folded_wrong_commitment_binding() {
use crate::ccs::verifier_steps::read_limb;
use crate::ccs::{eq_step, verifier_steps};
use lens::Transcript as LensTranscript;
let poly_q = MultilinearPoly::new(make_poly(&[9, 8, 7, 6]));
let commitment_q = Brakedown::commit(&poly_q);
let commitment_p = Brakedown::commit(&MultilinearPoly::new(make_poly(&[1, 2, 3, 4])));
let point = vec![Goldilocks::ZERO, Goldilocks::ONE];
let value = poly_q.evaluate(&point);
let opening = {
let mut lt = LensTranscript::new(b"raw-axis");
Brakedown::open(&poly_q, &point, &mut lt)
};
let mut steps = verifier_steps(&commitment_q, &point, value, &opening);
for k in 0..4 {
steps.push(eq_step(
read_limb(commitment_q.as_bytes(), k),
read_limb(commitment_p.as_bytes(), k),
));
}
let trace_proof = prove_raw_linear_steps(&steps);
let err = verify(&trace_proof, &zero_statement(), &ProofParams::default());
assert!(matches!(err, Err(VerifyError::LinearErrorNonzero)));
}
#[test]
fn verify_rejects_folded_forged_result_binding() {
use crate::ccs::{eq_step, verifier_steps};
use lens::Transcript as LensTranscript;
let poly = MultilinearPoly::new(make_poly(&[5, 15, 25, 35]));
let commitment = Brakedown::commit(&poly);
let point = vec![Goldilocks::ONE, Goldilocks::ZERO];
let value = poly.evaluate(&point);
let opening = {
let mut lt = LensTranscript::new(b"raw-axis-forge");
Brakedown::open(&poly, &point, &mut lt)
};
let mut steps = verifier_steps(&commitment, &point, value, &opening);
let forged_r7 = value + Goldilocks::ONE;
steps.push(eq_step(value, forged_r7));
let trace_proof = prove_raw_linear_steps(&steps);
let err = verify(&trace_proof, &zero_statement(), &ProofParams::default());
assert!(matches!(err, Err(VerifyError::LinearErrorNonzero)));
}
#[test]
fn verify_accepts_raw_satisfied_axis_steps() {
use crate::ccs::verifier_steps;
use lens::Transcript as LensTranscript;
let poly = MultilinearPoly::new(make_poly(&[5, 15, 25, 35]));
let commitment = Brakedown::commit(&poly);
let point = vec![Goldilocks::ONE, Goldilocks::ZERO];
let value = poly.evaluate(&point);
let opening = {
let mut lt = LensTranscript::new(b"raw-axis-honest");
Brakedown::open(&poly, &point, &mut lt)
};
let steps = verifier_steps(&commitment, &point, value, &opening);
let trace_proof = prove_raw_linear_steps(&steps);
assert!(verify(&trace_proof, &zero_statement(), &ProofParams::default()).is_ok());
}
fn hash_setup(val: u64) -> (VecTrace, crate::ccs::HashAux) {
let g = Goldilocks::new;
let mut order = Reduction::<1024>::new();
let s = order.atom(g(val)).unwrap();
let tag1 = order.atom(g(1)).unwrap();
let tag15 = order.atom(g(15)).unwrap();
let quote_f = order.pair(tag1, s).unwrap();
let hash_f = order.pair(tag15, quote_f).unwrap();
let mut trace = VecTrace::default();
nox::reduce(&mut order, s, hash_f, 100, &NullCalls, &mut trace);
assert_eq!(trace.0.len(), 26);
let d = *order.digest(s).unwrap();
let rate = [
d[0], d[1], d[2], d[3],
Goldilocks::ZERO, Goldilocks::ZERO, Goldilocks::ZERO, Goldilocks::ZERO,
];
(trace, crate::ccs::HashAux { rate })
}
#[test]
fn e2e_two_hash_blocks_roundtrip() {
let (mut trace, aux1) = hash_setup(42);
let (trace2, aux2) = hash_setup(42);
trace.0.extend(trace2.0);
let stmt = zero_statement();
let params = ProofParams::default();
let tp = commit(&trace, &[aux1, aux2], &[], &[], &stmt, ¶ms).unwrap();
assert!(verify(&tp, &stmt, ¶ms).is_ok());
}
#[test]
fn commit_rejects_tampered_hash_rate() {
let (trace, _) = hash_setup(42);
let forged = crate::ccs::HashAux { rate: [Goldilocks::new(3); 8] };
let err = commit(&trace, &[forged], &[], &[], &zero_statement(), &ProofParams::default());
assert!(matches!(err, Err(CommitError::HashBinding)));
}
#[test]
fn commit_rejects_swapped_hash_rate() {
let (trace, _) = hash_setup(42);
let (_, aux_other) = hash_setup(43);
let err = commit(&trace, &[aux_other], &[], &[], &zero_statement(), &ProofParams::default());
assert!(matches!(err, Err(CommitError::HashBinding)));
}
#[test]
fn verify_rejects_folded_forged_hash_digest() {
use crate::ccs::eq_step;
let (trace, aux) = hash_setup(42);
let mut steps =
crate::ccs::build_hash_binding_steps_from_trace(&trace.0, &[aux]).unwrap();
let digest0 = Goldilocks::new(trace.0[25].r()[4]).canonicalize();
steps.push(eq_step(digest0 + Goldilocks::ONE, digest0));
let trace_proof = prove_raw_linear_steps(&steps);
let err = verify(&trace_proof, &zero_statement(), &ProofParams::default());
assert!(matches!(err, Err(VerifyError::LinearErrorNonzero)));
}
#[test]
fn verify_rejects_spliced_hash_binding_group() {
let stmt = zero_statement();
let params = ProofParams::default();
let (t1, a1) = hash_setup(42);
let (t2, a2) = hash_setup(43);
let p1 = commit(&t1, &[a1], &[], &[], &stmt, ¶ms).unwrap();
let p2 = commit(&t2, &[a2], &[], &[], &stmt, ¶ms).unwrap();
assert!(verify(&p1, &stmt, ¶ms).is_ok());
assert!(verify(&p2, &stmt, ¶ms).is_ok());
let eq_group = |p: &TraceProof| {
let idxs: Vec<usize> = p
.groups
.iter()
.enumerate()
.filter(|(_, (_, a))| a.committed_instance.num_cols == 3)
.map(|(i, _)| i)
.collect();
assert_eq!(idxs.len(), 1, "exactly one eq-step binding group");
idxs[0]
};
let i1 = eq_group(&p1);
let i2 = eq_group(&p2);
assert_ne!(
p1.groups[i1].1.witness_commitment.as_bytes(),
p2.groups[i2].1.witness_commitment.as_bytes(),
"different hashed particles give different binding witnesses"
);
let mut spliced = p1.clone();
spliced.groups[i1] = p2.groups[i2].clone();
assert!(
verify(&spliced, &stmt, ¶ms).is_err(),
"hash binding group spliced from another proof must not verify"
);
}
#[test]
fn e2e_compose_roundtrip() {
let g = Goldilocks::new;
let mut ar = Reduction::<1024>::new();
let obj = ar.atom(g(5)).unwrap();
let t1 = ar.atom(g(1)).unwrap();
let five = ar.atom(g(5)).unwrap();
let nine = ar.atom(g(9)).unwrap();
let qx = ar.pair(t1, five).unwrap();
let q9 = ar.pair(t1, nine).unwrap();
let qq9 = ar.pair(t1, q9).unwrap();
let body = ar.pair(qx, qq9).unwrap();
let t2 = ar.atom(g(2)).unwrap();
let formula = ar.pair(t2, body).unwrap();
let mut trace = VecTrace::default();
nox::reduce(&mut ar, obj, formula, 1000, &NullCalls, &mut trace);
nox::reduce(&mut ar, obj, formula, 1000, &NullCalls, &mut trace);
assert!(trace.0.iter().any(|r| r.r()[0] == 2), "trace has a compose row");
let stmt = zero_statement();
let params = ProofParams::default();
let tp = commit(&trace, &[], &[], &[], &stmt, ¶ms).unwrap();
assert!(verify(&tp, &stmt, ¶ms).is_ok());
}
#[test]
fn e2e_cons_roundtrip() {
let g = Goldilocks::new;
let mut ar = Reduction::<1024>::new();
let obj = ar.atom(g(5)).unwrap();
let t1 = ar.atom(g(1)).unwrap();
let seven = ar.atom(g(7)).unwrap();
let nine = ar.atom(g(9)).unwrap();
let qa = ar.pair(t1, seven).unwrap();
let qb = ar.pair(t1, nine).unwrap();
let body = ar.pair(qa, qb).unwrap();
let t3 = ar.atom(g(3)).unwrap();
let formula = ar.pair(t3, body).unwrap();
let mut trace = VecTrace::default();
nox::reduce(&mut ar, obj, formula, 1000, &NullCalls, &mut trace);
nox::reduce(&mut ar, obj, formula, 1000, &NullCalls, &mut trace);
assert!(trace.0.iter().any(|r| r.r()[0] == 3), "trace has a cons row");
let stmt = zero_statement();
let params = ProofParams::default();
let tp = commit(&trace, &[], &[], &[], &stmt, ¶ms).unwrap();
assert!(verify(&tp, &stmt, ¶ms).is_ok());
}
#[test]
fn verify_rejects_tampered_eval_value() {
let trace = malformed_trace();
let stmt = zero_statement();
let params = ProofParams::default();
let mut trace_proof = commit(&trace, &[], &[], &[], &stmt, ¶ms).unwrap();
let (proof, _acc) = &mut trace_proof.groups[0];
proof.eval_value = Goldilocks::new(proof.eval_value.as_u64().wrapping_add(1));
assert!(verify(&trace_proof, &stmt, ¶ms).is_err());
}
}