use crate::transcript::{Summary, Transcript};
use bytes::{Buf, BufMut};
use commonware_codec::{Encode, EncodeSize, Error, RangeCfg, Read, ReadExt, Write};
use commonware_math::{
algebra::{powers, CryptoGroup, Field, Random, Space},
synthetic::Synthetic,
};
use commonware_parallel::{Sequential, Strategy};
#[derive(Debug, PartialEq)]
pub struct Setup<G> {
g: Vec<G>,
h: Vec<G>,
product_generator: G,
}
impl<G: Write> Write for Setup<G> {
fn write(&self, buf: &mut impl BufMut) {
self.product_generator.write(buf);
self.g.len().write(buf);
for (g_i, h_i) in self.g.iter().zip(&self.h) {
g_i.write(buf);
h_i.write(buf);
}
}
}
impl<G: EncodeSize> EncodeSize for Setup<G> {
fn encode_size(&self) -> usize {
self.product_generator.encode_size()
+ self.g.len().encode_size()
+ self
.g
.iter()
.zip(&self.h)
.map(|(g_i, h_i)| g_i.encode_size() + h_i.encode_size())
.sum::<usize>()
}
}
impl<G: Read> Read for Setup<G> {
type Cfg = (usize, G::Cfg);
fn read_cfg(buf: &mut impl Buf, (max_len, cfg): &Self::Cfg) -> Result<Self, Error> {
let product_generator = G::read_cfg(buf, cfg)?;
let len = usize::read_cfg(buf, &RangeCfg::new(..=*max_len))?;
let mut g = Vec::with_capacity(len);
let mut h = Vec::with_capacity(len);
for _ in 0..len {
g.push(G::read_cfg(buf, cfg)?);
h.push(G::read_cfg(buf, cfg)?);
}
Ok(Self {
g,
h,
product_generator,
})
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl<G> arbitrary::Arbitrary<'_> for Setup<G>
where
G: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let g_and_h = u.arbitrary::<Vec<(G, G)>>()?;
Ok(Self::new(u.arbitrary()?, g_and_h))
}
}
impl<G> Setup<G> {
pub fn new(product_generator: G, g_and_h: impl IntoIterator<Item = (G, G)>) -> Self {
let (g, h): (Vec<G>, Vec<G>) = g_and_h.into_iter().collect();
Self {
g,
h,
product_generator,
}
}
pub fn g(&self) -> &[G] {
&self.g
}
pub fn h(&self) -> &[G] {
&self.h
}
pub const fn product_generator(&self) -> &G {
&self.product_generator
}
pub const fn supports(&self, lg_len: u8) -> bool {
self.g.len() >> lg_len > 0
}
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 n = self.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 vs = Setup::new(vq, vg.into_iter().zip(vh));
let mut flat = Vec::with_capacity(2 * n + 1);
flat.extend_from_slice(&self.g);
flat.extend_from_slice(&self.h);
flat.push(self.product_generator.clone());
f(&vs).map(|v| v.eval(&flat, strategy))
}
}
#[derive(Debug, PartialEq)]
pub struct Claim<F, G> {
pub commitment: G,
pub product: F,
pub y: F,
pub log_len: u8,
}
impl<F: Write, G: Write> Write for Claim<F, G> {
fn write(&self, buf: &mut impl BufMut) {
self.commitment.write(buf);
self.product.write(buf);
self.y.write(buf);
self.log_len.write(buf);
}
}
impl<F: EncodeSize, G: EncodeSize> EncodeSize for Claim<F, G> {
fn encode_size(&self) -> usize {
self.commitment.encode_size()
+ self.product.encode_size()
+ self.y.encode_size()
+ self.log_len.encode_size()
}
}
impl<F: Read, G: Read> Read for Claim<F, G> {
type Cfg = (G::Cfg, F::Cfg);
fn read_cfg(buf: &mut impl Buf, (g_cfg, f_cfg): &Self::Cfg) -> Result<Self, Error> {
Ok(Self {
commitment: G::read_cfg(buf, g_cfg)?,
product: F::read_cfg(buf, f_cfg)?,
y: F::read_cfg(buf, f_cfg)?,
log_len: u8::read(buf)?,
})
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl<F, G> arbitrary::Arbitrary<'_> for Claim<F, G>
where
F: for<'a> arbitrary::Arbitrary<'a>,
G: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self {
commitment: u.arbitrary()?,
product: u.arbitrary()?,
y: u.arbitrary()?,
log_len: u.arbitrary()?,
})
}
}
#[derive(Clone)]
pub struct Witness<F> {
a: Vec<F>,
b: Vec<F>,
}
impl<F> Witness<F> {
pub fn new(elements: impl IntoIterator<Item = (F, F)>) -> Option<Self> {
let (a, b): (Vec<F>, Vec<F>) = elements.into_iter().collect();
if !a.len().is_power_of_two() {
return None;
}
Some(Self { a, b })
}
}
impl<F: Field> Witness<F> {
pub fn new_with_claim<G: Space<F>>(
setup: &Setup<G>,
y: F,
elements: impl IntoIterator<Item = (F, F)>,
) -> Option<(Self, Claim<F, G>)> {
let witness = Self::new(elements)?;
if setup.g.len() < witness.a.len() {
return None;
}
let claim = {
let mut commitment = G::zero();
let mut product = F::zero();
for ((((a_i, b_i), g_i), h_i), y_i) in witness
.a
.iter()
.zip(&witness.b)
.zip(&setup.g)
.zip(&setup.h)
.zip(powers(F::one(), &y))
{
commitment += &(g_i.clone() * a_i + &(h_i.clone() * &(b_i.clone() * &y_i)));
product += &(a_i.clone() * b_i);
}
Claim {
commitment,
product,
y,
log_len: witness.a.len().ilog2() as u8,
}
};
Some((witness, claim))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Proof<F, G> {
l_r_coms: Vec<(G, G)>,
transcript_summary: Summary,
a_final: F,
b_final: F,
}
impl<F: Write, G: Write> Write for Proof<F, G> {
fn write(&self, buf: &mut impl BufMut) {
self.l_r_coms.write(buf);
self.transcript_summary.write(buf);
self.a_final.write(buf);
self.b_final.write(buf);
}
}
impl<F: EncodeSize, G: EncodeSize> EncodeSize for Proof<F, G> {
fn encode_size(&self) -> usize {
self.l_r_coms.encode_size()
+ self.transcript_summary.encode_size()
+ self.a_final.encode_size()
+ self.b_final.encode_size()
}
}
impl<F: Read, G: Read> Read for Proof<F, G> {
type Cfg = (usize, (G::Cfg, F::Cfg));
fn read_cfg(buf: &mut impl Buf, (max_len, (g_cfg, f_cfg)): &Self::Cfg) -> Result<Self, Error> {
let max_rounds = if *max_len == 0 {
0
} else {
max_len.ilog2() as usize
};
Ok(Self {
l_r_coms: Vec::<(G, G)>::read_cfg(
buf,
&(RangeCfg::new(..=max_rounds), (g_cfg.clone(), g_cfg.clone())),
)?,
transcript_summary: Summary::read(buf)?,
a_final: F::read_cfg(buf, f_cfg)?,
b_final: F::read_cfg(buf, f_cfg)?,
})
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl<F, G> arbitrary::Arbitrary<'_> for Proof<F, G>
where
F: for<'a> arbitrary::Arbitrary<'a>,
G: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let rounds = u.int_in_range(0..=usize::BITS as usize - 1)?;
let l_r_coms = (0..rounds)
.map(|_| u.arbitrary())
.collect::<arbitrary::Result<Vec<_>>>()?;
Ok(Self {
l_r_coms,
transcript_summary: u.arbitrary()?,
a_final: u.arbitrary()?,
b_final: u.arbitrary()?,
})
}
}
pub fn prove<F: Field + Random, G: CryptoGroup<Scalar = F> + Encode>(
transcript: &mut Transcript,
setup: &Setup<G>,
claim: &Claim<F, G>,
witness: Witness<F>,
strategy: &impl Strategy,
) -> Option<Proof<F, G>>
where
Claim<F, G>: Encode,
{
let witness_len = witness.a.len();
let claimed_len = 1usize.checked_shl(u32::from(claim.log_len))?;
if claimed_len != witness_len || setup.g.len() < witness_len {
return None;
}
transcript.commit(claim.encode());
let w = F::random(&mut transcript.noise(b"w challenge"));
let w_q = setup.product_generator.clone() * &w;
let mut l_r_coms = Vec::<(G, G)>::new();
let mut a = witness.a;
let mut b = witness.b;
let mut g = setup.g[..witness_len].to_vec();
let mut h = setup.h[..witness_len].to_vec();
for (h_i, y_i) in h.iter_mut().zip(powers(F::one(), &claim.y)) {
*h_i *= &y_i;
}
while a.len() > 1 {
let mid = a.len() / 2;
let (a_lo, a_hi) = a.split_at_mut(mid);
let (b_lo, b_hi) = b.split_at_mut(mid);
let (g_lo, g_hi) = g.split_at_mut(mid);
let (h_lo, h_hi) = h.split_at_mut(mid);
let l = G::msm(g_hi, a_lo, strategy)
+ &G::msm(h_lo, b_hi, strategy)
+ &(w_q.clone() * &F::msm(a_lo, b_hi, strategy));
let r = G::msm(g_lo, a_hi, strategy)
+ &G::msm(h_hi, b_lo, strategy)
+ &(w_q.clone() * &F::msm(a_hi, b_lo, strategy));
l_r_coms.push((l.clone(), r.clone()));
transcript.commit(l.encode());
transcript.commit(r.encode());
let u = F::random(&mut transcript.noise(b"u challenge"));
let u_inv = u.inv();
for (a_lo_i, a_hi_i) in a_lo.iter_mut().zip(a_hi.iter_mut()) {
*a_lo_i *= &u;
*a_lo_i += &(u_inv.clone() * a_hi_i);
}
a.truncate(mid);
for (b_lo_i, b_hi_i) in b_lo.iter_mut().zip(b_hi.iter_mut()) {
*b_lo_i *= &u_inv;
*b_lo_i += &(u.clone() * b_hi_i);
}
b.truncate(mid);
for (g_lo_i, g_hi_i) in g_lo.iter_mut().zip(g_hi.iter_mut()) {
*g_lo_i *= &u_inv;
*g_lo_i += &(g_hi_i.clone() * &u);
}
g.truncate(mid);
for (h_lo_i, h_hi_i) in h_lo.iter_mut().zip(h_hi.iter_mut()) {
*h_lo_i *= &u;
*h_lo_i += &(h_hi_i.clone() * &u_inv);
}
h.truncate(mid);
}
let a_final = a.pop().expect("a should not be empty");
let b_final = b.pop().expect("b should not be empty");
Some(Proof {
l_r_coms,
transcript_summary: transcript.summarize(),
a_final,
b_final,
})
}
pub fn verify<F: Field + Random, G: CryptoGroup<Scalar = F> + Encode>(
transcript: &mut Transcript,
setup: &Setup<Synthetic<F, G>>,
claim: &Claim<F, G>,
proof: Proof<F, G>,
) -> Option<Synthetic<F, G>>
where
Claim<F, G>: Encode,
{
let rounds = usize::from(claim.log_len);
let claimed_len = 1usize.checked_shl(u32::from(claim.log_len))?;
let Proof {
l_r_coms,
transcript_summary,
a_final,
b_final,
} = proof;
if l_r_coms.len() != rounds {
return None;
}
transcript.commit(claim.encode());
let w = F::random(&mut transcript.noise(b"w challenge"));
let mut us = Vec::<(F, F)>::with_capacity(rounds);
let mut out = Synthetic::concrete(l_r_coms.into_iter().flat_map(|(l, r)| {
transcript.commit(l.encode());
transcript.commit(r.encode());
let u = F::random(transcript.noise(b"u challenge"));
let u_inv = u.inv();
us.push((u.clone(), u_inv.clone()));
let u2 = {
let mut out = u;
out.square();
out
};
let u_inv2 = {
let mut out = u_inv;
out.square();
out
};
[(u2, l), (u_inv2, r)]
}));
if transcript.summarize() != transcript_summary {
return None;
}
out += &Synthetic::concrete([(F::one(), claim.commitment.clone())]);
out += &(setup.product_generator().clone() * &(claim.product.clone() * &w));
let g_weights = {
let mut weights = Vec::<F>::with_capacity(claimed_len);
weights.push(F::one());
for (u, u_inv) in us.into_iter().rev() {
let end = weights.len();
weights.extend_from_within(..);
for left_i in &mut weights[..end] {
*left_i *= &u_inv;
}
for right_i in &mut weights[end..] {
*right_i *= &u;
}
}
weights
};
let h_weights: Vec<F> = g_weights
.iter()
.rev()
.zip(powers(F::one(), &claim.y))
.map(|(w_i, y_i)| y_i * w_i)
.collect();
let g = &setup.g()[..claimed_len];
let h = &setup.h()[..claimed_len];
out -= &(Synthetic::msm(h, &h_weights, &Sequential) * &b_final);
out -= &(Synthetic::msm(g, &g_weights, &Sequential) * &a_final);
out -= &(setup.product_generator().clone() * &(a_final * &b_final * &w));
Some(out)
}
#[cfg(all(test, feature = "arbitrary"))]
mod conformance {
use super::{Claim, Proof, Setup};
use commonware_codec::conformance::CodecConformance;
use commonware_math::test::{F as TestF, G as TestG};
commonware_conformance::conformance_tests! {
CodecConformance<Setup<TestG>>,
CodecConformance<Claim<TestF, TestG>>,
CodecConformance<Proof<TestF, TestG>>,
}
}
#[commonware_macros::stability(ALPHA)]
#[cfg(any(test, feature = "fuzz"))]
pub mod fuzz {
use super::*;
use arbitrary::{Arbitrary, Unstructured};
#[cfg(test)]
use commonware_codec::Decode;
use commonware_math::{
algebra::Additive,
test::{F, G},
};
use commonware_parallel::Sequential;
use std::sync::OnceLock;
const MAX_VECTOR_LG: u8 = 5;
const MAX_VECTOR_LEN: usize = 1 << MAX_VECTOR_LG;
const MAX_SETUP_VECTOR_LEN: usize = 2 * MAX_VECTOR_LEN;
const NUM_GENERATORS: usize = 2 * MAX_SETUP_VECTOR_LEN + 1;
const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_ZK_BULLETPROOFS_IPA";
const BAD_NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_ZK_BULLETPROOFS_IPA_BUT_DIFFERENT";
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(
generators[0],
generators[1..]
.chunks_exact(2)
.map(|chunk| (chunk[0], chunk[1])),
)
})
}
struct Prover<'a> {
setup: &'a Setup<G>,
witness: Witness<F>,
claim: Claim<F, G>,
proof: Proof<F, G>,
bad_namespace: bool,
honest: bool,
}
impl<'a> Prover<'a> {
fn new(setup: &'a Setup<G>, y: F, a: &[F], b: &[F]) -> Self {
let (witness, claim) =
Witness::new_with_claim(setup, y, a.iter().zip(b).map(|(&a, &b)| (a, b)))
.expect("prover expects arguments to match setup");
let proof = prove(
&mut Transcript::new(NAMESPACE),
setup,
&claim,
witness.clone(),
&Sequential,
)
.expect("proving should work");
Self {
setup,
witness,
claim,
proof,
bad_namespace: false,
honest: true,
}
}
#[allow(clippy::missing_const_for_fn)]
fn bad_namespace(&mut self) {
self.honest = false;
self.bad_namespace = true;
}
fn tweak_product(&mut self, delta: F) {
if delta == F::zero() {
return;
}
self.honest = false;
self.claim.product -= δ
self.claim.commitment += &(*self.setup.product_generator() * &delta);
self.proof = prove(
&mut Transcript::new(NAMESPACE),
self.setup,
&self.claim,
self.witness.clone(),
&Sequential,
)
.expect("proving should work after tweaking the public claim");
}
fn increase_length(&mut self) {
self.honest = false;
let longer_log_len = self
.claim
.log_len
.checked_add(1)
.expect("test vectors should support doubling the witness length");
let longer_len = 1usize
.checked_shl(u32::from(longer_log_len))
.expect("witness length should fit into usize");
self.witness.a.resize_with(longer_len, F::zero);
self.witness.b.resize_with(longer_len, F::zero);
let longer_claim = Claim {
log_len: longer_log_len,
..self.claim
};
self.proof = prove(
&mut Transcript::new(NAMESPACE),
self.setup,
&longer_claim,
self.witness.clone(),
&Sequential,
)
.expect("proving should work after increasing the witness length");
}
fn tweak_l_r_coms<'b>(&mut self, u: &mut Unstructured<'b>) -> arbitrary::Result<()> {
let Some(last_round) = self.proof.l_r_coms.len().checked_sub(1) else {
return Ok(());
};
let round = u.int_in_range(0..=last_round)?;
let tweak_left = u.arbitrary::<bool>()?;
let delta = u.arbitrary::<G>()?;
if delta == G::zero() {
return Ok(());
}
self.honest = false;
let (l, r) = &mut self.proof.l_r_coms[round];
if tweak_left {
*l += δ
} else {
*r += δ
}
Ok(())
}
#[allow(clippy::missing_const_for_fn)]
fn honest(&self) -> bool {
self.honest
}
fn verify(self) -> bool {
let ns = if self.bad_namespace {
BAD_NAMESPACE
} else {
NAMESPACE
};
let setup = self.setup;
let claim = self.claim;
let proof = self.proof;
setup
.eval(
|vs| super::verify(&mut Transcript::new(ns), vs, &claim, proof),
&Sequential,
)
.map(|g| g == G::zero())
.unwrap_or(false)
}
}
#[derive(Debug)]
pub struct Plan {
y: F,
a: Vec<F>,
b: Vec<F>,
}
impl<'a> Arbitrary<'a> for Plan {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let lg_len = u.int_in_range(0..=MAX_VECTOR_LG)?;
let len = 1usize << lg_len;
let y = u.arbitrary()?;
let a = (0..len)
.map(|_| u.arbitrary())
.collect::<arbitrary::Result<Vec<_>>>()?;
let b = (0..len)
.map(|_| u.arbitrary())
.collect::<arbitrary::Result<Vec<_>>>()?;
Ok(Self { y, a, b })
}
}
impl Plan {
pub fn run(self, u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
let setup = test_setup();
let mut prover = Prover::new(setup, self.y, &self.a, &self.b);
if u.arbitrary::<bool>()? {
match u.arbitrary::<u8>()? {
x if x < 64 => prover.tweak_product(u.arbitrary::<F>()?),
x if x < 128 => prover.increase_length(),
x if x < 192 => prover.tweak_l_r_coms(u)?,
_ => prover.bad_namespace(),
}
}
match (prover.honest(), prover.verify()) {
(true, true) | (false, false) => {}
(true, false) => panic!("prover honest, but proof didn't verify"),
(false, true) => panic!("prover malicious, but proof verifies!!!"),
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
fn assert_setup_roundtrip(setup: &Setup<G>) {
let encoded = setup.encode();
let decoded: Setup<G> = Setup::decode_cfg(encoded.clone(), &(setup.g.len(), ()))
.expect("setup should decode with its own length bound");
assert_eq!(setup, &decoded);
assert_eq!(decoded.encode(), encoded);
}
fn assert_claim_roundtrip(claim: &Claim<F, G>) {
let encoded = claim.encode();
let decoded: Claim<F, G> = Claim::decode_cfg(encoded.clone(), &((), ()))
.expect("claim should decode with unit cfg");
assert_eq!(claim, &decoded);
assert_eq!(decoded.encode(), encoded);
}
fn assert_proof_roundtrip(proof: &Proof<F, G>) {
let max_len = if proof.l_r_coms.is_empty() {
0
} else {
1usize
.checked_shl(proof.l_r_coms.len() as u32)
.expect("proof arbitrary bounds rounds to fit in usize")
};
let encoded = proof.encode();
let decoded: Proof<F, G> = Proof::decode_cfg(encoded.clone(), &(max_len, ((), ())))
.expect("proof should decode with a matching round bound");
assert_eq!(proof, &decoded);
assert_eq!(decoded.encode(), encoded);
}
#[test]
fn test_codec_roundtrip() {
commonware_invariants::minifuzz::test(|u| {
assert_setup_roundtrip(&u.arbitrary::<Setup<G>>()?);
assert_claim_roundtrip(&u.arbitrary::<Claim<F, G>>()?);
assert_proof_roundtrip(&u.arbitrary::<Proof<F, G>>()?);
Ok(())
});
}
#[test]
fn test_fuzz() {
commonware_invariants::minifuzz::test(|u| u.arbitrary::<Plan>()?.run(u));
}
#[test]
fn prover_tweaks_cover_edge_paths() {
let setup = test_setup();
let mut honest = Prover::new(setup, F::from(1u8), &[F::from(3u8)], &[F::from(4u8)]);
honest.tweak_product(F::zero());
honest
.tweak_l_r_coms(&mut Unstructured::new(&[]))
.expect("single-round proof should no-op before reading fuzz input");
assert!(honest.honest());
assert!(honest.verify());
type Tweak = Box<dyn FnOnce(&mut Prover<'static>)>;
let failures: [Tweak; 4] = [
Box::new(|p| p.tweak_product(F::from(1u8))),
Box::new(|p| p.increase_length()),
Box::new(|p| {
p.tweak_l_r_coms(&mut Unstructured::new(&[1, 1, 0, 0, 0, 0, 0, 0, 0]))
.expect("structured fuzz input should mutate a proof round");
}),
Box::new(|p| p.bad_namespace()),
];
for tweak in failures {
let mut prover = Prover::new(
setup,
F::from(1u8),
&[F::from(3u8), F::from(5u8)],
&[F::from(4u8), F::from(6u8)],
);
tweak(&mut prover);
assert!(!prover.honest());
assert!(!prover.verify());
}
}
}
}