use crate::{
CommitmentKey,
bellpepper::{
r1cs::{
MultiRoundSpartanShape, MultiRoundSpartanWitness, PrecommittedState, RerandomizationTrait,
SpartanShape, SpartanWitness,
},
shape_cs::ShapeCS,
solver::SatisfyingAssignment,
},
big_num::DelayedReduction,
digest::{DigestComputer, SimpleDigestible},
errors::SpartanError,
math::Math,
nifs::NovaNIFS,
polys::{
eq::EqPolynomial,
multilinear::{MultilinearPolynomial, SparsePolynomial},
power::PowPolynomial,
univariate::UniPoly,
},
r1cs::{
R1CSInstance, R1CSShape, R1CSWitness, RelaxedR1CSInstance, RelaxedR1CSWitness,
SplitMultiRoundR1CSInstance, SplitMultiRoundR1CSShape, SplitR1CSInstance, SplitR1CSShape,
},
start_span,
sumcheck::SumcheckProof,
traits::{
Engine,
circuit::SpartanCircuit,
pcs::{FoldingEngineTrait, PCSEngineTrait},
snark::{DigestHelperTrait, SpartanDigest},
transcript::TranscriptEngineTrait,
},
zk::NeutronNovaVerifierCircuit,
};
use ff::Field;
use num_traits::Zero;
use once_cell::sync::OnceCell;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
fn compute_tensor_decomp(n: usize) -> (usize, usize, usize) {
let ell = n.next_power_of_two().log_2();
let ell1 = ell.div_ceil(2); let ell2 = ell / 2;
let left = 1 << ell1;
let right = 1 << ell2;
(ell, left, right)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound = "")]
pub struct NeutronNovaNIFS<E: Engine> {
polys: Vec<UniPoly<E::Scalar>>,
}
#[inline]
#[allow(clippy::needless_range_loop)]
fn suffix_weight_full<F: Field>(t: usize, ell_b: usize, pair_idx: usize, rhos: &[F]) -> F {
let mut w = F::ONE;
let mut k = pair_idx;
for s in (t + 1)..ell_b {
let bit = (k & 1) as u8; w *= if bit == 0 { F::ONE - rhos[s] } else { rhos[s] };
k >>= 1;
}
w
}
#[inline]
fn mul_opt<F: Field>(a: &F, b: &F) -> F {
if a == &F::ZERO || b == &F::ZERO {
F::ZERO
} else if a == &F::ONE {
*b
} else if b == &F::ONE {
*a
} else {
*a * *b
}
}
impl<E: Engine> NeutronNovaNIFS<E>
where
E::PCS: FoldingEngineTrait<E>,
{
#[inline]
fn prove_helper(
round: usize,
(left, right): (usize, usize),
e: &[E::Scalar],
Az1: &[E::Scalar],
Bz1: &[E::Scalar],
Cz1: &[E::Scalar],
Az2: &[E::Scalar],
Bz2: &[E::Scalar],
Cz2: &[E::Scalar],
) -> (E::Scalar, E::Scalar) {
type Acc<S> = <S as DelayedReduction<S>>::Accumulator;
assert_eq!(e.len(), left + right);
assert_eq!(Az1.len(), left * right);
assert_eq!(Bz1.len(), left * right);
assert_eq!(Cz1.len(), left * right);
assert_eq!(Az2.len(), left * right);
assert_eq!(Bz2.len(), left * right);
assert_eq!(Cz2.len(), left * right);
let f = &e[left..];
let (acc_e0, acc_quad) = (0..right)
.into_par_iter()
.fold(
|| (Acc::<E::Scalar>::zero(), Acc::<E::Scalar>::zero()),
|mut middle_acc, i| {
let mut inner_e0 = Acc::<E::Scalar>::zero();
let mut inner_quad = Acc::<E::Scalar>::zero();
for (j, e_j) in e[..left].iter().enumerate() {
let k = i * left + j;
if round != 0 {
let inner_val = Az1[k] * Bz1[k] - Cz1[k];
<E::Scalar as DelayedReduction<E::Scalar>>::unreduced_multiply_accumulate(
&mut inner_e0,
e_j,
&inner_val,
);
}
let az_diff = Az2[k] - Az1[k];
let bz_diff = Bz2[k] - Bz1[k];
let quad_val = az_diff * bz_diff;
<E::Scalar as DelayedReduction<E::Scalar>>::unreduced_multiply_accumulate(
&mut inner_quad,
e_j,
&quad_val,
);
}
let inner_e0_red = <E::Scalar as DelayedReduction<E::Scalar>>::reduce(&inner_e0);
let inner_quad_red = <E::Scalar as DelayedReduction<E::Scalar>>::reduce(&inner_quad);
let f_i = &f[i];
<E::Scalar as DelayedReduction<E::Scalar>>::unreduced_multiply_accumulate(
&mut middle_acc.0,
f_i,
&inner_e0_red,
);
<E::Scalar as DelayedReduction<E::Scalar>>::unreduced_multiply_accumulate(
&mut middle_acc.1,
f_i,
&inner_quad_red,
);
middle_acc
},
)
.reduce(
|| (Acc::<E::Scalar>::zero(), Acc::<E::Scalar>::zero()),
|mut a, b| {
a.0 += b.0;
a.1 += b.1;
a
},
);
(
<E::Scalar as DelayedReduction<E::Scalar>>::reduce(&acc_e0),
<E::Scalar as DelayedReduction<E::Scalar>>::reduce(&acc_quad),
)
}
pub fn prove(
S: &SplitR1CSShape<E>,
Us: &[R1CSInstance<E>],
Ws: &[R1CSWitness<E>],
vc: &mut NeutronNovaVerifierCircuit<E>,
vc_state: &mut <SatisfyingAssignment<E> as MultiRoundSpartanWitness<E>>::MultiRoundState,
vc_shape: &SplitMultiRoundR1CSShape<E>,
vc_ck: &CommitmentKey<E>,
transcript: &mut E::TE,
) -> Result<
(
Vec<E::Scalar>, // E_eq (split evals, length left+right)
Vec<E::Scalar>, // Az layer 0
Vec<E::Scalar>, // Bz layer 0
Vec<E::Scalar>, // Cz layer 0
R1CSWitness<E>, // final folded witness
R1CSInstance<E>, // final folded instance
),
SpartanError,
> {
let n = Us.len();
let n_padded = Us.len().next_power_of_two();
let ell_b = n_padded.log_2();
info!(
"NeutronNova NIFS prove for {} instances and padded to {} instances",
Us.len(),
n_padded
);
let mut Us = Us.to_vec();
let mut Ws = Ws.to_vec();
if Us.len() < n_padded {
Us.extend(vec![Us[0].clone(); n_padded - n]);
Ws.extend(vec![Ws[0].clone(); n_padded - n]);
}
for U in Us.iter() {
transcript.absorb(b"U", U);
}
let T = E::Scalar::ZERO;
transcript.absorb(b"T", &T);
let (ell_cons, left, right) = compute_tensor_decomp(S.num_cons);
let tau = transcript.squeeze(b"tau")?;
let E_eq = PowPolynomial::split_evals(tau, ell_cons, left, right);
let mut rhos = Vec::with_capacity(ell_b);
for _ in 0..ell_b {
rhos.push(transcript.squeeze(b"rho")?);
}
let (_matrix_span, matrix_t) =
start_span!("matrix_vector_multiply_instances", instances = n_padded);
let triples = (0..n_padded)
.into_par_iter()
.map(|i| {
let w = &Ws[i].W;
let x = &Us[i].X;
let mut z = Vec::with_capacity(w.len() + 1 + x.len());
z.extend_from_slice(w);
z.push(E::Scalar::ONE);
z.extend_from_slice(x);
S.multiply_vec(&z)
})
.collect::<Result<Vec<_>, _>>()?;
let mut A_layers: Vec<Vec<E::Scalar>> = Vec::with_capacity(n_padded);
let mut B_layers: Vec<Vec<E::Scalar>> = Vec::with_capacity(n_padded);
let mut C_layers: Vec<Vec<E::Scalar>> = Vec::with_capacity(n_padded);
for (a, b, c) in triples {
A_layers.push(a);
B_layers.push(b);
C_layers.push(c);
}
info!(elapsed_ms = %matrix_t.elapsed().as_millis(), instances = n_padded, "matrix_vector_multiply_instances");
let (_nifs_rounds_span, nifs_rounds_t) = start_span!("nifs_folding_rounds", rounds = ell_b);
let mut polys: Vec<UniPoly<E::Scalar>> = Vec::with_capacity(ell_b);
let mut r_bs: Vec<E::Scalar> = Vec::with_capacity(ell_b);
let mut T_cur = E::Scalar::ZERO; let mut acc_eq = E::Scalar::ONE;
let mut m = n_padded;
for t in 0..ell_b {
let rho_t = rhos[t];
let pairs = m / 2;
let (e0, quad_coeff) = A_layers
.par_chunks(2)
.zip(B_layers.par_chunks(2))
.zip(C_layers.par_chunks(2))
.enumerate()
.map(|(pair_idx, ((pair_a, pair_b), pair_c))| {
let (e0, quad_coeff) = Self::prove_helper(
t,
(left, right),
&E_eq,
&pair_a[0],
&pair_b[0],
&pair_c[0],
&pair_a[1],
&pair_b[1],
&pair_c[1],
);
let w = suffix_weight_full::<E::Scalar>(t, ell_b, pair_idx, &rhos);
(e0 * w, quad_coeff * w)
})
.reduce(
|| (E::Scalar::ZERO, E::Scalar::ZERO),
|a, b| (a.0 + b.0, a.1 + b.1),
);
let one_minus_rho = E::Scalar::ONE - rho_t;
let two_rho_minus_one = rho_t - one_minus_rho;
let c = e0 * acc_eq;
let a = quad_coeff * acc_eq;
let a_b_c = (T_cur - c * one_minus_rho) * rho_t.invert().unwrap();
let b = a_b_c - a - c;
let new_a = a * two_rho_minus_one;
let new_b = b * two_rho_minus_one + a * one_minus_rho;
let new_c = c * two_rho_minus_one + b * one_minus_rho;
let new_d = c * one_minus_rho;
let poly_t = UniPoly {
coeffs: vec![new_d, new_c, new_b, new_a],
};
polys.push(poly_t.clone());
let c = &poly_t.coeffs;
vc.nifs_polys[t] = [c[0], c[1], c[2], c[3]];
let chals =
SatisfyingAssignment::<E>::process_round(vc_state, vc_shape, vc_ck, vc, t, transcript)?;
let r_b = chals[0];
r_bs.push(r_b);
acc_eq *= (E::Scalar::ONE - r_b) * (E::Scalar::ONE - rho_t) + r_b * rho_t;
T_cur = poly_t.evaluate(&r_b);
let mut next_A = vec![vec![]; m];
let mut next_B = vec![vec![]; m];
let mut next_C = vec![vec![]; m];
for i in 0..m {
let t = if i & 1 == 0 { i >> 1 } else { (i >> 1) + pairs };
next_A[t] = std::mem::take(&mut A_layers[i]);
next_B[t] = std::mem::take(&mut B_layers[i]);
next_C[t] = std::mem::take(&mut C_layers[i]);
}
A_layers = next_A;
B_layers = next_B;
C_layers = next_C;
for matrix_layer in [&mut A_layers, &mut B_layers, &mut C_layers] {
let (low, high) = matrix_layer.split_at_mut(pairs);
low.iter_mut().zip(high.iter()).for_each(|(lo, hi)| {
lo.iter_mut().zip(hi.iter()).for_each(|(l, h)| {
*l += mul_opt(&(*h - *l), &r_b);
});
});
matrix_layer.truncate(pairs);
}
m = pairs;
}
info!(elapsed_ms = %nifs_rounds_t.elapsed().as_millis(), rounds = ell_b, "nifs_folding_rounds");
let T_out = T_cur * acc_eq.invert().unwrap();
vc.t_out_step = T_out;
vc.eq_rho_at_rb = acc_eq;
let _ =
SatisfyingAssignment::<E>::process_round(vc_state, vc_shape, vc_ck, vc, ell_b, transcript)?;
let (_fold_final_span, fold_final_t) = start_span!("fold_witnesses");
let folded_W = R1CSWitness::fold_multiple(&r_bs, &Ws)?;
info!(elapsed_ms = %fold_final_t.elapsed().as_millis(), "fold_witnesses");
let (_fold_final_span, fold_final_t) = start_span!("fold_instances");
let folded_U = R1CSInstance::fold_multiple(&r_bs, &Us)?;
info!(elapsed_ms = %fold_final_t.elapsed().as_millis(), "fold_instances");
Ok((
E_eq,
A_layers[0].clone(),
B_layers[0].clone(),
C_layers[0].clone(),
folded_W,
folded_U,
))
}
}
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
pub struct NeutronNovaProverKey<E: Engine> {
ck: CommitmentKey<E>,
S_step: SplitR1CSShape<E>,
S_core: SplitR1CSShape<E>,
vk_digest: SpartanDigest, vc_shape: SplitMultiRoundR1CSShape<E>,
vc_shape_regular: R1CSShape<E>,
vc_ck: CommitmentKey<E>,
}
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
pub struct NeutronNovaVerifierKey<E: Engine> {
ck: CommitmentKey<E>,
vk_ee: <E::PCS as PCSEngineTrait<E>>::VerifierKey,
S_step: SplitR1CSShape<E>,
S_core: SplitR1CSShape<E>,
vc_shape: SplitMultiRoundR1CSShape<E>,
vc_shape_regular: R1CSShape<E>,
vc_ck: CommitmentKey<E>,
#[serde(skip, default = "OnceCell::new")]
digest: OnceCell<SpartanDigest>,
}
impl<E: Engine> SimpleDigestible for NeutronNovaVerifierKey<E> {}
impl<E: Engine> DigestHelperTrait<E> for NeutronNovaVerifierKey<E> {
fn digest(&self) -> Result<SpartanDigest, SpartanError> {
self
.digest
.get_or_try_init(|| {
let dc = DigestComputer::<_>::new(self);
dc.digest()
})
.cloned()
.map_err(|_| SpartanError::DigestError {
reason: "Unable to compute digest for SpartanVerifierKey".to_string(),
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound = "")]
pub struct NeutronNovaPrepZkSNARK<E: Engine> {
ps_step: Vec<PrecommittedState<E>>,
ps_core: PrecommittedState<E>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound = "")]
pub struct NeutronNovaZkSNARK<E: Engine> {
step_instances: Vec<SplitR1CSInstance<E>>,
core_instance: SplitR1CSInstance<E>,
eval_arg: <E::PCS as PCSEngineTrait<E>>::EvaluationArgument,
U_verifier: SplitMultiRoundR1CSInstance<E>,
nifs: NovaNIFS<E>,
random_U: RelaxedR1CSInstance<E>,
folded_W: RelaxedR1CSWitness<E>,
}
impl<E: Engine> NeutronNovaZkSNARK<E>
where
E::PCS: FoldingEngineTrait<E>,
{
pub fn setup<C1: SpartanCircuit<E>, C2: SpartanCircuit<E>>(
step_circuit: &C1,
core_circuit: &C2,
num_steps: usize,
) -> Result<(NeutronNovaProverKey<E>, NeutronNovaVerifierKey<E>), SpartanError> {
let (_setup_span, setup_t) = start_span!("neutronnova_setup");
let (_r1cs_span, r1cs_t) = start_span!("r1cs_shape_generation");
debug!("Synthesizing step circuit");
let mut S_step = ShapeCS::r1cs_shape(step_circuit)?;
debug!("Finished synthesizing step circuit");
debug!("Synthesizing core circuit");
let mut S_core = ShapeCS::r1cs_shape(core_circuit)?;
debug!("Finished synthesizing core circuit");
SplitR1CSShape::equalize(&mut S_step, &mut S_core);
info!(
"Step circuit's witness sizes: shared = {}, precommitted = {}, rest = {}",
S_step.num_shared, S_step.num_precommitted, S_step.num_rest
);
info!(
"Core circuit's witness sizes: shared = {}, precommitted = {}, rest = {}",
S_core.num_shared, S_core.num_precommitted, S_core.num_rest
);
info!(elapsed_ms = %r1cs_t.elapsed().as_millis(), "r1cs_shape_generation");
let (_ck_span, ck_t) = start_span!("commitment_key_generation");
let (ck, vk_ee) = SplitR1CSShape::commitment_key(&[&S_step, &S_core])?;
info!(elapsed_ms = %ck_t.elapsed().as_millis(), "commitment_key_generation");
let (_vc_span, vc_t) = start_span!("verifier_circuit_setup");
let num_rounds_b = num_steps.next_power_of_two().log_2();
let num_vars = S_step.num_shared + S_step.num_precommitted + S_step.num_rest;
let num_rounds_x = usize::try_from(S_step.num_cons.ilog2()).unwrap();
let num_rounds_y = usize::try_from(num_vars.ilog2()).unwrap() + 1;
let vc = NeutronNovaVerifierCircuit::<E>::default(num_rounds_b, num_rounds_x, num_rounds_y);
let (vc_shape, vc_ck, _vk_mr) =
<ShapeCS<E> as MultiRoundSpartanShape<E>>::multiround_r1cs_shape(&vc)?;
let vc_shape_regular = vc_shape.to_regular_shape();
info!(elapsed_ms = %vc_t.elapsed().as_millis(), "verifier_circuit_setup");
let vk: NeutronNovaVerifierKey<E> = NeutronNovaVerifierKey {
ck: ck.clone(),
S_step: S_step.clone(),
S_core: S_core.clone(),
vk_ee,
vc_shape: vc_shape.clone(),
vc_shape_regular: vc_shape_regular.clone(),
vc_ck: vc_ck.clone(),
digest: OnceCell::new(),
};
let pk = NeutronNovaProverKey {
ck,
S_step,
S_core,
vc_shape,
vc_shape_regular,
vc_ck,
vk_digest: vk.digest()?,
};
info!(elapsed_ms = %setup_t.elapsed().as_millis(), "neutronnova_setup");
Ok((pk, vk))
}
pub fn prep_prove<C1: SpartanCircuit<E>, C2: SpartanCircuit<E>>(
pk: &NeutronNovaProverKey<E>,
step_circuits: &[C1],
core_circuit: &C2,
is_small: bool, ) -> Result<NeutronNovaPrepZkSNARK<E>, SpartanError> {
let (_prep_span, prep_t) = start_span!("neutronnova_prep_prove");
let (_shared_span, shared_t) = start_span!("generate_shared_witness");
let mut ps =
SatisfyingAssignment::shared_witness(&pk.S_step, &pk.ck, &step_circuits[0], is_small)?;
info!(elapsed_ms = %shared_t.elapsed().as_millis(), "generate_shared_witness");
let (_precommit_span, precommit_t) = start_span!(
"generate_precommitted_witnesses",
circuits = step_circuits.len() + 1
);
let ps_step = (0..step_circuits.len())
.into_par_iter()
.map(|i| {
let mut ps_i = ps.clone();
SatisfyingAssignment::precommitted_witness(
&mut ps_i,
&pk.S_step,
&pk.ck,
&step_circuits[i],
is_small,
)?;
Ok(ps_i)
})
.collect::<Result<Vec<_>, _>>()?;
SatisfyingAssignment::precommitted_witness(
&mut ps,
&pk.S_core,
&pk.ck,
core_circuit,
is_small,
)?;
info!(elapsed_ms = %precommit_t.elapsed().as_millis(), circuits = step_circuits.len() + 1, "generate_precommitted_witnesses");
info!(elapsed_ms = %prep_t.elapsed().as_millis(), "neutronnova_prep_prove");
Ok(NeutronNovaPrepZkSNARK {
ps_step,
ps_core: ps,
})
}
pub fn prove<C1: SpartanCircuit<E>, C2: SpartanCircuit<E>>(
pk: &NeutronNovaProverKey<E>,
step_circuits: &[C1],
core_circuit: &C2,
prep_snark: &NeutronNovaPrepZkSNARK<E>,
is_small: bool, ) -> Result<Self, SpartanError> {
let (_prove_span, prove_t) = start_span!("neutronnova_prove");
let (_rerandomize_span, rerandomize_t) = start_span!("rerandomize_prep_state");
let mut ps_core = prep_snark.ps_core.rerandomize(&pk.ck, &pk.S_core)?;
let mut ps_step = prep_snark
.ps_step
.par_iter()
.map(|ps_i| {
ps_i.rerandomize_with_shared(
&pk.ck,
&pk.S_step,
&ps_core.comm_W_shared,
&ps_core.r_W_shared,
)
})
.collect::<Result<Vec<_>, _>>()?;
info!(elapsed_ms = %rerandomize_t.elapsed().as_millis(), "rerandomize_prep_state");
let (_gen_span, gen_t) = start_span!(
"generate_instances_witnesses",
step_circuits = step_circuits.len()
);
let (res_steps, res_core) = rayon::join(
|| {
ps_step
.par_iter_mut()
.zip(step_circuits.par_iter().enumerate())
.map(|(pre_state, (i, circuit))| {
let mut transcript = E::TE::new(b"neutronnova_prove");
transcript.absorb(b"vk", &pk.vk_digest);
transcript.absorb(
b"num_circuits",
&E::Scalar::from(step_circuits.len() as u64),
);
transcript.absorb(b"circuit_index", &E::Scalar::from(i as u64));
let public_values =
circuit
.public_values()
.map_err(|e| SpartanError::SynthesisError {
reason: format!("Circuit does not provide public IO: {e}"),
})?;
transcript.absorb(b"public_values", &public_values.as_slice());
SatisfyingAssignment::r1cs_instance_and_witness(
pre_state,
&pk.S_step,
&pk.ck,
circuit,
is_small,
&mut transcript,
)
})
.try_fold(
|| (Vec::new(), Vec::new()),
|mut acc, res: Result<_, SpartanError>| {
let (u, w) = res?;
acc.0.push(u);
acc.1.push(w);
Ok(acc)
},
)
.try_reduce(
|| (Vec::new(), Vec::new()),
|mut a, mut b| {
a.0.append(&mut b.0);
a.1.append(&mut b.1);
Ok(a)
},
)
},
|| {
let mut transcript = E::TE::new(b"neutronnova_prove");
transcript.absorb(b"vk", &pk.vk_digest);
let public_values_core =
core_circuit
.public_values()
.map_err(|e| SpartanError::SynthesisError {
reason: format!("Core circuit does not provide public IO: {e}"),
})?;
transcript.absorb(b"public_values", &public_values_core.as_slice());
SatisfyingAssignment::r1cs_instance_and_witness(
&mut ps_core,
&pk.S_core,
&pk.ck,
core_circuit,
is_small,
&mut transcript,
)
},
);
let ((step_instances, step_witnesses), (core_instance, core_witness)) = (res_steps?, res_core?);
info!(elapsed_ms = %gen_t.elapsed().as_millis(), step_circuits = step_circuits.len(), "generate_instances_witnesses");
let (_reg_span, reg_t) = start_span!("convert_to_regular_instances");
let step_instances_regular = step_instances
.iter()
.map(|u| u.to_regular_instance())
.collect::<Result<Vec<_>, _>>()?;
let core_instance_regular = core_instance.to_regular_instance()?;
info!(elapsed_ms = %reg_t.elapsed().as_millis(), "convert_to_regular_instances");
let mut transcript = E::TE::new(b"neutronnova_prove");
transcript.absorb(b"vk", &pk.vk_digest);
transcript.absorb(b"core_instance", &core_instance_regular);
let n_padded = step_instances_regular.len().next_power_of_two();
let num_vars = pk.S_step.num_shared + pk.S_step.num_precommitted + pk.S_step.num_rest;
let num_rounds_b = n_padded.log_2();
let num_rounds_x = pk.S_step.num_cons.log_2();
let num_rounds_y = num_vars.log_2() + 1;
let mut vc = NeutronNovaVerifierCircuit::<E>::default(num_rounds_b, num_rounds_x, num_rounds_y);
let mut vc_state = SatisfyingAssignment::<E>::initialize_multiround_witness(&pk.vc_shape)?;
let (_nifs_span, nifs_t) = start_span!("NIFS");
let (E_eq, Az_step, Bz_step, Cz_step, folded_W, folded_U) = NeutronNovaNIFS::<E>::prove(
&pk.S_step,
&step_instances_regular,
&step_witnesses,
&mut vc,
&mut vc_state,
&pk.vc_shape,
&pk.vc_ck,
&mut transcript,
)?;
info!(elapsed_ms = %nifs_t.elapsed().as_millis(), "NIFS");
let (_tensor_span, tensor_t) = start_span!("compute_tensor_and_poly_tau");
let (_ell, left, _right) = compute_tensor_decomp(pk.S_step.num_cons);
let mut E1 = E_eq;
let E2 = E1.split_off(left);
let mut poly_tau_left = MultilinearPolynomial::new(E1);
let poly_tau_right = MultilinearPolynomial::new(E2);
info!(elapsed_ms = %tensor_t.elapsed().as_millis(), "compute_tensor_and_poly_tau");
let (_mp_span, mp_t) = start_span!("prepare_multilinear_polys");
let (mut poly_Az_step, mut poly_Bz_step, mut poly_Cz_step) = (
MultilinearPolynomial::new(Az_step),
MultilinearPolynomial::new(Bz_step),
MultilinearPolynomial::new(Cz_step),
);
let (mut poly_Az_core, mut poly_Bz_core, mut poly_Cz_core) = {
let (_core_span, core_t) = start_span!("compute_core_polys");
let z = [
core_witness.W.clone(),
vec![E::Scalar::ONE],
core_instance.public_values.clone(),
core_instance.challenges.clone(),
]
.concat();
let (Az, Bz, Cz) = pk.S_core.multiply_vec(&z)?;
info!(elapsed_ms = %core_t.elapsed().as_millis(), "compute_core_polys");
(
MultilinearPolynomial::new(Az),
MultilinearPolynomial::new(Bz),
MultilinearPolynomial::new(Cz),
)
};
info!(elapsed_ms = %mp_t.elapsed().as_millis(), "prepare_multilinear_polys");
let outer_start_index = num_rounds_b + 1;
let (_sc_span, sc_t) = start_span!("outer_sumcheck_batched");
let r_x = SumcheckProof::<E>::prove_cubic_with_additive_term_batched_zk(
num_rounds_x,
&mut poly_tau_left,
&poly_tau_right,
&mut poly_Az_step,
&mut poly_Az_core,
&mut poly_Bz_step,
&mut poly_Bz_core,
&mut poly_Cz_step,
&mut poly_Cz_core,
&mut vc,
&mut vc_state,
&pk.vc_shape,
&pk.vc_ck,
&mut transcript,
outer_start_index,
)?;
info!(elapsed_ms = %sc_t.elapsed().as_millis(), "outer_sumcheck_batched");
vc.claim_Az_step = poly_Az_step[0];
vc.claim_Bz_step = poly_Bz_step[0];
vc.claim_Cz_step = poly_Cz_step[0];
vc.claim_Az_core = poly_Az_core[0];
vc.claim_Bz_core = poly_Bz_core[0];
vc.claim_Cz_core = poly_Cz_core[0];
vc.tau_at_rx = poly_tau_left[0];
let chals = SatisfyingAssignment::<E>::process_round(
&mut vc_state,
&pk.vc_shape,
&pk.vc_ck,
&vc,
outer_start_index + num_rounds_x,
&mut transcript,
)?;
let r = chals[0];
let claim_inner_joint_step = vc.claim_Az_step + r * vc.claim_Bz_step + r * r * vc.claim_Cz_step;
let claim_inner_joint_core = vc.claim_Az_core + r * vc.claim_Bz_core + r * r * vc.claim_Cz_core;
let (_eval_rx_span, eval_rx_t) = start_span!("compute_eval_rx");
let evals_rx = EqPolynomial::evals_from_points(&r_x);
info!(elapsed_ms = %eval_rx_t.elapsed().as_millis(), "compute_eval_rx");
let (_sparse_span, sparse_t) = start_span!("compute_eval_table_sparse");
let (evals_A_step, evals_B_step, evals_C_step) = pk.S_step.bind_row_vars(&evals_rx);
let (evals_A_core, evals_B_core, evals_C_core) = pk.S_core.bind_row_vars(&evals_rx);
info!(elapsed_ms = %sparse_t.elapsed().as_millis(), "compute_eval_table_sparse");
let (_abc_span, abc_t) = start_span!("prepare_poly_ABC");
let poly_ABC_step = (0..evals_A_step.len())
.into_par_iter()
.map(|i| evals_A_step[i] + r * evals_B_step[i] + r * r * evals_C_step[i])
.collect::<Vec<E::Scalar>>();
let poly_ABC_core = (0..evals_A_core.len())
.into_par_iter()
.map(|i| evals_A_core[i] + r * evals_B_core[i] + r * r * evals_C_core[i])
.collect::<Vec<E::Scalar>>();
info!(elapsed_ms = %abc_t.elapsed().as_millis(), "prepare_poly_ABC");
let (_sc2_span, sc2_t) = start_span!("inner_sumcheck_batched");
debug!("Proving inner sum-check with {} rounds", num_rounds_y);
debug!(
"Inner sum-check sizes - poly_ABC_step: {}, poly_ABC_core: {}",
poly_ABC_step.len(),
poly_ABC_core.len()
);
let (r_y, evals) = SumcheckProof::<E>::prove_quad_batched_zk(
&[claim_inner_joint_step, claim_inner_joint_core],
num_rounds_y,
&mut MultilinearPolynomial::new(poly_ABC_step),
&mut MultilinearPolynomial::new(poly_ABC_core),
&mut MultilinearPolynomial::new({
let mut v = vec![E::Scalar::ZERO; num_vars * 2];
let w_len = folded_W.W.len();
v[..w_len].copy_from_slice(&folded_W.W);
v[w_len] = E::Scalar::ONE;
let x_len = folded_U.X.len();
v[w_len + 1..w_len + 1 + x_len].copy_from_slice(&folded_U.X);
v
}),
&mut MultilinearPolynomial::new({
let mut v = vec![E::Scalar::ZERO; num_vars * 2];
let w_len = core_witness.W.len();
v[..w_len].copy_from_slice(&core_witness.W);
v[w_len] = E::Scalar::ONE;
let x_len = core_instance_regular.X.len();
v[w_len + 1..w_len + 1 + x_len].copy_from_slice(&core_instance_regular.X);
v
}),
&mut vc,
&mut vc_state,
&pk.vc_shape,
&pk.vc_ck,
&mut transcript,
outer_start_index + num_rounds_x + 1,
)?;
info!(elapsed_ms = %sc2_t.elapsed().as_millis(), "inner_sumcheck_batched");
let eval_Z_step = evals[2];
let eval_Z_core = evals[3];
let eval_X_step = {
let X = vec![E::Scalar::ONE]
.into_iter()
.chain(folded_U.X.iter().cloned())
.collect::<Vec<E::Scalar>>();
let num_vars_log2 = usize::try_from(num_vars.ilog2()).unwrap();
SparsePolynomial::new(num_vars_log2, X).evaluate(&r_y[1..])
};
let eval_X_core = {
let X = vec![E::Scalar::ONE]
.into_iter()
.chain(core_instance_regular.X.iter().cloned())
.collect::<Vec<E::Scalar>>();
let num_vars_log2 = usize::try_from(num_vars.ilog2()).unwrap();
SparsePolynomial::new(num_vars_log2, X).evaluate(&r_y[1..])
};
let eval_W_step =
(eval_Z_step - r_y[0] * eval_X_step) * (E::Scalar::ONE - r_y[0]).invert().unwrap();
let eval_W_core =
(eval_Z_core - r_y[0] * eval_X_core) * (E::Scalar::ONE - r_y[0]).invert().unwrap();
vc.eval_W_step = eval_W_step;
vc.eval_W_core = eval_W_core;
vc.eval_X_step = eval_X_step;
vc.eval_X_core = eval_X_core;
let _ = SatisfyingAssignment::<E>::process_round(
&mut vc_state,
&pk.vc_shape,
&pk.vc_ck,
&vc,
outer_start_index + num_rounds_x + 1 + num_rounds_y,
&mut transcript,
)?;
let eval_w_step_commit_round = outer_start_index + num_rounds_x + 1 + num_rounds_y + 1;
let _ = SatisfyingAssignment::<E>::process_round(
&mut vc_state,
&pk.vc_shape,
&pk.vc_ck,
&vc,
eval_w_step_commit_round,
&mut transcript,
)?;
let _ = SatisfyingAssignment::<E>::process_round(
&mut vc_state,
&pk.vc_shape,
&pk.vc_ck,
&vc,
eval_w_step_commit_round + 1,
&mut transcript,
)?;
let (U_verifier, W_verifier) =
SatisfyingAssignment::<E>::finalize_multiround_witness(&mut vc_state, &pk.vc_shape)?;
let U_verifier_regular = U_verifier.to_regular_instance()?;
let (random_U, random_W) = pk
.vc_shape_regular
.sample_random_instance_witness(&pk.vc_ck)?;
let (nifs, folded_W_verifier) = NovaNIFS::<E>::prove(
&pk.vc_ck,
&pk.vc_shape_regular,
&random_U,
&random_W,
&U_verifier_regular,
&W_verifier,
&mut transcript,
)?;
let comm_eval_W_step = U_verifier.comm_w_per_round[eval_w_step_commit_round].clone();
let blind_eval_W_step = vc_state.r_w_per_round[eval_w_step_commit_round].clone();
let comm_eval_W_core = U_verifier.comm_w_per_round[eval_w_step_commit_round + 1].clone();
let blind_eval_W_core = vc_state.r_w_per_round[eval_w_step_commit_round + 1].clone();
let c_eval = transcript.squeeze(b"c_eval")?;
let (_fold_eval_span, fold_eval_t) = start_span!("fold_evaluation_claims");
let comm = <E::PCS as FoldingEngineTrait<E>>::fold_commitments(
&[folded_U.comm_W, core_instance_regular.comm_W],
&[E::Scalar::ONE, c_eval],
)?;
let blind = <E::PCS as FoldingEngineTrait<E>>::fold_blinds(
&[folded_W.r_W.clone(), core_witness.r_W.clone()],
&[E::Scalar::ONE, c_eval],
)?;
let W = folded_W
.W
.par_iter()
.zip(core_witness.W.par_iter())
.map(|(w1, w2)| *w1 + c_eval * *w2)
.collect::<Vec<_>>();
let comm_eval = <E::PCS as FoldingEngineTrait<E>>::fold_commitments(
&[comm_eval_W_step, comm_eval_W_core],
&[E::Scalar::ONE, c_eval],
)?;
let blind_eval = <E::PCS as FoldingEngineTrait<E>>::fold_blinds(
&[blind_eval_W_step, blind_eval_W_core],
&[E::Scalar::ONE, c_eval],
)?;
info!(elapsed_ms = %fold_eval_t.elapsed().as_millis(), "fold_evaluation_claims");
let (_pcs_span, pcs_t) = start_span!("pcs_prove");
let eval_arg = E::PCS::prove(
&pk.ck,
&pk.vc_ck,
&mut transcript,
&comm,
&W,
&blind,
&r_y[1..],
&comm_eval,
&blind_eval,
)?;
info!(elapsed_ms = %pcs_t.elapsed().as_millis(), "pcs_prove");
let result = Self {
step_instances,
core_instance,
eval_arg,
U_verifier,
nifs,
random_U,
folded_W: folded_W_verifier,
};
info!(elapsed_ms = %prove_t.elapsed().as_millis(), "neutronnova_prove");
Ok(result)
}
pub fn verify(
&self,
vk: &NeutronNovaVerifierKey<E>,
num_instances: usize,
) -> Result<(Vec<Vec<E::Scalar>>, Vec<E::Scalar>), SpartanError> {
let (_verify_span, _verify_t) = start_span!("neutronnova_verify");
if num_instances != self.step_instances.len() {
return Err(SpartanError::ProofVerifyError {
reason: format!(
"Expected {} instances, got {}",
num_instances,
self.step_instances.len()
),
});
}
let (_validate_span, validate_t) =
start_span!("validate_instances", instances = self.step_instances.len());
for (i, u) in self.step_instances.iter().enumerate() {
let mut transcript = E::TE::new(b"neutronnova_prove");
transcript.absorb(b"vk", &vk.digest()?);
transcript.absorb(
b"num_circuits",
&E::Scalar::from(self.step_instances.len() as u64),
);
transcript.absorb(b"circuit_index", &E::Scalar::from(i as u64));
transcript.absorb(b"public_values", &u.public_values.as_slice());
u.validate(&vk.S_step, &mut transcript)?;
}
let mut transcript = E::TE::new(b"neutronnova_prove");
transcript.absorb(b"vk", &vk.digest()?);
transcript.absorb(
b"public_values",
&self.core_instance.public_values.as_slice(),
);
self.core_instance.validate(&vk.S_core, &mut transcript)?;
info!(elapsed_ms = %validate_t.elapsed().as_millis(), instances = self.step_instances.len(), "validate_instances");
for u in &self.step_instances {
if u.comm_W_shared != self.core_instance.comm_W_shared {
return Err(SpartanError::ProofVerifyError {
reason: "All instances must have the same shared commitment".to_string(),
});
}
}
let (_convert_span, convert_t) = start_span!("convert_to_regular_verify");
let mut step_instances_padded = self.step_instances.clone();
if step_instances_padded.len() != step_instances_padded.len().next_power_of_two() {
step_instances_padded.extend(std::iter::repeat_n(
step_instances_padded[0].clone(),
step_instances_padded.len().next_power_of_two() - step_instances_padded.len(),
));
}
let step_instances_regular = step_instances_padded
.par_iter()
.map(|u| u.to_regular_instance())
.collect::<Result<Vec<_>, _>>()?;
let core_instance_regular = self.core_instance.to_regular_instance()?;
info!(elapsed_ms = %convert_t.elapsed().as_millis(), "convert_to_regular_verify");
let mut transcript = E::TE::new(b"neutronnova_prove");
transcript.absorb(b"vk", &vk.digest()?);
transcript.absorb(b"core_instance", &core_instance_regular);
for U in step_instances_regular.iter() {
transcript.absorb(b"U", U);
}
transcript.absorb(b"T", &E::Scalar::ZERO);
let num_rounds_b = step_instances_regular.len().log_2();
let num_vars = vk.S_step.num_shared + vk.S_step.num_precommitted + vk.S_step.num_rest;
let num_rounds_x = vk.S_step.num_cons.log_2();
let num_rounds_y = num_vars.log_2() + 1;
let tau = transcript.squeeze(b"tau")?;
let rhos = (0..num_rounds_b)
.map(|_| transcript.squeeze(b"rho"))
.collect::<Result<Vec<_>, _>>()?;
self.U_verifier.validate(&vk.vc_shape, &mut transcript)?;
let U_verifier_regular = self.U_verifier.to_regular_instance()?;
let num_public_values = 6usize;
let num_challenges = num_rounds_b + num_rounds_x + 1 + num_rounds_y;
if U_verifier_regular.X.len() != num_challenges + num_public_values {
return Err(SpartanError::ProofVerifyError {
reason: format!(
"Verifier instance has incorrect number of public IO: expected {}, got {}",
num_challenges + num_public_values,
U_verifier_regular.X.len()
),
});
}
let challenges = &U_verifier_regular.X[0..num_challenges];
let public_values = &U_verifier_regular.X[num_challenges..num_challenges + 6];
let r_b = challenges[0..num_rounds_b].to_vec();
let r_x = challenges[num_rounds_b..num_rounds_b + num_rounds_x].to_vec();
let r = challenges[num_rounds_b + num_rounds_x]; let r_y = challenges[num_rounds_b + num_rounds_x + 1..].to_vec();
let folded_U = R1CSInstance::fold_multiple(&r_b, &step_instances_regular)?;
let folded_U_verifier =
self
.nifs
.verify(&mut transcript, &self.random_U, &U_verifier_regular)?;
vk.vc_shape_regular
.is_sat_relaxed(&vk.vc_ck, &folded_U_verifier, &self.folded_W)
.map_err(|e| SpartanError::ProofVerifyError {
reason: format!("Folded instance not satisfiable: {e}"),
})?;
let (_matrix_eval_span, matrix_eval_t) = start_span!("matrix_evaluations");
let (eval_A_step, eval_B_step, eval_C_step, eval_A_core, eval_B_core, eval_C_core) = {
let T_x = EqPolynomial::evals_from_points(&r_x);
let T_y = EqPolynomial::evals_from_points(&r_y);
let (eval_A_step, eval_B_step, eval_C_step) = vk.S_step.evaluate_with_tables(&T_x, &T_y);
let (eval_A_core, eval_B_core, eval_C_core) = vk.S_core.evaluate_with_tables(&T_x, &T_y);
(
eval_A_step,
eval_B_step,
eval_C_step,
eval_A_core,
eval_B_core,
eval_C_core,
)
};
info!(elapsed_ms = %matrix_eval_t.elapsed().as_millis(), "matrix_evaluations");
let eval_X_step = {
let X = vec![E::Scalar::ONE]
.into_iter()
.chain(folded_U.X.iter().cloned())
.collect::<Vec<E::Scalar>>();
let num_vars_log2 = usize::try_from(num_vars.ilog2()).unwrap();
SparsePolynomial::new(num_vars_log2, X).evaluate(&r_y[1..])
};
let eval_X_core = {
let X = vec![E::Scalar::ONE]
.into_iter()
.chain(core_instance_regular.X.iter().cloned())
.collect::<Vec<E::Scalar>>();
let num_vars_log2 = usize::try_from(num_vars.ilog2()).unwrap();
SparsePolynomial::new(num_vars_log2, X).evaluate(&r_y[1..])
};
let quotient_step = eval_A_step + r * eval_B_step + r * r * eval_C_step;
let quotient_core = eval_A_core + r * eval_B_core + r * r * eval_C_core;
let tau_at_rx = PowPolynomial::new(&tau, r_x.len()).evaluate(&r_x)?;
let eq_rho_at_rb = EqPolynomial::new(r_b).evaluate(&rhos);
if public_values[0] != tau_at_rx
|| public_values[1] != eval_X_step
|| public_values[2] != eval_X_core
|| public_values[3] != eq_rho_at_rb
|| public_values[4] != quotient_step
|| public_values[5] != quotient_core
{
return Err(SpartanError::ProofVerifyError {
reason:
"Verifier instance public tau_at_rx/eval_X_step/eq_rho_at_rb/eval_X_core/quotients do not match recomputation"
.to_string(),
});
}
let c_eval = transcript.squeeze(b"c_eval")?;
let eval_w_step_commit_round = num_rounds_b + 1 + num_rounds_x + 1 + num_rounds_y + 1;
let comm_eval_W_step = self.U_verifier.comm_w_per_round[eval_w_step_commit_round].clone();
let comm_eval_W_core = self.U_verifier.comm_w_per_round[eval_w_step_commit_round + 1].clone();
let comm = <E::PCS as FoldingEngineTrait<E>>::fold_commitments(
&[folded_U.comm_W, core_instance_regular.comm_W],
&[E::Scalar::ONE, c_eval],
)?;
let comm_eval = <E::PCS as FoldingEngineTrait<E>>::fold_commitments(
&[comm_eval_W_step, comm_eval_W_core],
&[E::Scalar::ONE, c_eval],
)?;
let (_pcs_verify_span, pcs_verify_t) = start_span!("pcs_verify");
E::PCS::verify(
&vk.vk_ee,
&vk.vc_ck,
&mut transcript,
&comm,
&r_y[1..],
&comm_eval,
&self.eval_arg,
)?;
info!(elapsed_ms = %pcs_verify_t.elapsed().as_millis(), "pcs_verify");
info!(elapsed_ms = %_verify_t.elapsed().as_millis(), "neutronnova_verify");
let public_values_step = self
.step_instances
.iter()
.take(num_instances)
.map(|u| u.public_values.clone())
.collect::<Vec<Vec<_>>>();
let public_values_core = self.core_instance.public_values.clone();
Ok((public_values_step, public_values_core))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::T256HyraxEngine;
use bellpepper::gadgets::{
boolean::{AllocatedBit, Boolean},
num::AllocatedNum,
sha256::sha256,
};
use bellpepper_core::{ConstraintSystem, SynthesisError};
use core::marker::PhantomData;
#[derive(Clone, Debug)]
struct Sha256Circuit<E: Engine> {
preimage: Vec<u8>,
_p: PhantomData<E>,
}
impl<E: Engine> SpartanCircuit<E> for Sha256Circuit<E> {
fn public_values(&self) -> Result<Vec<E::Scalar>, SynthesisError> {
Ok(vec![E::Scalar::ZERO]) }
fn shared<CS: ConstraintSystem<E::Scalar>>(
&self,
_: &mut CS,
) -> Result<Vec<AllocatedNum<E::Scalar>>, SynthesisError> {
Ok(vec![]) }
fn precommitted<CS: ConstraintSystem<E::Scalar>>(
&self,
_: &mut CS,
_: &[AllocatedNum<E::Scalar>],
) -> Result<Vec<AllocatedNum<E::Scalar>>, SynthesisError> {
Ok(vec![]) }
fn num_challenges(&self) -> usize {
0 }
fn synthesize<CS: ConstraintSystem<E::Scalar>>(
&self,
cs: &mut CS,
_shared: &[AllocatedNum<E::Scalar>],
_precommitted: &[AllocatedNum<E::Scalar>],
_challenges: Option<&[E::Scalar]>, ) -> Result<(), SynthesisError> {
let bit_values: Vec<_> = self
.preimage
.clone()
.into_iter()
.flat_map(|byte| (0..8).map(move |i| (byte >> i) & 1u8 == 1u8))
.map(Some)
.collect();
assert_eq!(bit_values.len(), self.preimage.len() * 8);
let preimage_bits = bit_values
.into_iter()
.enumerate()
.map(|(i, b)| AllocatedBit::alloc(cs.namespace(|| format!("preimage bit {i}")), b))
.map(|b| b.map(Boolean::from))
.collect::<Result<Vec<_>, _>>()?;
let _ = sha256(cs.namespace(|| "sha256"), &preimage_bits)?;
let x = AllocatedNum::alloc(cs.namespace(|| "x"), || Ok(E::Scalar::ZERO))?;
x.inputize(cs.namespace(|| "inputize x"))?;
Ok(())
}
}
fn generate_sha_r1cs<E: Engine>(
num_circuits: usize,
len: usize,
) -> (
NeutronNovaProverKey<E>,
NeutronNovaVerifierKey<E>,
Vec<Sha256Circuit<E>>,
)
where
E::PCS: FoldingEngineTrait<E>, {
let circuit = Sha256Circuit::<E> {
preimage: vec![0u8; len],
_p: Default::default(),
};
let (pk, vk) = NeutronNovaZkSNARK::<E>::setup(&circuit, &circuit, num_circuits).unwrap();
let circuits = (0..num_circuits)
.map(|i| Sha256Circuit::<E> {
preimage: vec![i as u8; len],
_p: Default::default(),
})
.collect::<Vec<_>>();
(pk, vk, circuits)
}
fn test_neutron_inner<E: Engine, C1: SpartanCircuit<E>, C2: SpartanCircuit<E>>(
name: &str,
pk: &NeutronNovaProverKey<E>,
vk: &NeutronNovaVerifierKey<E>,
step_circuits: &[C1],
core_circuit: &C2,
) where
E::PCS: FoldingEngineTrait<E>,
{
println!(
"[bench_neutron_inner] name: {name}, num_circuits: {}",
step_circuits.len()
);
let ps = NeutronNovaZkSNARK::<E>::prep_prove(pk, step_circuits, core_circuit, true).unwrap();
let res = NeutronNovaZkSNARK::prove(pk, step_circuits, core_circuit, &ps, true);
assert!(res.is_ok());
let snark = res.unwrap();
let res = snark.verify(vk, step_circuits.len());
println!(
"[bench_neutron_inner] name: {name}, num_circuits: {}, verify res: {:?}",
step_circuits.len(),
res
);
assert!(res.is_ok());
let (public_values_step, _public_values_core) = res.unwrap();
assert_eq!(public_values_step.len(), step_circuits.len());
}
#[test]
fn test_neutron_sha256() {
let _ = tracing_subscriber::fmt()
.with_target(false)
.with_ansi(true)
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init();
type E = T256HyraxEngine;
for num_circuits in [2, 7, 32, 64] {
for len in [32, 64].iter() {
let (pk, vk, circuits) = generate_sha_r1cs::<E>(num_circuits, *len);
test_neutron_inner(
&format!("sha256_num_circuits={num_circuits}_len={len}"),
&pk,
&vk,
&circuits,
&circuits[0], );
}
}
}
}