use super::ipa;
use crate::transcript::Transcript;
use bytes::{Buf, BufMut};
use commonware_codec::{Encode, EncodeSize, Error, Read, Write};
use commonware_math::{
algebra::{powers, Additive, CryptoGroup, Field, HashToGroup, Random, Ring, Space},
synthetic::Synthetic,
};
use commonware_parallel::{Sequential, Strategy};
use commonware_utils::ordered::Set;
use rand_core::CryptoRngCore;
use std::{
collections::BTreeMap,
ops::{Index, IndexMut, Mul},
};
pub struct SparseMatrix<F> {
width: usize,
height: usize,
weights: BTreeMap<(usize, usize), F>,
zero: F,
}
impl<F> SparseMatrix<F> {
pub const fn width(&self) -> usize {
self.width
}
pub const fn height(&self) -> usize {
self.height
}
pub fn pad(&mut self, width: usize, height: usize) {
self.width = self.width.max(width);
self.height = self.height.max(height);
}
}
impl<F> IntoIterator for SparseMatrix<F> {
type Item = ((usize, usize), F);
type IntoIter = <BTreeMap<(usize, usize), F> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.weights.into_iter()
}
}
impl<F: Additive> Default for SparseMatrix<F> {
fn default() -> Self {
Self {
width: 0,
height: 0,
weights: Default::default(),
zero: F::zero(),
}
}
}
impl<F: Additive> Index<(usize, usize)> for SparseMatrix<F> {
type Output = F;
fn index(&self, idx: (usize, usize)) -> &Self::Output {
self.weights.get(&idx).unwrap_or(&self.zero)
}
}
impl<F: Additive> IndexMut<(usize, usize)> for SparseMatrix<F> {
fn index_mut(&mut self, idx: (usize, usize)) -> &mut Self::Output {
self.height = self
.height
.max(idx.0.checked_add(1).expect("row index overflow"));
self.width = self
.width
.max(idx.1.checked_add(1).expect("column index overflow"));
self.weights.entry(idx).or_insert(F::zero())
}
}
impl<F: Ring> Mul<&[F]> for &SparseMatrix<F> {
type Output = Vec<F>;
fn mul(self, rhs: &[F]) -> Self::Output {
let mut out = vec![F::zero(); self.height];
for (&(i, j), weight) in &self.weights {
let Some(value) = rhs.get(j) else {
continue;
};
out[i] += &(weight.clone() * value);
}
out
}
}
impl<F: Write> Write for SparseMatrix<F> {
fn write(&self, buf: &mut impl BufMut) {
self.weights.write(buf);
}
}
impl<F: EncodeSize> EncodeSize for SparseMatrix<F> {
fn encode_size(&self) -> usize {
self.weights.encode_size()
}
}
pub struct Circuit<F> {
committed_vars: usize,
internal_vars: usize,
weights: SparseMatrix<F>,
}
impl<F: Write> Write for Circuit<F> {
fn write(&self, buf: &mut impl BufMut) {
self.committed_vars.write(buf);
self.weights.write(buf);
}
}
impl<F: Encode> Circuit<F> {
fn commit(&self, transcript: &mut Transcript) {
transcript.commit(self.encode());
}
}
impl<F: EncodeSize> EncodeSize for Circuit<F> {
fn encode_size(&self) -> usize {
self.committed_vars.encode_size() + self.weights.encode_size()
}
}
impl<F: Ring> Circuit<F> {
pub fn new(committed_vars: usize, weights: SparseMatrix<F>) -> Option<Self> {
let remaining_vars = weights.width.checked_sub(committed_vars.checked_add(1)?)?;
if remaining_vars % 3 != 0 {
return None;
}
let internal_vars = remaining_vars / 3;
Some(Self {
committed_vars,
internal_vars,
weights,
})
}
pub const fn internal_vars(&self) -> usize {
self.internal_vars
}
pub const fn committed_vars(&self) -> usize {
self.committed_vars
}
#[must_use]
pub fn is_satisfied(
&self,
committed_values: &[F],
left_values: &[F],
right_values: &[F],
) -> bool {
if committed_values.len() != self.committed_vars
|| left_values.len() != self.internal_vars
|| right_values.len() != self.internal_vars
{
return false;
}
let mut output = Vec::with_capacity(1 + self.committed_vars + 3 * self.internal_vars);
output.push(F::one());
output.extend_from_slice(committed_values);
output.extend_from_slice(left_values);
output.extend_from_slice(right_values);
output.extend(
left_values
.iter()
.zip(right_values)
.map(|(l_i, r_i)| l_i.clone() * r_i),
);
let mut res = vec![F::zero(); self.weights.height];
for (&(i, j), w_ij) in &self.weights.weights {
res[i] += &(output[j].clone() * w_ij);
}
let zero = F::zero();
res.iter().all(|r_i| r_i == &zero)
}
}
pub struct R1cs<F> {
pub a: SparseMatrix<F>,
pub b: SparseMatrix<F>,
pub c: SparseMatrix<F>,
}
impl<F> R1cs<F> {
pub fn normalize(&mut self) {
let width = self.width();
let height = self.height();
for m in [&mut self.a, &mut self.b, &mut self.c] {
m.pad(width, height);
}
}
pub fn width(&self) -> usize {
self.a.width.max(self.b.width).max(self.c.width)
}
pub fn height(&self) -> usize {
self.a.height.max(self.b.height).max(self.c.height)
}
}
fn r1cs_to_circuit_maybe_witness<F: Ring>(
mut r1cs: R1cs<F>,
committed_indices: &[usize],
witness: Option<(Vec<F>, Vec<F>)>,
) -> (Circuit<F>, Option<Witness<F>>) {
r1cs.normalize();
let (r1cs_width, r1cs_height) = (r1cs.width(), r1cs.height());
let committed_indices = Set::from_iter_dedup(committed_indices.iter().cloned());
assert!(
committed_indices.iter().next().is_none_or(|&i| i > 0),
"R1CS column 0 (the constant `1`) cannot be a committed value"
);
let v_len = committed_indices.len();
let non_const_width = r1cs_width.saturating_sub(1);
let x_len = non_const_width - v_len;
let old_to_new = {
let mut out = vec![(true, 0usize); non_const_width];
let mut v_i = 0;
let mut x_i = 0;
let mut committed_indices = committed_indices.iter().peekable();
for (j, out_j) in out.iter_mut().enumerate() {
let r1cs_index = j + 1;
let (in_v, counter) = if committed_indices.next_if(|&&k| k == r1cs_index).is_some() {
(true, &mut v_i)
} else {
(false, &mut x_i)
};
*out_j = (in_v, *counter);
*counter += 1;
}
out
};
let internal_len = x_len + r1cs.height();
let witness = witness.map(|(witness, blinding)| {
debug_assert!(
witness.first().is_none_or(|w| w == &F::one()),
"R1CS witness slot 0 must be the constant 1"
);
let mut left = &r1cs.a * witness.as_slice();
let mut right = &r1cs.b * witness.as_slice();
let mut out = &r1cs.c * witness.as_slice();
let mut x = vec![F::zero(); x_len];
let mut values = vec![F::zero(); v_len];
for (i, w_i) in witness.into_iter().enumerate().skip(1) {
let (in_v, i_new) = old_to_new[i - 1];
let array = if in_v { &mut values } else { &mut x };
array[i_new] = w_i;
}
left.extend(x);
right.resize(left.len(), F::zero());
out.resize(left.len(), F::zero());
Witness::new(values, blinding, left, right, out)
.expect("should be able to construct witness")
});
let mut weights = SparseMatrix::default();
weights.pad(1 + v_len + 3 * internal_len, 3 * r1cs.height());
let minus_one = -F::one();
let x_col_base = 1 + v_len + r1cs_height;
for (row_start, col_start, m) in [
(0, 1 + v_len, r1cs.a),
(r1cs_height, 1 + v_len + internal_len, r1cs.b),
(2 * r1cs_height, 1 + v_len + 2 * internal_len, r1cs.c),
] {
for ((i, j), m_ij) in m {
let col = if j == 0 {
0
} else {
let (in_v, j_new) = old_to_new[j - 1];
if in_v {
j_new + 1
} else {
x_col_base + j_new
}
};
weights[(row_start + i, col)] = m_ij;
}
for i in 0..r1cs_height {
weights[(row_start + i, col_start + i)] = minus_one.clone();
}
}
let circuit = Circuit::new(v_len, weights).expect("circuit construction should succeed");
(circuit, witness)
}
pub fn r1cs_to_circuit<F: Ring>(r1cs: R1cs<F>, committed_indices: &[usize]) -> Circuit<F> {
r1cs_to_circuit_maybe_witness(r1cs, committed_indices, None).0
}
pub fn r1cs_to_circuit_and_witness<F: Ring + Random>(
blinding_rng: Option<&mut impl CryptoRngCore>,
r1cs: R1cs<F>,
witness: Vec<F>,
committed_indices: &[usize],
) -> (Circuit<F>, Witness<F>) {
let blinding = blinding_rng.map_or_else(
|| vec![F::zero(); committed_indices.len()],
|rng| {
(0..committed_indices.len())
.map(|_| F::random(&mut *rng))
.collect::<Vec<_>>()
},
);
let (circuit, witness) =
r1cs_to_circuit_maybe_witness(r1cs, committed_indices, Some((witness, blinding)));
let witness = witness.expect("witness should be present because we passed witness");
(circuit, witness)
}
#[derive(PartialEq)]
pub struct Setup<G> {
ipa: ipa::Setup<G>,
value_generator: G,
blinding_generator: G,
}
impl<G> Setup<G> {
pub const fn new(ipa: ipa::Setup<G>, value_generator: G, blinding_generator: G) -> Self {
Self {
ipa,
value_generator,
blinding_generator,
}
}
pub const fn value_generator(&self) -> &G {
&self.value_generator
}
pub const fn blinding_generator(&self) -> &G {
&self.blinding_generator
}
pub const fn supports(&self, lg_len: u8) -> bool {
self.ipa.supports(lg_len)
}
pub fn hashed(domain_separator: &[u8], lg_len: u8, value_generator: G) -> Self
where
G: HashToGroup,
{
let n: usize = 1usize << lg_len;
let product_generator = G::hash_to_group(domain_separator, b"product");
let blinding_generator = G::hash_to_group(domain_separator, b"blinding");
let g_and_h = (0..n).map(|i| {
let i_bytes = (i as u64).to_le_bytes();
let mut g_msg = Vec::with_capacity(2 + i_bytes.len());
g_msg.extend_from_slice(b"g/");
g_msg.extend_from_slice(&i_bytes);
let mut h_msg = Vec::with_capacity(2 + i_bytes.len());
h_msg.extend_from_slice(b"h/");
h_msg.extend_from_slice(&i_bytes);
(
G::hash_to_group(domain_separator, &g_msg),
G::hash_to_group(domain_separator, &h_msg),
)
});
Self::new(
ipa::Setup::new(product_generator, g_and_h),
value_generator,
blinding_generator,
)
}
fn build_virtual<F: Field>(&self) -> (Setup<Synthetic<F, G>>, Vec<G>)
where
G: Clone,
{
let n = self.ipa.g().len();
let mut gens = Synthetic::<F, G>::generators();
let vg: Vec<_> = (0..n)
.map(|_| gens.next().expect("generators is infinite"))
.collect();
let vh: Vec<_> = (0..n)
.map(|_| gens.next().expect("generators is infinite"))
.collect();
let vq = gens.next().expect("generators is infinite");
let ipa_vs = ipa::Setup::new(vq, vg.into_iter().zip(vh));
let pv = gens.next().expect("generators is infinite");
let pb = gens.next().expect("generators is infinite");
let vs = Setup::new(ipa_vs, pv, pb);
let mut flat = Vec::with_capacity(2 * n + 3);
flat.extend_from_slice(self.ipa.g());
flat.extend_from_slice(self.ipa.h());
flat.push(self.ipa.product_generator().clone());
flat.push(self.value_generator.clone());
flat.push(self.blinding_generator.clone());
(vs, flat)
}
pub fn eval<F: Field>(
&self,
f: impl FnOnce(&Setup<Synthetic<F, G>>) -> Option<Synthetic<F, G>>,
strategy: &impl Strategy,
) -> Option<G>
where
G: Space<F>,
{
let (vs, flat) = self.build_virtual::<F>();
f(&vs).map(|v| v.eval(&flat, strategy))
}
pub fn eval_check_batched<F: Field + Random, R: CryptoRngCore>(
&self,
rng: &mut R,
f: impl FnOnce(&Setup<Synthetic<F, G>>, &mut R) -> Option<Vec<Option<Synthetic<F, G>>>>,
strategy: &impl Strategy,
) -> Option<Vec<bool>>
where
G: Space<F> + PartialEq,
{
let (vs, flat) = self.build_virtual::<F>();
let synths = f(&vs, &mut *rng)?;
let n = synths.len();
let scaled: Vec<Option<Synthetic<F, G>>> = synths
.into_iter()
.map(|opt| opt.map(|s| s * &F::random(&mut *rng)))
.collect();
let active: Vec<usize> = (0..n).filter(|&i| scaled[i].is_some()).collect();
let check = |range: &[usize]| -> bool {
let mut acc = Synthetic::<F, G>::default();
for &i in range {
acc += scaled[i].as_ref().expect("active indices are Some");
}
acc.eval(&flat, strategy) == G::zero()
};
let mut valid = vec![false; n];
let mut stack: Vec<&[usize]> = Vec::new();
if !active.is_empty() {
stack.push(&active);
}
while let Some(range) = stack.pop() {
if check(range) {
for &i in range {
valid[i] = true;
}
} else if range.len() > 1 {
let mid = range.len() / 2;
let (left, right) = range.split_at(mid);
stack.push(right);
stack.push(left);
}
}
Some(valid)
}
}
impl<G: Write> Write for Setup<G> {
fn write(&self, buf: &mut impl BufMut) {
self.ipa.write(buf);
self.value_generator.write(buf);
self.blinding_generator.write(buf);
}
}
impl<G: EncodeSize> EncodeSize for Setup<G> {
fn encode_size(&self) -> usize {
self.ipa.encode_size()
+ self.value_generator.encode_size()
+ self.blinding_generator.encode_size()
}
}
impl<G: Read> Read for Setup<G>
where
G::Cfg: Clone,
{
type Cfg = (usize, G::Cfg);
fn read_cfg(buf: &mut impl Buf, (max_len, cfg): &Self::Cfg) -> Result<Self, Error> {
let ipa = ipa::Setup::read_cfg(buf, &(*max_len, cfg.clone()))?;
let value_generator = G::read_cfg(buf, cfg)?;
let blinding_generator = G::read_cfg(buf, cfg)?;
Ok(Self::new(ipa, value_generator, blinding_generator))
}
}
#[allow(dead_code)]
pub struct Witness<F> {
values: Vec<F>,
blinding: Vec<F>,
left: Vec<F>,
right: Vec<F>,
out: Vec<F>,
}
impl<F> Witness<F> {
pub fn new(
values: Vec<F>,
blinding: Vec<F>,
left: Vec<F>,
right: Vec<F>,
out: Vec<F>,
) -> Option<Self> {
if values.len() != blinding.len() {
return None;
}
if left.len() != right.len() || right.len() != out.len() {
return None;
}
Some(Self {
values,
blinding,
left,
right,
out,
})
}
pub fn values(&self) -> &[F] {
&self.values
}
#[must_use]
pub fn is_satisfied(&self, circuit: &Circuit<F>) -> bool
where
F: Ring,
{
circuit.is_satisfied(&self.values, &self.left, &self.right)
}
pub fn claim<G: Space<F>>(&self, setup: &Setup<G>) -> Claim<G> {
Claim {
commitments: self
.values
.iter()
.zip(&self.blinding)
.map(|(value, blind)| {
setup.value_generator.clone() * value
+ &(setup.blinding_generator.clone() * blind)
})
.collect(),
}
}
}
pub struct Claim<G> {
pub commitments: Vec<G>,
}
impl<G: Write> Write for Claim<G> {
fn write(&self, buf: &mut impl BufMut) {
self.commitments.write(buf);
}
}
impl<G: EncodeSize> EncodeSize for Claim<G> {
fn encode_size(&self) -> usize {
self.commitments.encode_size()
}
}
#[allow(dead_code)]
#[derive(Clone)]
pub struct Proof<F, G> {
m_big: G,
o_big: G,
m_big_tilde: G,
t_big: [G; 5],
s_tilde: F,
t_x: F,
t_tilde_x: F,
p_big: G,
ipa_proof: ipa::Proof<F, G>,
}
impl<F: Write, G: Write> Write for Proof<F, G> {
fn write(&self, buf: &mut impl BufMut) {
self.m_big.write(buf);
self.o_big.write(buf);
self.m_big_tilde.write(buf);
for t in &self.t_big {
t.write(buf);
}
self.s_tilde.write(buf);
self.t_x.write(buf);
self.t_tilde_x.write(buf);
self.p_big.write(buf);
self.ipa_proof.write(buf);
}
}
impl<F: EncodeSize, G: EncodeSize> EncodeSize for Proof<F, G> {
fn encode_size(&self) -> usize {
self.m_big.encode_size()
+ self.o_big.encode_size()
+ self.m_big_tilde.encode_size()
+ self.t_big.iter().map(|t| t.encode_size()).sum::<usize>()
+ self.s_tilde.encode_size()
+ self.t_x.encode_size()
+ self.t_tilde_x.encode_size()
+ self.p_big.encode_size()
+ self.ipa_proof.encode_size()
}
}
impl<F: Read, G: Read> Read for Proof<F, G>
where
F::Cfg: Clone,
G::Cfg: Clone,
{
type Cfg = (usize, (G::Cfg, F::Cfg));
fn read_cfg(buf: &mut impl Buf, cfg @ (_, (g_cfg, f_cfg)): &Self::Cfg) -> Result<Self, Error> {
let m_big = G::read_cfg(buf, g_cfg)?;
let o_big = G::read_cfg(buf, g_cfg)?;
let m_big_tilde = G::read_cfg(buf, g_cfg)?;
let t_big = [
G::read_cfg(buf, g_cfg)?,
G::read_cfg(buf, g_cfg)?,
G::read_cfg(buf, g_cfg)?,
G::read_cfg(buf, g_cfg)?,
G::read_cfg(buf, g_cfg)?,
];
let s_tilde = F::read_cfg(buf, f_cfg)?;
let t_x = F::read_cfg(buf, f_cfg)?;
let t_tilde_x = F::read_cfg(buf, f_cfg)?;
let p_big = G::read_cfg(buf, g_cfg)?;
let ipa_proof = ipa::Proof::read_cfg(buf, cfg)?;
Ok(Self {
m_big,
o_big,
m_big_tilde,
t_big,
s_tilde,
t_x,
t_tilde_x,
p_big,
ipa_proof,
})
}
}
pub fn prove<F: Field + Encode + Random, G: CryptoGroup<Scalar = F> + Encode>(
rng: &mut impl CryptoRngCore,
transcript: &mut Transcript,
setup: &Setup<G>,
circuit: &Circuit<F>,
claim: &Claim<G>,
witness: &Witness<F>,
strategy: &impl Strategy,
) -> Option<Proof<F, G>> {
let l_tilde = (0..circuit.internal_vars)
.map(|_| F::random(&mut *rng))
.collect::<Vec<_>>();
let r_tilde = (0..circuit.internal_vars)
.map(|_| F::random(&mut *rng))
.collect::<Vec<_>>();
let m = F::random(&mut *rng);
let o_tilde = F::random(&mut *rng);
let m_tilde = F::random(&mut *rng);
let g_internal = &setup.ipa.g()[..circuit.internal_vars];
let h_internal = &setup.ipa.h()[..circuit.internal_vars];
let m_big = G::msm(g_internal, &witness.left, strategy)
+ &G::msm(h_internal, &witness.right, strategy)
+ &(setup.blinding_generator.clone() * &m);
let o_big =
G::msm(g_internal, &witness.out, strategy) + &(setup.blinding_generator.clone() * &o_tilde);
let m_big_tilde = G::msm(g_internal, &l_tilde, strategy)
+ &G::msm(h_internal, &r_tilde, strategy)
+ &(setup.blinding_generator.clone() * &m_tilde);
circuit.commit(transcript);
transcript.commit(claim.encode());
transcript.commit(m_big.encode());
transcript.commit(o_big.encode());
transcript.commit(m_big_tilde.encode());
let padded_vars = circuit.internal_vars.next_power_of_two();
let y = F::random(transcript.noise(b"y"));
let y_powers = powers(F::one(), &y).take(padded_vars).collect::<Vec<_>>();
let y_inv = y.inv();
let y_inv_powers = powers(F::one(), &y_inv)
.take(padded_vars)
.collect::<Vec<_>>();
let z = F::random(transcript.noise(b"z"));
let z_powers = powers(z.clone(), &z)
.take(circuit.weights.height())
.collect::<Vec<_>>();
let (kappa, theta, lambda, rho, omega) = {
let mut kappa = F::zero();
let mut theta = vec![F::zero(); circuit.committed_vars];
let mut lambda = vec![F::zero(); circuit.internal_vars];
let mut rho = vec![F::zero(); circuit.internal_vars];
let mut omega = vec![F::zero(); circuit.internal_vars];
let theta_start = 1;
let lambda_start = theta_start + circuit.committed_vars;
let rho_start = lambda_start + circuit.internal_vars;
let omega_start = rho_start + circuit.internal_vars;
for (&(i, j), w_ij) in &circuit.weights.weights {
let w_ij = w_ij.clone();
if j >= omega_start {
omega[j - omega_start] += &(w_ij * &z_powers[i]);
} else if j >= rho_start {
rho[j - rho_start] += &(w_ij * &z_powers[i]);
} else if j >= lambda_start {
lambda[j - lambda_start] += &(w_ij * &z_powers[i]);
} else if j >= theta_start {
theta[j - theta_start] += &(w_ij * &z_powers[i]);
} else {
kappa += &(w_ij * &z_powers[i]);
}
}
(kappa, theta, lambda, rho, omega)
};
let mut omega_minus_y = omega
.iter()
.cloned()
.zip(&y_powers)
.map(|(omega_i, y_i)| omega_i - y_i)
.collect::<Vec<_>>();
omega_minus_y.extend(
y_powers
.iter()
.skip(circuit.internal_vars)
.cloned()
.map(|y_i| -y_i),
);
let y_inv_rho = y_inv_powers
.iter()
.cloned()
.zip(&rho)
.map(|(y_inv_i, rho_i)| y_inv_i * rho_i)
.collect::<Vec<_>>();
let y_inv_lambda = y_inv_powers
.iter()
.cloned()
.zip(&lambda)
.map(|(y_inv_i, lambda_i)| y_inv_i * lambda_i)
.collect::<Vec<_>>();
let y_inv_omega_minus_y = y_inv_powers
.iter()
.cloned()
.zip(&omega_minus_y)
.map(|(y_inv_i, omega_minus_y_i)| y_inv_i * omega_minus_y_i)
.collect::<Vec<_>>();
let y_r = y_powers
.iter()
.cloned()
.zip(&witness.right)
.map(|(y_i, r_i)| y_i * r_i)
.collect::<Vec<_>>();
let y_r_tilde = y_powers
.iter()
.cloned()
.zip(&r_tilde)
.map(|(y_i, r_i)| y_i * r_i)
.collect::<Vec<_>>();
let delta_y_z = <F as Space<F>>::msm(&y_inv_rho, &lambda, strategy);
let t = {
let mut t = std::array::from_fn::<_, 6, _>(|_| F::zero());
for i in 0..circuit.internal_vars {
t[0] += &((witness.left[i].clone() + &y_inv_rho[i]) * &omega_minus_y[i]);
}
t[1] = delta_y_z - &kappa - &<F as Space<F>>::msm(&theta, &witness.values, strategy);
for i in 0..circuit.internal_vars {
t[2] += &(l_tilde[i].clone() * &omega_minus_y[i]);
t[2] += &(witness.out[i].clone() * &(y_r[i].clone() + &lambda[i]));
}
for i in 0..circuit.internal_vars {
t[3] += &(l_tilde[i].clone() * &(y_r[i].clone() + &lambda[i]));
t[3] += &((witness.left[i].clone() + &y_inv_rho[i]) * &y_r_tilde[i]);
}
t[4] = <F as Space<F>>::msm(&witness.out, &y_r_tilde, strategy);
t[5] = <F as Space<F>>::msm(&l_tilde, &y_r_tilde, strategy);
t
};
let t_tilde = std::array::from_fn::<_, 6, _>(|i| {
if i == 1 {
-<F as Space<F>>::msm(&theta, &witness.blinding, strategy)
} else {
F::random(&mut *rng)
}
});
let t_big = std::array::from_fn::<_, 5, _>(|i| {
let i = if i >= 1 { i + 1 } else { i };
setup.value_generator.clone() * &t[i] + &(setup.blinding_generator.clone() * &t_tilde[i])
});
let p_0 = G::msm(
&setup.ipa.h()[..padded_vars],
&y_inv_omega_minus_y,
strategy,
);
let h_internal = &setup.ipa.h()[..circuit.internal_vars];
let p_1 =
G::msm(g_internal, &y_inv_rho, strategy) + &G::msm(h_internal, &y_inv_lambda, strategy);
for t_big_i in &t_big {
transcript.commit(t_big_i.encode());
}
let x = F::random(transcript.noise(b"x"));
let x = powers(x.clone(), &x).take(6).collect::<Vec<_>>();
let s_tilde = m * &x[0] + &(o_tilde * &x[1]) + &(m_tilde * &x[2]);
let p = setup.blinding_generator.clone() * &(-s_tilde.clone())
+ &p_0
+ &((p_1 + &m_big) * &x[0])
+ &(o_big.clone() * &x[1])
+ &(m_big_tilde.clone() * &x[2]);
let t_x = <F as Space<F>>::msm(&t, &x, strategy);
let t_tilde_x = <F as Space<F>>::msm(&t_tilde, &x, strategy);
let ipa_claim = ipa::Claim {
commitment: p.clone(),
product: t_x.clone(),
y: y_inv,
log_len: padded_vars.ilog2().try_into().ok()?,
};
let mut f_x = (0..circuit.internal_vars)
.map(|i| {
(witness.left[i].clone() + &y_inv_rho[i]) * &x[0]
+ &(witness.out[i].clone() * &x[1])
+ &(l_tilde[i].clone() * &x[2])
})
.collect::<Vec<_>>();
f_x.resize(padded_vars, F::zero());
let mut g_x = (0..circuit.internal_vars)
.map(|i| {
(y_r[i].clone() + &lambda[i]) * &x[0]
+ &omega_minus_y[i]
+ &(y_r_tilde[i].clone() * &x[2])
})
.collect::<Vec<_>>();
g_x.extend_from_slice(&omega_minus_y[circuit.internal_vars..]);
let witness = ipa::Witness::new(f_x.into_iter().zip(g_x))?;
let ipa_proof = ipa::prove(transcript, &setup.ipa, &ipa_claim, witness, strategy)?;
Some(Proof {
m_big,
o_big,
m_big_tilde,
t_big,
s_tilde,
t_x,
t_tilde_x,
p_big: p,
ipa_proof,
})
}
pub fn verify<F: Field + Encode + Random, G: CryptoGroup<Scalar = F> + Encode>(
rng: &mut impl CryptoRngCore,
transcript: &mut Transcript,
setup: &Setup<Synthetic<F, G>>,
circuit: &Circuit<F>,
claim: &Claim<G>,
proof: Proof<F, G>,
strategy: &impl Strategy,
) -> Option<Synthetic<F, G>> {
let Proof {
m_big,
o_big,
m_big_tilde,
t_big,
s_tilde,
t_x,
t_tilde_x,
ipa_proof,
p_big: p,
} = proof;
if claim.commitments.len() != circuit.committed_vars {
return None;
}
circuit.commit(transcript);
transcript.commit(claim.encode());
transcript.commit(m_big.encode());
transcript.commit(o_big.encode());
transcript.commit(m_big_tilde.encode());
let padded_vars = circuit.internal_vars.next_power_of_two();
let y = F::random(transcript.noise(b"y"));
let y_powers = powers(F::one(), &y).take(padded_vars).collect::<Vec<_>>();
let y_inv = y.inv();
let y_inv_powers = powers(F::one(), &y_inv)
.take(padded_vars)
.collect::<Vec<_>>();
let z = F::random(transcript.noise(b"z"));
let z_powers = powers(z.clone(), &z)
.take(circuit.weights.height())
.collect::<Vec<_>>();
let (kappa, theta, lambda, rho, omega) = {
let mut kappa = F::zero();
let mut theta = vec![F::zero(); circuit.committed_vars];
let mut lambda = vec![F::zero(); circuit.internal_vars];
let mut rho = vec![F::zero(); circuit.internal_vars];
let mut omega = vec![F::zero(); circuit.internal_vars];
let theta_start = 1;
let lambda_start = theta_start + circuit.committed_vars;
let rho_start = lambda_start + circuit.internal_vars;
let omega_start = rho_start + circuit.internal_vars;
for (&(i, j), w_ij) in &circuit.weights.weights {
let w_ij = w_ij.clone();
if j >= omega_start {
omega[j - omega_start] += &(w_ij * &z_powers[i]);
} else if j >= rho_start {
rho[j - rho_start] += &(w_ij * &z_powers[i]);
} else if j >= lambda_start {
lambda[j - lambda_start] += &(w_ij * &z_powers[i]);
} else if j >= theta_start {
theta[j - theta_start] += &(w_ij * &z_powers[i]);
} else {
kappa += &(w_ij * &z_powers[i]);
}
}
(kappa, theta, lambda, rho, omega)
};
let mut omega_minus_y = omega
.iter()
.cloned()
.zip(&y_powers)
.map(|(omega_i, y_i)| omega_i - y_i)
.collect::<Vec<_>>();
omega_minus_y.extend(
y_powers
.iter()
.skip(circuit.internal_vars)
.cloned()
.map(|y_i| -y_i),
);
let y_inv_rho = y_inv_powers
.iter()
.cloned()
.zip(&rho)
.map(|(y_inv_i, rho_i)| y_inv_i * rho_i)
.collect::<Vec<_>>();
let y_inv_lambda = y_inv_powers
.iter()
.cloned()
.zip(&lambda)
.map(|(y_inv_i, lambda_i)| y_inv_i * lambda_i)
.collect::<Vec<_>>();
let y_inv_omega_minus_y = y_inv_powers
.iter()
.cloned()
.zip(&omega_minus_y)
.map(|(y_inv_i, omega_minus_y_i)| y_inv_i * omega_minus_y_i)
.collect::<Vec<_>>();
let delta_y_z = <F as Space<F>>::msm(&y_inv_rho, &lambda, strategy);
for t_big_i in &t_big {
transcript.commit(t_big_i.encode());
}
let x = F::random(transcript.noise(b"x"));
let x = powers(x.clone(), &x).take(6).collect::<Vec<_>>();
let ipa_g = setup.ipa.g();
let ipa_h = setup.ipa.h();
let value_generator = &setup.value_generator;
let blinding_generator = &setup.blinding_generator;
let t_check = Synthetic::msm(
&[value_generator.clone(), blinding_generator.clone()],
&[t_x.clone(), t_tilde_x],
&Sequential,
) - &(value_generator.clone() * &((-kappa + &delta_y_z) * &x[1]))
+ &(Synthetic::concrete(theta.iter().cloned().zip(claim.commitments.iter().cloned()))
* &x[1])
- &Synthetic::concrete(std::iter::once(&x[0]).chain(&x[2..]).cloned().zip(t_big));
let p_check = {
let p_0 = Synthetic::msm(&ipa_h[..padded_vars], &y_inv_omega_minus_y, &Sequential);
let p_1 = Synthetic::msm(&ipa_g[..circuit.internal_vars], &y_inv_rho, &Sequential)
+ &Synthetic::msm(&ipa_h[..circuit.internal_vars], &y_inv_lambda, &Sequential);
Synthetic::concrete([
(F::one(), p.clone()),
(-x[0].clone(), m_big),
(-x[1].clone(), o_big),
(-x[2].clone(), m_big_tilde),
]) - &p_0
- &(p_1 * &x[0])
+ &(blinding_generator.clone() * &s_tilde)
};
let ipa_claim = ipa::Claim {
commitment: p,
product: t_x,
y: y_inv,
log_len: padded_vars
.ilog2()
.try_into()
.expect("should be less than 2^256 rows"),
};
let ipa_check = ipa::verify(transcript, &setup.ipa, &ipa_claim, ipa_proof)?;
let final_check =
ipa_check + &(p_check * &F::random(&mut *rng)) + &(t_check * &F::random(&mut *rng));
Some(final_check)
}
#[commonware_macros::stability(ALPHA)]
#[cfg(any(test, feature = "fuzz"))]
pub mod fuzz {
use super::*;
use arbitrary::{Arbitrary, Unstructured};
use commonware_math::{
algebra::{Additive, Ring},
test::{F, G},
};
use commonware_parallel::Sequential;
use commonware_utils::test_rng;
use std::sync::OnceLock;
const NUM_GENERATORS: usize = 5;
const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_ZK_BULLETPROOFS_CIRCUIT";
pub(super) fn test_setup() -> &'static Setup<G> {
static TEST_SETUP: OnceLock<Setup<G>> = OnceLock::new();
TEST_SETUP.get_or_init(|| {
let generators = (1..=NUM_GENERATORS)
.map(|i| G::generator() * &F::from(i as u8))
.collect::<Vec<_>>();
Setup::new(
ipa::Setup::new(
generators[0],
generators[1..3]
.chunks_exact(2)
.map(|chunk| (chunk[0], chunk[1])),
),
generators[3],
generators[4],
)
})
}
fn quadratic_value(a: F, b: F, c: F, x: F) -> F {
a * &x * &x + &(b * &x) + &c
}
pub(super) fn quadratic_circuit(a: F, b: F, c: F) -> Circuit<F> {
let mut weights = SparseMatrix::default();
weights[(0, 1)] = F::one();
weights[(0, 3)] = -F::one();
weights[(1, 1)] = F::one();
weights[(1, 4)] = -F::one();
weights[(2, 0)] = c;
weights[(2, 1)] = b;
weights[(2, 2)] = -F::one();
weights[(2, 5)] = a;
Circuit::new(2, weights).expect("quadratic circuit layout should be valid")
}
struct Case {
circuit: Circuit<F>,
witness: Witness<F>,
}
impl Case {
fn prove(&self, setup: &Setup<G>) -> (Claim<G>, Proof<F, G>) {
let mut rng = test_rng();
let mut transcript = Transcript::new(NAMESPACE);
let claim = self.witness.claim(setup);
let proof = super::prove(
&mut rng,
&mut transcript,
setup,
&self.circuit,
&claim,
&self.witness,
&Sequential,
)
.expect("generated case should always create a proof");
(claim, proof)
}
fn is_satisfied(&self) -> bool {
self.circuit.is_satisfied(
&self.witness.values,
&self.witness.left,
&self.witness.right,
)
}
fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
let a = u.arbitrary::<F>()?;
let b = u.arbitrary::<F>()?;
let c = u.arbitrary::<F>()?;
let x = u.arbitrary::<F>()?;
let valid = u.arbitrary::<bool>()?;
let mut y = quadratic_value(a, b, c, x);
if !valid {
let mut tweak = u.arbitrary::<F>()?;
if tweak == F::zero() {
tweak = F::one()
}
y += &tweak;
}
let x_sq = x * &x;
let witness = Witness::new(
vec![x, y],
vec![u.arbitrary::<F>()?, u.arbitrary::<F>()?],
vec![x],
vec![x],
vec![x_sq],
)
.expect("quadratic witness should have matching vector lengths");
let circuit = quadratic_circuit(a, b, c);
let out = Self { circuit, witness };
assert_eq!(
out.is_satisfied(),
valid,
"quadratic case should match requested validity",
);
Ok(out)
}
}
pub struct Plan {
case: Case,
}
impl<'a> Arbitrary<'a> for Plan {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Self {
case: Case::arbitrary(u)?,
})
}
}
fn assert_verify_matches_satisfaction(case: &Case) {
let mut rng = test_rng();
let setup = test_setup();
let (claim, proof) = case.prove(setup);
let mut transcript = Transcript::new(NAMESPACE);
let verified = setup
.eval(
|vs| {
verify(
&mut rng,
&mut transcript,
vs,
&case.circuit,
&claim,
proof,
&Sequential,
)
},
&Sequential,
)
.map(|g| g == G::zero())
.unwrap_or(false);
assert_eq!(verified, case.is_satisfied());
}
impl Plan {
pub fn run(self, _u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
assert_verify_matches_satisfaction(&self.case);
Ok(())
}
}
}
#[cfg(test)]
mod test {
use super::{
fuzz, prove, r1cs_to_circuit, verify, Circuit, R1cs, Setup, SparseMatrix, Witness,
};
use crate::transcript::Transcript;
use commonware_codec::{Decode, Encode};
use commonware_invariants::minifuzz;
use commonware_math::{
algebra::{Additive, CryptoGroup, Ring},
test::{F, G},
};
use commonware_parallel::Sequential;
use commonware_utils::test_rng;
#[test]
fn test_random_r1cs_minifuzz() {
const N: usize = 2;
const M: usize = 4;
minifuzz::test(|u| {
let a = u.arbitrary::<[[F; N]; M]>()?;
let b = u.arbitrary::<[[F; N]; M]>()?;
let c = u.arbitrary::<[[F; N]; M]>()?;
let z = u.arbitrary::<[F; N]>()?;
let mut left = [F::zero(); M];
let mut right = [F::zero(); M];
let mut satisfied = true;
for i in 0..M {
let mut acc = F::zero();
for j in 0..N {
left[i] += &(a[i][j] * &z[j]);
right[i] += &(b[i][j] * &z[j]);
acc += &(c[i][j] * &z[j]);
}
satisfied = satisfied && acc == left[i] * &right[i];
}
let mut k = 0;
let mut weights = SparseMatrix::default();
for i in 0..M {
weights[(k, 1 + N + i)] = -F::one();
for j in 0..N {
weights[(k, 1 + j)] = a[i][j];
}
k += 1;
}
for i in 0..M {
weights[(k, 1 + N + M + i)] = -F::one();
for j in 0..N {
weights[(k, 1 + j)] = b[i][j];
}
k += 1;
}
for i in 0..M {
weights[(k, 1 + N + 2 * M + i)] = -F::one();
for j in 0..N {
weights[(k, 1 + j)] = c[i][j];
}
k += 1;
}
assert_eq!(
satisfied,
Circuit::new(N, weights)
.expect("should be able to make circuit")
.is_satisfied(&z, &left, &right)
);
Ok(())
});
}
#[test]
fn test_setup_roundtrip() {
let setup = fuzz::test_setup();
let encoded = setup.encode();
let decoded: Setup<G> = Setup::decode_cfg(encoded.clone(), &(setup.ipa.g().len(), ()))
.expect("setup should decode with its own length bound");
assert!(setup == &decoded);
assert_eq!(decoded.encode(), encoded);
}
#[test]
fn test_fuzz() {
minifuzz::test(|u| {
u.arbitrary::<fuzz::Plan>()?.run(u)?;
Ok(())
});
}
#[test]
fn r1cs_to_circuit_enforces_constant_column() {
let mut a = SparseMatrix::<F>::default();
a[(0, 0)] = F::one();
a[(0, 1)] = F::one();
let mut b = SparseMatrix::<F>::default();
b[(0, 0)] = F::one();
let mut c = SparseMatrix::<F>::default();
c[(0, 0)] = F::from(4u8);
let r1cs = R1cs { a, b, c };
let circuit = r1cs_to_circuit(r1cs, &[1usize]);
let malicious = Witness::new(
vec![F::zero()], vec![F::zero()], vec![F::zero()], vec![F::zero()], vec![F::zero()], )
.expect("witness lengths must match the circuit");
assert!(
!malicious.is_satisfied(&circuit),
"circuit derived from an R1CS that depends on the constant \
column must reject witnesses corresponding to z_0 != 1"
);
}
#[test]
fn verify_rejects_over_long_claim() {
let setup = fuzz::test_setup();
let a = F::one();
let b = F::one();
let c = F::one();
let x = F::from(3u8);
let y = a * &x * &x + &(b * &x) + &c;
let circuit = fuzz::quadratic_circuit(a, b, c);
let witness = Witness::new(
vec![x, y],
vec![F::zero(), F::zero()],
vec![x],
vec![x],
vec![x * &x],
)
.expect("witness vector lengths must be consistent");
let mut claim = witness.claim(setup);
claim.commitments.push(G::generator() * &F::from(9u8));
let mut rng = test_rng();
let mut prover_transcript = Transcript::new(b"verify-rejects-over-long-claim");
let proof = prove(
&mut rng,
&mut prover_transcript,
setup,
&circuit,
&claim,
&witness,
&Sequential,
)
.expect("prove still produces a proof against the malformed claim");
let mut verifier_transcript = Transcript::new(b"verify-rejects-over-long-claim");
let verified = setup.eval(
|vs| {
verify(
&mut rng,
&mut verifier_transcript,
vs,
&circuit,
&claim,
proof,
&Sequential,
)
},
&Sequential,
);
assert!(
verified.is_none(),
"verify must reject a claim whose commitment arity does not match the circuit"
);
}
}