commonware_cryptography/zk/bulletproofs/ipa.rs
1//! This module provides an "Inner Product Argument", using [Bulletproofs](https://eprint.iacr.org/2017/1066).
2//!
3//! # Background
4//!
5//! We have a cryptographic group `G`, with associated scalar field `F`.
6//!
7//! Prior to this, we have agreed on distinct group elements `G_i`, `H_i`, and `Q`.
8//!
9//! A prover has two vectors of field elements `a_i` and `b_i`. They have created
10//! a commitment `P` to these vectors, defined as `P = <a_i, G_i> + <b_i, y^i H_i>`.
11//! They want to convince the verifier that the product of the vectors that `P`
12//! commits to is equal to `c = <a_i, b_i>`.
13//!
14//! One way to do this is to simply send the vectors `a_i` and `b_i`. The goal
15//! of the Bulletproofs IPA is to convince the verifier while sending less information.
16//! The result is that we can convince the verifier while sending `O(lg N)` group elements
17//! rather than `O(N)` field elements.
18//!
19//! Importantly, this argument is NOT zero-knowledge. The verifier learns information
20//! about the vectors. This argument can be used as part of a broader zero-knowledge
21//! protocol though, but this step by itself does not provide that property.
22//!
23//! The prover's work is mostly `O(N)` scalar multiplications, and the verifier's
24//! work is mostly an MSM of size `O(N)`. The verifier gets to be a bit faster because
25//! they can do a large MSM rather than many individual scalar multiplications,
26//! but the asymptotic complexity is the same.
27//!
28//! # Usage
29//!
30//! Let's look at the concrete API now.
31//!
32//! We need group elements we can use to create commitments. This is what [`Setup`]
33//! is for. A single [`Setup`] can support arguments for vectors of various sizes.
34//! [`Setup::new`] creates a setup by explicitly providing all of the generators
35//! we need.
36//!
37//! Next, we need to actual vectors we want to make a proof over. This is the
38//! [`Witness`] type. This can be constructed with [`Witness::new`], which enforces
39//! that the vectors have the same length, and that this length is a power of two.
40//! This is a technical requirement for the Bulletproofs IPA. Padding should be
41//! handled at the layer above.
42//!
43//! Next, we need the public statement, represented by [`Claim`]. This contains
44//! the claimed product `c`, and the commitment `P`. For a honest prover that
45//! wants to generate a claim from a witness, you can use [`Witness::new_with_claim`],
46//! which makes sure that the witness satisfies the same conditions as [`Witness::new`],
47//! while also calculating the claim.
48//!
49//! This is not necessarily what you want to do in all situations. When using
50//! the Bulletproofs IPA as a step in a larger proof system, you might have a claim
51//! and witness which come from previous steps. Because of that, you can construct
52//! a [`Claim`] directly, using its public fields.
53//!
54//! Because a single [`Setup`] can support vectors of different lengths, the claim
55//! also needs to contain information about the length of these vectors.
56//!
57//! Given a [`Setup`], [`Witness`], and [`Claim`], you can create a [`Proof`]
58//! with [`prove`].
59//!
60//! Both [`prove`] and [`verify`] take a [`Transcript`]. The proof is only valid
61//! for the transcript state used to produce it, so the verifier must replay the
62//! same transcript history before calling [`verify`].
63//!
64//! On the verifier side, we don't have a [`Witness`], and can instead check
65//! that the prover had a valid witness, using their [`Proof`], through [`verify`].
66//! The result is a [`Synthetic`] verification equation that can be evaluated
67//! against the setup's generators, or combined with other equations for batching.
68//!
69//! ## Example
70//!
71//! ```rust
72//! # use commonware_cryptography::{
73//! # bls12381::primitives::group::{G1, Scalar},
74//! # transcript::Transcript,
75//! # zk::bulletproofs::ipa::{prove, verify, Setup, Witness},
76//! # };
77//! # use commonware_math::algebra::{Additive, CryptoGroup, Ring};
78//! # use commonware_parallel::Sequential;
79//! # type F = Scalar;
80//! # type G = G1;
81//! # #[allow(non_snake_case)]
82//! # let GENERATORS: [G; 9] = core::array::from_fn(|i| G::generator() * &F::from(i as u64 + 1));
83//!
84//! // It's important that these generators have no known discrete logarithm
85//! // relationships relative to each other. For example, multipying a single
86//! // generator would be insecure!
87//! let setup = Setup::new(
88//! GENERATORS[0].clone(),
89//! GENERATORS[1..]
90//! .chunks_exact(2)
91//! .map(|chunk| (chunk[0].clone(), chunk[1].clone())),
92//! );
93//!
94//! // Witness vectors must have the same power-of-two length.
95//! let (witness, claim) = Witness::new_with_claim(
96//! &setup,
97//! F::one(),
98//! [
99//! (F::from(3u64), F::from(4u64)),
100//! (F::from(5u64), F::from(6u64)),
101//! (F::from(7u64), F::from(8u64)),
102//! (F::from(9u64), F::from(10u64)),
103//! ],
104//! )
105//! .expect("witness should fit the setup");
106//!
107//! // The proof is bound to this transcript state.
108//! let mut prover_transcript = Transcript::new(b"ipa-example");
109//! prover_transcript.commit(b"context".as_slice());
110//!
111//! // Any Strategy works here. Sequential is simplest; a parallel strategy can
112//! // reduce wall-clock time on larger inputs without changing the proof.
113//! let strategy = Sequential;
114//! let proof = prove(&mut prover_transcript, &setup, &claim, witness, &strategy)
115//! .expect("claim should match the witness and setup");
116//!
117//! // Verification must replay the same transcript state.
118//! let mut verifier_transcript = Transcript::new(b"ipa-example");
119//! verifier_transcript.commit(b"context".as_slice());
120//! let valid = setup
121//! .eval(|vs| verify(&mut verifier_transcript, vs, &claim, proof), &strategy)
122//! .map(|g| g == G::zero())
123//! .unwrap_or(false);
124//! assert!(valid);
125//! ```
126//!
127//! # References
128//!
129//! The [Dalek crate](https://doc-internal.dalek.rs/bulletproofs/notes/inner_product_proof/index.html)
130//! was an invaluable reference when implementing and documenting this module.
131
132use crate::transcript::{Summary, Transcript};
133use bytes::{Buf, BufMut};
134use commonware_codec::{Encode, EncodeSize, Error, RangeCfg, Read, ReadExt, Write};
135use commonware_math::{
136 algebra::{powers, CryptoGroup, Field, Random, Space},
137 synthetic::Synthetic,
138};
139use commonware_parallel::{Sequential, Strategy};
140
141/// A setup decides on what group elements we use to commit to vectors and their product.
142///
143/// A setup for an inner product argument for `c = <a_i, b_i>` needs generators
144/// to commit to `a_i`, which we call `G_i`, generators for `b_i`, which we call
145/// `H_i`, and a generator for the product, `c`, which we call `Q`, or "the product generator".
146///
147/// We can support inner products of different sizes, as long as we have enough generators.
148///
149/// To construct this type, see [`Self::new`].
150#[derive(Debug, PartialEq)]
151pub struct Setup<G> {
152 g: Vec<G>,
153 h: Vec<G>,
154 product_generator: G,
155}
156
157impl<G: Write> Write for Setup<G> {
158 fn write(&self, buf: &mut impl BufMut) {
159 self.product_generator.write(buf);
160 self.g.len().write(buf);
161 for (g_i, h_i) in self.g.iter().zip(&self.h) {
162 g_i.write(buf);
163 h_i.write(buf);
164 }
165 }
166}
167
168impl<G: EncodeSize> EncodeSize for Setup<G> {
169 fn encode_size(&self) -> usize {
170 self.product_generator.encode_size()
171 + self.g.len().encode_size()
172 + self
173 .g
174 .iter()
175 .zip(&self.h)
176 .map(|(g_i, h_i)| g_i.encode_size() + h_i.encode_size())
177 .sum::<usize>()
178 }
179}
180
181impl<G: Read> Read for Setup<G> {
182 type Cfg = (usize, G::Cfg);
183
184 fn read_cfg(buf: &mut impl Buf, (max_len, cfg): &Self::Cfg) -> Result<Self, Error> {
185 let product_generator = G::read_cfg(buf, cfg)?;
186 let len = usize::read_cfg(buf, &RangeCfg::new(..=*max_len))?;
187 let mut g = Vec::with_capacity(len);
188 let mut h = Vec::with_capacity(len);
189 for _ in 0..len {
190 g.push(G::read_cfg(buf, cfg)?);
191 h.push(G::read_cfg(buf, cfg)?);
192 }
193 Ok(Self {
194 g,
195 h,
196 product_generator,
197 })
198 }
199}
200
201#[cfg(any(test, feature = "arbitrary"))]
202impl<G> arbitrary::Arbitrary<'_> for Setup<G>
203where
204 G: for<'a> arbitrary::Arbitrary<'a>,
205{
206 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
207 let g_and_h = u.arbitrary::<Vec<(G, G)>>()?;
208 Ok(Self::new(u.arbitrary()?, g_and_h))
209 }
210}
211
212impl<G> Setup<G> {
213 /// Create a new [`Setup`], given specific choices of the generator.
214 ///
215 /// You MUST ensure that all of the values provided to this function are unique.
216 pub fn new(product_generator: G, g_and_h: impl IntoIterator<Item = (G, G)>) -> Self {
217 let (g, h): (Vec<G>, Vec<G>) = g_and_h.into_iter().collect();
218 Self {
219 g,
220 h,
221 product_generator,
222 }
223 }
224
225 /// The left-side generators `G_i`.
226 pub fn g(&self) -> &[G] {
227 &self.g
228 }
229
230 /// The right-side generators `H_i`.
231 pub fn h(&self) -> &[G] {
232 &self.h
233 }
234
235 /// The product generator `Q`.
236 pub const fn product_generator(&self) -> &G {
237 &self.product_generator
238 }
239
240 /// Check if this setup supports claims of a given length.
241 pub const fn supports(&self, lg_len: u8) -> bool {
242 self.g.len() >> lg_len > 0
243 }
244
245 /// Build a virtual setup, call `f` to obtain a verification equation,
246 /// and evaluate it against the concrete generators in `self`.
247 ///
248 /// Returns `None` when `f` returns `None` (malformed proof).
249 /// Otherwise returns the evaluated group element, which should be
250 /// zero for a valid proof.
251 pub fn eval<F: Field>(
252 &self,
253 f: impl FnOnce(&Setup<Synthetic<F, G>>) -> Option<Synthetic<F, G>>,
254 strategy: &impl Strategy,
255 ) -> Option<G>
256 where
257 G: Space<F>,
258 {
259 let n = self.g.len();
260 let mut gens = Synthetic::<F, G>::generators();
261 let vg: Vec<_> = (0..n)
262 .map(|_| gens.next().expect("generators is infinite"))
263 .collect();
264 let vh: Vec<_> = (0..n)
265 .map(|_| gens.next().expect("generators is infinite"))
266 .collect();
267 let vq = gens.next().expect("generators is infinite");
268 let vs = Setup::new(vq, vg.into_iter().zip(vh));
269 let mut flat = Vec::with_capacity(2 * n + 1);
270 flat.extend_from_slice(&self.g);
271 flat.extend_from_slice(&self.h);
272 flat.push(self.product_generator.clone());
273 f(&vs).map(|v| v.eval(&flat, strategy))
274 }
275}
276
277/// The public claim we're making about the inner product.
278///
279/// We claim that our commitment `P` is equal to `<a_i, G_i> + <b_i, y^i H_i>`,
280/// and that our product `c` is equal to `<a_i, b_i>`.
281#[derive(Debug, PartialEq)]
282pub struct Claim<F, G> {
283 pub commitment: G,
284 pub product: F,
285 pub y: F,
286 /// The claimed vector length, stored as `log2(len)`.
287 ///
288 /// Inner product arguments require power-of-two vector lengths, so storing
289 /// the logarithm is enough to recover the full claimed length.
290 pub log_len: u8,
291}
292
293impl<F: Write, G: Write> Write for Claim<F, G> {
294 fn write(&self, buf: &mut impl BufMut) {
295 self.commitment.write(buf);
296 self.product.write(buf);
297 self.y.write(buf);
298 self.log_len.write(buf);
299 }
300}
301
302impl<F: EncodeSize, G: EncodeSize> EncodeSize for Claim<F, G> {
303 fn encode_size(&self) -> usize {
304 self.commitment.encode_size()
305 + self.product.encode_size()
306 + self.y.encode_size()
307 + self.log_len.encode_size()
308 }
309}
310
311impl<F: Read, G: Read> Read for Claim<F, G> {
312 type Cfg = (G::Cfg, F::Cfg);
313
314 fn read_cfg(buf: &mut impl Buf, (g_cfg, f_cfg): &Self::Cfg) -> Result<Self, Error> {
315 Ok(Self {
316 commitment: G::read_cfg(buf, g_cfg)?,
317 product: F::read_cfg(buf, f_cfg)?,
318 y: F::read_cfg(buf, f_cfg)?,
319 log_len: u8::read(buf)?,
320 })
321 }
322}
323
324#[cfg(any(test, feature = "arbitrary"))]
325impl<F, G> arbitrary::Arbitrary<'_> for Claim<F, G>
326where
327 F: for<'a> arbitrary::Arbitrary<'a>,
328 G: for<'a> arbitrary::Arbitrary<'a>,
329{
330 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
331 Ok(Self {
332 commitment: u.arbitrary()?,
333 product: u.arbitrary()?,
334 y: u.arbitrary()?,
335 log_len: u.arbitrary()?,
336 })
337 }
338}
339
340/// The witness contains the actual vectors `a_i` and `b_i` for the inner product argument.
341///
342/// This struct guarantees that their lengths are equal, and a power of two.
343#[derive(Clone)]
344pub struct Witness<F> {
345 a: Vec<F>,
346 b: Vec<F>,
347}
348
349impl<F> Witness<F> {
350 /// Create a new witness, from the two vectors whose product we're taking.
351 ///
352 /// This function returns `None` if the iterator does not produce a power of
353 /// two number of elements.
354 pub fn new(elements: impl IntoIterator<Item = (F, F)>) -> Option<Self> {
355 let (a, b): (Vec<F>, Vec<F>) = elements.into_iter().collect();
356 if !a.len().is_power_of_two() {
357 return None;
358 }
359 Some(Self { a, b })
360 }
361}
362
363impl<F: Field> Witness<F> {
364 /// Like [`Self::new`], but also produces a [`Claim`], for convenience.
365 ///
366 /// In some situations, you have a claim from somewhere else, using the
367 /// proof system in this module as just one step in some larger proof.
368 ///
369 /// If you don't have a claim, this lets you compute a valid one.
370 ///
371 /// To do so, you need a [`Setup`], which can be reused across different
372 /// witnesses.
373 pub fn new_with_claim<G: Space<F>>(
374 setup: &Setup<G>,
375 y: F,
376 elements: impl IntoIterator<Item = (F, F)>,
377 ) -> Option<(Self, Claim<F, G>)> {
378 let witness = Self::new(elements)?;
379 // By invariant, h has the same len as g, and b has the same len as a,
380 // so we can just check this.
381 if setup.g.len() < witness.a.len() {
382 return None;
383 }
384 let claim = {
385 let mut commitment = G::zero();
386 let mut product = F::zero();
387 for ((((a_i, b_i), g_i), h_i), y_i) in witness
388 .a
389 .iter()
390 .zip(&witness.b)
391 .zip(&setup.g)
392 .zip(&setup.h)
393 .zip(powers(F::one(), &y))
394 {
395 commitment += &(g_i.clone() * a_i + &(h_i.clone() * &(b_i.clone() * &y_i)));
396 product += &(a_i.clone() * b_i);
397 }
398 Claim {
399 commitment,
400 product,
401 y,
402 log_len: witness.a.len().ilog2() as u8,
403 }
404 };
405 Some((witness, claim))
406 }
407}
408
409/// A proof for the inner product argument.
410#[derive(Clone, Debug, PartialEq)]
411pub struct Proof<F, G> {
412 l_r_coms: Vec<(G, G)>,
413 /// Summary of the transcript after the public statement and all proof messages.
414 ///
415 /// This binds even zero-round exchanges to the transcript.
416 transcript_summary: Summary,
417 a_final: F,
418 b_final: F,
419}
420
421impl<F: Write, G: Write> Write for Proof<F, G> {
422 fn write(&self, buf: &mut impl BufMut) {
423 self.l_r_coms.write(buf);
424 self.transcript_summary.write(buf);
425 self.a_final.write(buf);
426 self.b_final.write(buf);
427 }
428}
429
430impl<F: EncodeSize, G: EncodeSize> EncodeSize for Proof<F, G> {
431 fn encode_size(&self) -> usize {
432 self.l_r_coms.encode_size()
433 + self.transcript_summary.encode_size()
434 + self.a_final.encode_size()
435 + self.b_final.encode_size()
436 }
437}
438
439impl<F: Read, G: Read> Read for Proof<F, G> {
440 type Cfg = (usize, (G::Cfg, F::Cfg));
441
442 fn read_cfg(buf: &mut impl Buf, (max_len, (g_cfg, f_cfg)): &Self::Cfg) -> Result<Self, Error> {
443 let max_rounds = if *max_len == 0 {
444 0
445 } else {
446 max_len.ilog2() as usize
447 };
448 Ok(Self {
449 l_r_coms: Vec::<(G, G)>::read_cfg(
450 buf,
451 &(RangeCfg::new(..=max_rounds), (g_cfg.clone(), g_cfg.clone())),
452 )?,
453 transcript_summary: Summary::read(buf)?,
454 a_final: F::read_cfg(buf, f_cfg)?,
455 b_final: F::read_cfg(buf, f_cfg)?,
456 })
457 }
458}
459
460#[cfg(any(test, feature = "arbitrary"))]
461impl<F, G> arbitrary::Arbitrary<'_> for Proof<F, G>
462where
463 F: for<'a> arbitrary::Arbitrary<'a>,
464 G: for<'a> arbitrary::Arbitrary<'a>,
465{
466 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
467 let rounds = u.int_in_range(0..=usize::BITS as usize - 1)?;
468 let l_r_coms = (0..rounds)
469 .map(|_| u.arbitrary())
470 .collect::<arbitrary::Result<Vec<_>>>()?;
471 Ok(Self {
472 l_r_coms,
473 transcript_summary: u.arbitrary()?,
474 a_final: u.arbitrary()?,
475 b_final: u.arbitrary()?,
476 })
477 }
478}
479
480/// Prove that a given [`Witness`] is valid, relative to a [`Claim`] and [`Setup`].
481///
482/// We also take in a transcript. The proof is bound to the transcript state at
483/// the time of this call, so the verifier must replay the same transcript
484/// history before calling [`verify`].
485///
486/// This returns `None` if the setup is too short for the witness, or if the
487/// claim's vector length does not match the witness length.
488pub fn prove<F: Field + Random, G: CryptoGroup<Scalar = F> + Encode>(
489 transcript: &mut Transcript,
490 setup: &Setup<G>,
491 claim: &Claim<F, G>,
492 witness: Witness<F>,
493 strategy: &impl Strategy,
494) -> Option<Proof<F, G>>
495where
496 Claim<F, G>: Encode,
497{
498 // Okay, let's explain the math behind how this proof system works.
499 //
500 // (Once again, https://doc-internal.dalek.rs/bulletproofs/notes/inner_product_proof/index.html,
501 // is a useful reference, inspiring much of this documentation).
502 //
503 // We'll describe the protocol as if it were interactive. We turn it into
504 // a non-interactive protocol using the venerable Fiat-Shamir transform.
505 // The Transcript abstraction helps us with that.
506 //
507 // We have vectors a_i and b_i, in our claim, we have:
508 //
509 // P = <a_i, G_i> + <b_i, y^i H_i>
510 // c = <a_i, b_i>
511 //
512 // for recursion, it's convenient to have a statement about one commitment
513 // instead. We can also run the protocol over H'_i := y^i H_i instead.
514 //
515 // We can have the verifier give us a challenge w, compressing this into:
516 //
517 // P = <a_i, G_i> + <b_i, H'_i> + c * w * Q
518 //
519 // where Q is the additional generator from our setup.
520 //
521 // For the recursion, the idea is that at each round, we have:
522 //
523 // P_k = <a_k_i, G_k_i> + <b_k_i, H_k_i> + <a_k_i, b_k_i> w Q
524 //
525 // and our goal is to turn (P_k, a_k_i, b_k_i) into (P_(k-1), a_(k-1)_i, b_(k-1)_i)
526 // at each round, with the vectors halving in size. Eventually, we'll just
527 // have a single element, which is trivial to prove just by sending it over.
528 //
529 // Not having a good explanation for why the following trick works, let's shut
530 // up and calculate. Assume we have some folding coefficient u_k:
531 //
532 // a_(k-1)_i := u_k a_i + u_k^-1 a_(mid + i)
533 // b_(k-1)_i := u_k^-1 b_i + u_k b_(mid + i)
534 // G_(k-1)_i := u_k^-1 G_i + u_k G_(mid + i)
535 // H_(k-1)_i := u_k H_i + u_k^-1 H_(mid + i)
536 //
537 // (the new vectors are half the size, and mid is the new midpoint)
538 //
539 // then, we get:
540 //
541 // P_(k-1) =
542 // <u_k a_i + u_k^-1 a_(mid + i), u_k^-1 G_i + u_k G_(mid + i)> +
543 // <u_k^-1 b_i + u_k b_(mid + i), u_k H_i + u_k^-1 H_(mid + i)> +
544 // <u_k a_i + u_k^-1 a_(mid + i), u_k^-1 b_i + u_k b_(mid + i)>
545 //
546 // shutting up and calculating, we get:
547 //
548 // <a_i, G_i> + <u_k^2 a_i, G_(mid + i)> + <u_k^-2 a_(mid + i), G_i> + <a_(mid + i), G_(mid + i)> +
549 // <b_i, H_i> + <u_k^-2 b_i, H_(mid + i)> + <u_k^2 b_(mid + i), H_i> + <b_(mid + i), H_(mid + i)> +
550 // <a_i, b_i> + <u_k^2 a_i, b_(mid + i)> + <u_k^-2 a_(mid + i), b_i> + <a_(mid + i), b_(mid + i)>
551 //
552 // we can group terms by coefficient, and notice that we have:
553 //
554 // <a_i, G_i> + <a_(mid + i), G_(mid + i)> +
555 // <b_i, H_i> + <b_(mid + i), H_(mid + i)> +
556 // <a_i, b_i> + <a_(mid + i), b_(mid + i)> +
557 // u_k^2 (<a_i, G_(mid + i)> + <b_(mid + i), H_i> + <a_i, b_(mid + i)>) +
558 // u_k^-2 (<a_(mid + i), G_i> + <b_i, H_(mid + i)> + <a_(mid + i), b_i>)
559 //
560 // However, the first few lines of this are just P_k, so we have:
561 //
562 // P_(k-1) = P_k + u_k^2 L_k + u_k^-2 R_k
563 //
564 // defining L_k and R_k as shorthand to the terms above.
565 //
566 // How do we use this fact? We have the prover calculate L_k and R_k, send
567 // them over to the verifier, who responds with a challenge u_k. We can then
568 // use that challenge to calculate the new vectors a_(k-1)_i,...
569 //
570 // The verifier can also check the provers work, by verifying:
571 //
572 // P_k + u_k^2 L_k + u_k^-2 R_k =? P_(k-1)
573 //
574 // In fact, we don't even need to send P_(k-1) either. The prover
575 // knows what P_(k-1) needs to equal, thus determining what P_(k-2) should
576 // be, and so on, until we reach a final value P_0.
577 //
578 // For that final value, we have vectors of size 1, so we can send them over,
579 // and have the verifier check:
580 //
581 // P_0 =? a_0 G_0 + b_0 H_0 + a_0 b_0 w B
582 //
583 // with P_0 being calculated by the verifier, from the initial generators,
584 // claim, and the challenges.
585 let witness_len = witness.a.len();
586 let claimed_len = 1usize.checked_shl(u32::from(claim.log_len))?;
587 if claimed_len != witness_len || setup.g.len() < witness_len {
588 return None;
589 }
590 // At this point, we've committed to the claim we're trying to prove, so
591 // we can't pull any shenanigans by modifying the claim based on the challenges.
592 transcript.commit(claim.encode());
593 let w = F::random(transcript.noise(b"w challenge"));
594 let w_q = setup.product_generator.clone() * &w;
595
596 let mut l_r_coms = Vec::<(G, G)>::new();
597 let mut a = witness.a;
598 let mut b = witness.b;
599 let mut g = setup.g[..witness_len].to_vec();
600 let mut h = setup.h[..witness_len].to_vec();
601 for (h_i, y_i) in h.iter_mut().zip(powers(F::one(), &claim.y)) {
602 *h_i *= &y_i;
603 }
604 while a.len() > 1 {
605 let mid = a.len() / 2;
606 let (a_lo, a_hi) = a.split_at_mut(mid);
607 let (b_lo, b_hi) = b.split_at_mut(mid);
608 let (g_lo, g_hi) = g.split_at(mid);
609 let (h_lo, h_hi) = h.split_at(mid);
610 let l = G::msm(g_hi, a_lo, strategy)
611 + &G::msm(h_lo, b_hi, strategy)
612 + &(w_q.clone() * &F::msm(a_lo, b_hi, strategy));
613 let r = G::msm(g_lo, a_hi, strategy)
614 + &G::msm(h_hi, b_lo, strategy)
615 + &(w_q.clone() * &F::msm(a_hi, b_lo, strategy));
616 l_r_coms.push((l.clone(), r.clone()));
617 transcript.commit(l.encode());
618 transcript.commit(r.encode());
619 let u = F::random(transcript.noise(b"u challenge"));
620 let u_inv = u.inv();
621
622 for (a_lo_i, a_hi_i) in a_lo.iter_mut().zip(a_hi.iter_mut()) {
623 *a_lo_i *= &u;
624 *a_lo_i += &(u_inv.clone() * a_hi_i);
625 }
626 a.truncate(mid);
627
628 for (b_lo_i, b_hi_i) in b_lo.iter_mut().zip(b_hi.iter_mut()) {
629 *b_lo_i *= &u_inv;
630 *b_lo_i += &(u.clone() * b_hi_i);
631 }
632 b.truncate(mid);
633
634 let u_u_inv = [u.clone(), u_inv.clone()];
635 let (new_g, new_h) = strategy.join(
636 || {
637 strategy.map_collect_vec(g_lo.iter().zip(g_hi), |(g_lo_i, g_hi_i)| {
638 G::msm(&[g_hi_i.clone(), g_lo_i.clone()], &u_u_inv, strategy)
639 })
640 },
641 || {
642 strategy.map_collect_vec(h_lo.iter().zip(h_hi), |(h_lo_i, h_hi_i)| {
643 G::msm(&[h_lo_i.clone(), h_hi_i.clone()], &u_u_inv, strategy)
644 })
645 },
646 );
647 g = new_g;
648 h = new_h;
649 }
650 let a_final = a.pop().expect("a should not be empty");
651 let b_final = b.pop().expect("b should not be empty");
652 Some(Proof {
653 l_r_coms,
654 transcript_summary: transcript.summarize(),
655 a_final,
656 b_final,
657 })
658}
659
660/// Construct the verification equation for a [`Proof`], relative to a
661/// [`Claim`] and a virtual [`Setup`].
662///
663/// If the check succeeds, we are convinced that the prover knows a valid
664/// [`Witness`] to this particular [`Claim`].
665///
666/// The returned [`Synthetic`] should evaluate to zero for a correct proof.
667/// Use [`Setup::eval`] to create the virtual setup and evaluate the result.
668///
669/// The return will be `None` if the proof is incorrect in an obvious way.
670pub fn verify<F: Field + Random, G: CryptoGroup<Scalar = F> + Encode>(
671 transcript: &mut Transcript,
672 setup: &Setup<Synthetic<F, G>>,
673 claim: &Claim<F, G>,
674 proof: Proof<F, G>,
675) -> Option<Synthetic<F, G>>
676where
677 Claim<F, G>: Encode,
678{
679 // See the prove function for some more explanation of the math.
680 // If you read that function's documentation naively, you might come under
681 // the impression that we have to naively follow the prover, folding the
682 // generators at each step, in order to produce the final value P_0, which
683 // we can then use to check that final a_0 and b_0. This is not ideal,
684 // because it's more efficient to do scalar multiplications as a batch, using
685 // an MSM. Our goal will thus be to reduce all of our work to hashing, in order
686 // to get the challenges, and a single large MSM.
687 //
688 // The final check we have is:
689 //
690 // P_0 =? a_0 G_0 + b_0 H_0 + a_0 b_0 w Q
691 //
692 // What is P_0? Well, it must be equal to:
693 //
694 // P_1 - u_1^2 L_1 - u_1^-2 R_1
695 //
696 // we can unravel P_1, and so, on, to get:
697 //
698 // P_0 = P + c w Q - <u_k^2, L_k> - <u_k^-2, R_k>
699 //
700 // and that's nice and ready for an MSM. The issue is now how to figure out
701 // G_0 and H_0. Intuitively, this should be possible to do as a large MSM
702 // of the original G_i and H_i. This is because each folding step is just a linear
703 // transformation of the prior vectors. Composing these will still result in
704 // a linear transformation. We just need to figure out the weights for this.
705 //
706 // For vectors of size 1, this is trivial, the weights are just 1.
707 //
708 // Let's say we've figured out the weights for G_(k-1), what should the weights
709 // for G_k be? We want:
710 //
711 // <g_(k-1)_j, G_(k-1)_j> = <g_k_i, G_k_i>
712 //
713 // i.e. the weights we want should produce the same result as folding, and then
714 // using the weights we know exist by induction. (If this is not easy to understand,
715 // imagine that the next layer beneath us is just the trivial layer, with one element,
716 // and a single weight equal to 1).
717 //
718 // We can expand the result of folding, to get:
719 //
720 // <g_(k-1)_j, u_k^-1 G_k_j + u_k G_k_(mid + j)>
721 //
722 // but, this gives us the weights we need, defining:
723 //
724 // g_k_i := u_k^{if i < mid { -1 } else { 1 }} g_(k - 1)_(i % mid)
725 //
726 // Another way of visualizing what's happening here: at each iteration, as
727 // we double the size of the weights, what we're doing is copying the existing
728 // weights, and then multiplying the left side by u_k^-1, and the right side
729 // by u_k.
730 //
731 // Here's an example progression:
732 //
733 // 1
734 //
735 // 1, 1
736 // u_1^-1, u_1
737 //
738 // u_1^-1, u_1, u_1^-1, u_1
739 // u_1^-1 u_2^-1, u_1 u_2^-1, u_1^-1 u_2, u_1 u_2
740 //
741 // Now, we don't actually need to do anything special for H, because it turns
742 // out that the weights we need are just the ones we've calculated for G, just
743 // in reverse order! To see why, note that the only difference with H is that
744 // we need to use u_k on the left, and u_k^-1 on the right. The vector we
745 // have at each step is the result of copying the previous vector, doubling its size,
746 // and then multiplying with one value and the left, and the other on the right.
747 // If we reverse this vector, the result we get is the same as if we had reversed
748 // the previous step's vector, copied it, and then multiplied with u_k on the left,
749 // and u_k^-1 on the right, which is exactly what we need to do.
750 //
751 // Recall also that the prover starts by turning H_i -> H'_i = y^i H_i. We can
752 // accomplish this by multiplying our weights for those values by y^i as well.
753 let rounds = usize::from(claim.log_len);
754 let claimed_len = 1usize.checked_shl(u32::from(claim.log_len))?;
755 let Proof {
756 l_r_coms,
757 transcript_summary,
758 a_final,
759 b_final,
760 } = proof;
761 if l_r_coms.len() != rounds {
762 return None;
763 }
764 transcript.commit(claim.encode());
765
766 let w = F::random(transcript.noise(b"w challenge"));
767
768 // We reduce verification down to one MSM which needs to equal 0:
769 // commitment + product * U + sum(u_i^2 * L_i + u_i^-2 * R_i)
770 // - a_final * g_final - b_final * h_final - a_final * b_final * U = 0.
771 let mut us = Vec::<(F, F)>::with_capacity(rounds);
772 let mut out = Synthetic::concrete(l_r_coms.into_iter().flat_map(|(l, r)| {
773 transcript.commit(l.encode());
774 transcript.commit(r.encode());
775 let u = F::random(transcript.noise(b"u challenge"));
776 let u_inv = u.inv();
777 us.push((u.clone(), u_inv.clone()));
778 let u2 = {
779 let mut out = u;
780 out.square();
781 out
782 };
783 let u_inv2 = {
784 let mut out = u_inv;
785 out.square();
786 out
787 };
788 [(u2, l), (u_inv2, r)]
789 }));
790 if transcript.summarize() != transcript_summary {
791 return None;
792 }
793 out += &Synthetic::concrete([(F::one(), claim.commitment.clone())]);
794 out += &(setup.product_generator().clone() * &(claim.product.clone() * &w));
795 let g_weights = {
796 let mut weights = Vec::<F>::with_capacity(claimed_len);
797 weights.push(F::one());
798 for (u, u_inv) in us.into_iter().rev() {
799 let end = weights.len();
800 weights.extend_from_within(..);
801 for left_i in &mut weights[..end] {
802 *left_i *= &u_inv;
803 }
804 for right_i in &mut weights[end..] {
805 *right_i *= &u;
806 }
807 }
808 weights
809 };
810 let h_weights: Vec<F> = g_weights
811 .iter()
812 .rev()
813 .zip(powers(F::one(), &claim.y))
814 .map(|(w_i, y_i)| y_i * w_i)
815 .collect();
816 let g = &setup.g()[..claimed_len];
817 let h = &setup.h()[..claimed_len];
818 out -= &(Synthetic::msm(h, &h_weights, &Sequential) * &b_final);
819 out -= &(Synthetic::msm(g, &g_weights, &Sequential) * &a_final);
820 out -= &(setup.product_generator().clone() * &(a_final * &b_final * &w));
821 Some(out)
822}
823
824#[cfg(all(test, feature = "arbitrary"))]
825mod conformance {
826 use super::{Claim, Proof, Setup};
827 use commonware_codec::conformance::CodecConformance;
828 use commonware_math::test::{F as TestF, G as TestG};
829
830 commonware_conformance::conformance_tests! {
831 CodecConformance<Setup<TestG>>,
832 CodecConformance<Claim<TestF, TestG>>,
833 CodecConformance<Proof<TestF, TestG>>,
834 }
835}
836
837#[commonware_macros::stability(ALPHA)]
838#[cfg(any(test, feature = "fuzz"))]
839pub mod fuzz {
840 use super::*;
841 use arbitrary::{Arbitrary, Unstructured};
842 #[cfg(test)]
843 use commonware_codec::Decode;
844 use commonware_math::{
845 algebra::Additive,
846 test::{F, G},
847 };
848 use commonware_parallel::Sequential;
849 use std::sync::OnceLock;
850
851 const MAX_VECTOR_LG: u8 = 5;
852 const MAX_VECTOR_LEN: usize = 1 << MAX_VECTOR_LG;
853 const MAX_SETUP_VECTOR_LEN: usize = 2 * MAX_VECTOR_LEN;
854 const NUM_GENERATORS: usize = 2 * MAX_SETUP_VECTOR_LEN + 1;
855 const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_ZK_BULLETPROOFS_IPA";
856 const BAD_NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_ZK_BULLETPROOFS_IPA_BUT_DIFFERENT";
857
858 fn test_setup() -> &'static Setup<G> {
859 static TEST_SETUP: OnceLock<Setup<G>> = OnceLock::new();
860 TEST_SETUP.get_or_init(|| {
861 let generators = (1..=NUM_GENERATORS)
862 .map(|i| G::generator() * &F::from(i as u8))
863 .collect::<Vec<_>>();
864 Setup::new(
865 generators[0],
866 generators[1..]
867 .chunks_exact(2)
868 .map(|chunk| (chunk[0], chunk[1])),
869 )
870 })
871 }
872
873 struct Prover<'a> {
874 setup: &'a Setup<G>,
875 witness: Witness<F>,
876 claim: Claim<F, G>,
877 proof: Proof<F, G>,
878 bad_namespace: bool,
879 honest: bool,
880 }
881
882 impl<'a> Prover<'a> {
883 fn new(setup: &'a Setup<G>, y: F, a: &[F], b: &[F]) -> Self {
884 let (witness, claim) =
885 Witness::new_with_claim(setup, y, a.iter().zip(b).map(|(&a, &b)| (a, b)))
886 .expect("prover expects arguments to match setup");
887 let proof = prove(
888 &mut Transcript::new(NAMESPACE),
889 setup,
890 &claim,
891 witness.clone(),
892 &Sequential,
893 )
894 .expect("proving should work");
895 Self {
896 setup,
897 witness,
898 claim,
899 proof,
900 bad_namespace: false,
901 honest: true,
902 }
903 }
904
905 #[allow(clippy::missing_const_for_fn)]
906 fn bad_namespace(&mut self) {
907 self.honest = false;
908 self.bad_namespace = true;
909 }
910
911 fn tweak_product(&mut self, delta: F) {
912 if delta == F::zero() {
913 return;
914 }
915 self.honest = false;
916 // Normally, we compress the separate product by doing:
917 //
918 // c w Q + P
919 //
920 // but, if you know w in advance, you can do:
921 //
922 // (c + d) w Q + (P - d w Q)
923 //
924 // i.e. tweak your commitment to change the product, without changing
925 // the actual vectors that make up your witness.
926 //
927 // One simple case where you know w is if the implementor forgets to multiply
928 // the product generator by this challenge. (I made this mistake myself).
929 self.claim.product -= δ
930 self.claim.commitment += &(*self.setup.product_generator() * &delta);
931 self.proof = prove(
932 &mut Transcript::new(NAMESPACE),
933 self.setup,
934 &self.claim,
935 self.witness.clone(),
936 &Sequential,
937 )
938 .expect("proving should work after tweaking the public claim");
939 }
940
941 fn increase_length(&mut self) {
942 self.honest = false;
943 let longer_log_len = self
944 .claim
945 .log_len
946 .checked_add(1)
947 .expect("test vectors should support doubling the witness length");
948 let longer_len = 1usize
949 .checked_shl(u32::from(longer_log_len))
950 .expect("witness length should fit into usize");
951 self.witness.a.resize_with(longer_len, F::zero);
952 self.witness.b.resize_with(longer_len, F::zero);
953
954 // Padding with zeros preserves the commitment and product, but the
955 // regenerated proof is now bound to a different claimed length.
956 let longer_claim = Claim {
957 log_len: longer_log_len,
958 ..self.claim
959 };
960 self.proof = prove(
961 &mut Transcript::new(NAMESPACE),
962 self.setup,
963 &longer_claim,
964 self.witness.clone(),
965 &Sequential,
966 )
967 .expect("proving should work after increasing the witness length");
968 }
969
970 fn tweak_l_r_coms<'b>(&mut self, u: &mut Unstructured<'b>) -> arbitrary::Result<()> {
971 let Some(last_round) = self.proof.l_r_coms.len().checked_sub(1) else {
972 return Ok(());
973 };
974 let round = u.int_in_range(0..=last_round)?;
975 let tweak_left = u.arbitrary::<bool>()?;
976 let delta = u.arbitrary::<G>()?;
977 if delta == G::zero() {
978 return Ok(());
979 }
980
981 self.honest = false;
982 let (l, r) = &mut self.proof.l_r_coms[round];
983 if tweak_left {
984 *l += δ
985 } else {
986 *r += δ
987 }
988 Ok(())
989 }
990
991 #[allow(clippy::missing_const_for_fn)]
992 fn honest(&self) -> bool {
993 self.honest
994 }
995
996 fn verify(self) -> bool {
997 let ns = if self.bad_namespace {
998 BAD_NAMESPACE
999 } else {
1000 NAMESPACE
1001 };
1002 let setup = self.setup;
1003 let claim = self.claim;
1004 let proof = self.proof;
1005 setup
1006 .eval(
1007 |vs| super::verify(&mut Transcript::new(ns), vs, &claim, proof),
1008 &Sequential,
1009 )
1010 .map(|g| g == G::zero())
1011 .unwrap_or(false)
1012 }
1013 }
1014
1015 #[derive(Debug)]
1016 pub struct Plan {
1017 y: F,
1018 a: Vec<F>,
1019 b: Vec<F>,
1020 }
1021
1022 impl<'a> Arbitrary<'a> for Plan {
1023 fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
1024 let lg_len = u.int_in_range(0..=MAX_VECTOR_LG)?;
1025 let len = 1usize << lg_len;
1026 let y = u.arbitrary()?;
1027 let a = (0..len)
1028 .map(|_| u.arbitrary())
1029 .collect::<arbitrary::Result<Vec<_>>>()?;
1030 let b = (0..len)
1031 .map(|_| u.arbitrary())
1032 .collect::<arbitrary::Result<Vec<_>>>()?;
1033 Ok(Self { y, a, b })
1034 }
1035 }
1036
1037 impl Plan {
1038 pub fn run(self, u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
1039 let setup = test_setup();
1040 let mut prover = Prover::new(setup, self.y, &self.a, &self.b);
1041 // is the prover going to be malicious at all?
1042 if u.arbitrary::<bool>()? {
1043 match u.arbitrary::<u8>()? {
1044 x if x < 64 => prover.tweak_product(u.arbitrary::<F>()?),
1045 x if x < 128 => prover.increase_length(),
1046 x if x < 192 => prover.tweak_l_r_coms(u)?,
1047 _ => prover.bad_namespace(),
1048 }
1049 }
1050 match (prover.honest(), prover.verify()) {
1051 (true, true) | (false, false) => {}
1052 (true, false) => panic!("prover honest, but proof didn't verify"),
1053 (false, true) => panic!("prover malicious, but proof verifies!!!"),
1054 }
1055 Ok(())
1056 }
1057 }
1058
1059 #[cfg(test)]
1060 mod test {
1061 use super::*;
1062
1063 fn assert_setup_roundtrip(setup: &Setup<G>) {
1064 let encoded = setup.encode();
1065 let decoded: Setup<G> = Setup::decode_cfg(encoded.clone(), &(setup.g.len(), ()))
1066 .expect("setup should decode with its own length bound");
1067 assert_eq!(setup, &decoded);
1068 assert_eq!(decoded.encode(), encoded);
1069 }
1070
1071 fn assert_claim_roundtrip(claim: &Claim<F, G>) {
1072 let encoded = claim.encode();
1073 let decoded: Claim<F, G> = Claim::decode_cfg(encoded.clone(), &((), ()))
1074 .expect("claim should decode with unit cfg");
1075 assert_eq!(claim, &decoded);
1076 assert_eq!(decoded.encode(), encoded);
1077 }
1078
1079 fn assert_proof_roundtrip(proof: &Proof<F, G>) {
1080 let max_len = if proof.l_r_coms.is_empty() {
1081 0
1082 } else {
1083 1usize
1084 .checked_shl(proof.l_r_coms.len() as u32)
1085 .expect("proof arbitrary bounds rounds to fit in usize")
1086 };
1087 let encoded = proof.encode();
1088 let decoded: Proof<F, G> = Proof::decode_cfg(encoded.clone(), &(max_len, ((), ())))
1089 .expect("proof should decode with a matching round bound");
1090 assert_eq!(proof, &decoded);
1091 assert_eq!(decoded.encode(), encoded);
1092 }
1093
1094 #[test]
1095 fn test_codec_roundtrip() {
1096 commonware_invariants::minifuzz::test(|u| {
1097 assert_setup_roundtrip(&u.arbitrary::<Setup<G>>()?);
1098 assert_claim_roundtrip(&u.arbitrary::<Claim<F, G>>()?);
1099 assert_proof_roundtrip(&u.arbitrary::<Proof<F, G>>()?);
1100 Ok(())
1101 });
1102 }
1103
1104 #[test]
1105 fn test_fuzz() {
1106 commonware_invariants::minifuzz::test(|u| u.arbitrary::<Plan>()?.run(u));
1107 }
1108
1109 #[test]
1110 fn prover_tweaks_cover_edge_paths() {
1111 let setup = test_setup();
1112
1113 let mut honest = Prover::new(setup, F::from(1u8), &[F::from(3u8)], &[F::from(4u8)]);
1114 honest.tweak_product(F::zero());
1115 honest
1116 .tweak_l_r_coms(&mut Unstructured::new(&[]))
1117 .expect("single-round proof should no-op before reading fuzz input");
1118 assert!(honest.honest());
1119 assert!(honest.verify());
1120
1121 type Tweak = Box<dyn FnOnce(&mut Prover<'static>)>;
1122 let failures: [Tweak; 4] = [
1123 Box::new(|p| p.tweak_product(F::from(1u8))),
1124 Box::new(|p| p.increase_length()),
1125 Box::new(|p| {
1126 p.tweak_l_r_coms(&mut Unstructured::new(&[1, 1, 0, 0, 0, 0, 0, 0, 0]))
1127 .expect("structured fuzz input should mutate a proof round");
1128 }),
1129 Box::new(|p| p.bad_namespace()),
1130 ];
1131 for tweak in failures {
1132 let mut prover = Prover::new(
1133 setup,
1134 F::from(1u8),
1135 &[F::from(3u8), F::from(5u8)],
1136 &[F::from(4u8), F::from(6u8)],
1137 );
1138 tweak(&mut prover);
1139 assert!(!prover.honest());
1140 assert!(!prover.verify());
1141 }
1142 }
1143 }
1144}