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(&mut 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_mut(mid);
609 let (h_lo, h_hi) = h.split_at_mut(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(&mut 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 for (g_lo_i, g_hi_i) in g_lo.iter_mut().zip(g_hi.iter_mut()) {
635 *g_lo_i *= &u_inv;
636 *g_lo_i += &(g_hi_i.clone() * &u);
637 }
638 g.truncate(mid);
639
640 for (h_lo_i, h_hi_i) in h_lo.iter_mut().zip(h_hi.iter_mut()) {
641 *h_lo_i *= &u;
642 *h_lo_i += &(h_hi_i.clone() * &u_inv);
643 }
644 h.truncate(mid);
645 }
646 let a_final = a.pop().expect("a should not be empty");
647 let b_final = b.pop().expect("b should not be empty");
648 Some(Proof {
649 l_r_coms,
650 transcript_summary: transcript.summarize(),
651 a_final,
652 b_final,
653 })
654}
655
656/// Construct the verification equation for a [`Proof`], relative to a
657/// [`Claim`] and a virtual [`Setup`].
658///
659/// If the check succeeds, we are convinced that the prover knows a valid
660/// [`Witness`] to this particular [`Claim`].
661///
662/// The returned [`Synthetic`] should evaluate to zero for a correct proof.
663/// Use [`Setup::eval`] to create the virtual setup and evaluate the result.
664///
665/// The return will be `None` if the proof is incorrect in an obvious way.
666pub fn verify<F: Field + Random, G: CryptoGroup<Scalar = F> + Encode>(
667 transcript: &mut Transcript,
668 setup: &Setup<Synthetic<F, G>>,
669 claim: &Claim<F, G>,
670 proof: Proof<F, G>,
671) -> Option<Synthetic<F, G>>
672where
673 Claim<F, G>: Encode,
674{
675 // See the prove function for some more explanation of the math.
676 // If you read that function's documentation naively, you might come under
677 // the impression that we have to naively follow the prover, folding the
678 // generators at each step, in order to produce the final value P_0, which
679 // we can then use to check that final a_0 and b_0. This is not ideal,
680 // because it's more efficient to do scalar multiplications as a batch, using
681 // an MSM. Our goal will thus be to reduce all of our work to hashing, in order
682 // to get the challenges, and a single large MSM.
683 //
684 // The final check we have is:
685 //
686 // P_0 =? a_0 G_0 + b_0 H_0 + a_0 b_0 w Q
687 //
688 // What is P_0? Well, it must be equal to:
689 //
690 // P_1 - u_1^2 L_1 - u_1^-2 R_1
691 //
692 // we can unravel P_1, and so, on, to get:
693 //
694 // P_0 = P + c w Q - <u_k^2, L_k> - <u_k^-2, R_k>
695 //
696 // and that's nice and ready for an MSM. The issue is now how to figure out
697 // G_0 and H_0. Intuitively, this should be possible to do as a large MSM
698 // of the original G_i and H_i. This is because each folding step is just a linear
699 // transformation of the prior vectors. Composing these will still result in
700 // a linear transformation. We just need to figure out the weights for this.
701 //
702 // For vectors of size 1, this is trivial, the weights are just 1.
703 //
704 // Let's say we've figured out the weights for G_(k-1), what should the weights
705 // for G_k be? We want:
706 //
707 // <g_(k-1)_j, G_(k-1)_j> = <g_k_i, G_k_i>
708 //
709 // i.e. the weights we want should produce the same result as folding, and then
710 // using the weights we know exist by induction. (If this is not easy to understand,
711 // imagine that the next layer beneath us is just the trivial layer, with one element,
712 // and a single weight equal to 1).
713 //
714 // We can expand the result of folding, to get:
715 //
716 // <g_(k-1)_j, u_k^-1 G_k_j + u_k G_k_(mid + j)>
717 //
718 // but, this gives us the weights we need, defining:
719 //
720 // g_k_i := u_k^{if i < mid { -1 } else { 1 }} g_(k - 1)_(i % mid)
721 //
722 // Another way of visualizing what's happening here: at each iteration, as
723 // we double the size of the weights, what we're doing is copying the existing
724 // weights, and then multiplying the left side by u_k^-1, and the right side
725 // by u_k.
726 //
727 // Here's an example progression:
728 //
729 // 1
730 //
731 // 1, 1
732 // u_1^-1, u_1
733 //
734 // u_1^-1, u_1, u_1^-1, u_1
735 // u_1^-1 u_2^-1, u_1 u_2^-1, u_1^-1 u_2, u_1 u_2
736 //
737 // Now, we don't actually need to do anything special for H, because it turns
738 // out that the weights we need are just the ones we've calculated for G, just
739 // in reverse order! To see why, note that the only difference with H is that
740 // we need to use u_k on the left, and u_k^-1 on the right. The vector we
741 // have at each step is the result of copying the previous vector, doubling its size,
742 // and then multiplying with one value and the left, and the other on the right.
743 // If we reverse this vector, the result we get is the same as if we had reversed
744 // the previous step's vector, copied it, and then multiplied with u_k on the left,
745 // and u_k^-1 on the right, which is exactly what we need to do.
746 //
747 // Recall also that the prover starts by turning H_i -> H'_i = y^i H_i. We can
748 // accomplish this by multiplying our weights for those values by y^i as well.
749 let rounds = usize::from(claim.log_len);
750 let claimed_len = 1usize.checked_shl(u32::from(claim.log_len))?;
751 let Proof {
752 l_r_coms,
753 transcript_summary,
754 a_final,
755 b_final,
756 } = proof;
757 if l_r_coms.len() != rounds {
758 return None;
759 }
760 transcript.commit(claim.encode());
761
762 let w = F::random(&mut transcript.noise(b"w challenge"));
763
764 // We reduce verification down to one MSM which needs to equal 0:
765 // commitment + product * U + sum(u_i^2 * L_i + u_i^-2 * R_i)
766 // - a_final * g_final - b_final * h_final - a_final * b_final * U = 0.
767 let mut us = Vec::<(F, F)>::with_capacity(rounds);
768 let mut out = Synthetic::concrete(l_r_coms.into_iter().flat_map(|(l, r)| {
769 transcript.commit(l.encode());
770 transcript.commit(r.encode());
771 let u = F::random(transcript.noise(b"u challenge"));
772 let u_inv = u.inv();
773 us.push((u.clone(), u_inv.clone()));
774 let u2 = {
775 let mut out = u;
776 out.square();
777 out
778 };
779 let u_inv2 = {
780 let mut out = u_inv;
781 out.square();
782 out
783 };
784 [(u2, l), (u_inv2, r)]
785 }));
786 if transcript.summarize() != transcript_summary {
787 return None;
788 }
789 out += &Synthetic::concrete([(F::one(), claim.commitment.clone())]);
790 out += &(setup.product_generator().clone() * &(claim.product.clone() * &w));
791 let g_weights = {
792 let mut weights = Vec::<F>::with_capacity(claimed_len);
793 weights.push(F::one());
794 for (u, u_inv) in us.into_iter().rev() {
795 let end = weights.len();
796 weights.extend_from_within(..);
797 for left_i in &mut weights[..end] {
798 *left_i *= &u_inv;
799 }
800 for right_i in &mut weights[end..] {
801 *right_i *= &u;
802 }
803 }
804 weights
805 };
806 let h_weights: Vec<F> = g_weights
807 .iter()
808 .rev()
809 .zip(powers(F::one(), &claim.y))
810 .map(|(w_i, y_i)| y_i * w_i)
811 .collect();
812 let g = &setup.g()[..claimed_len];
813 let h = &setup.h()[..claimed_len];
814 out -= &(Synthetic::msm(h, &h_weights, &Sequential) * &b_final);
815 out -= &(Synthetic::msm(g, &g_weights, &Sequential) * &a_final);
816 out -= &(setup.product_generator().clone() * &(a_final * &b_final * &w));
817 Some(out)
818}
819
820#[cfg(all(test, feature = "arbitrary"))]
821mod conformance {
822 use super::{Claim, Proof, Setup};
823 use commonware_codec::conformance::CodecConformance;
824 use commonware_math::test::{F as TestF, G as TestG};
825
826 commonware_conformance::conformance_tests! {
827 CodecConformance<Setup<TestG>>,
828 CodecConformance<Claim<TestF, TestG>>,
829 CodecConformance<Proof<TestF, TestG>>,
830 }
831}
832
833#[commonware_macros::stability(ALPHA)]
834#[cfg(any(test, feature = "fuzz"))]
835pub mod fuzz {
836 use super::*;
837 use arbitrary::{Arbitrary, Unstructured};
838 #[cfg(test)]
839 use commonware_codec::Decode;
840 use commonware_math::{
841 algebra::Additive,
842 test::{F, G},
843 };
844 use commonware_parallel::Sequential;
845 use std::sync::OnceLock;
846
847 const MAX_VECTOR_LG: u8 = 5;
848 const MAX_VECTOR_LEN: usize = 1 << MAX_VECTOR_LG;
849 const MAX_SETUP_VECTOR_LEN: usize = 2 * MAX_VECTOR_LEN;
850 const NUM_GENERATORS: usize = 2 * MAX_SETUP_VECTOR_LEN + 1;
851 const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_ZK_BULLETPROOFS_IPA";
852 const BAD_NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_ZK_BULLETPROOFS_IPA_BUT_DIFFERENT";
853
854 fn test_setup() -> &'static Setup<G> {
855 static TEST_SETUP: OnceLock<Setup<G>> = OnceLock::new();
856 TEST_SETUP.get_or_init(|| {
857 let generators = (1..=NUM_GENERATORS)
858 .map(|i| G::generator() * &F::from(i as u8))
859 .collect::<Vec<_>>();
860 Setup::new(
861 generators[0],
862 generators[1..]
863 .chunks_exact(2)
864 .map(|chunk| (chunk[0], chunk[1])),
865 )
866 })
867 }
868
869 struct Prover<'a> {
870 setup: &'a Setup<G>,
871 witness: Witness<F>,
872 claim: Claim<F, G>,
873 proof: Proof<F, G>,
874 bad_namespace: bool,
875 honest: bool,
876 }
877
878 impl<'a> Prover<'a> {
879 fn new(setup: &'a Setup<G>, y: F, a: &[F], b: &[F]) -> Self {
880 let (witness, claim) =
881 Witness::new_with_claim(setup, y, a.iter().zip(b).map(|(&a, &b)| (a, b)))
882 .expect("prover expects arguments to match setup");
883 let proof = prove(
884 &mut Transcript::new(NAMESPACE),
885 setup,
886 &claim,
887 witness.clone(),
888 &Sequential,
889 )
890 .expect("proving should work");
891 Self {
892 setup,
893 witness,
894 claim,
895 proof,
896 bad_namespace: false,
897 honest: true,
898 }
899 }
900
901 #[allow(clippy::missing_const_for_fn)]
902 fn bad_namespace(&mut self) {
903 self.honest = false;
904 self.bad_namespace = true;
905 }
906
907 fn tweak_product(&mut self, delta: F) {
908 if delta == F::zero() {
909 return;
910 }
911 self.honest = false;
912 // Normally, we compress the separate product by doing:
913 //
914 // c w Q + P
915 //
916 // but, if you know w in advance, you can do:
917 //
918 // (c + d) w Q + (P - d w Q)
919 //
920 // i.e. tweak your commitment to change the product, without changing
921 // the actual vectors that make up your witness.
922 //
923 // One simple case where you know w is if the implementor forgets to multiply
924 // the product generator by this challenge. (I made this mistake myself).
925 self.claim.product -= δ
926 self.claim.commitment += &(*self.setup.product_generator() * &delta);
927 self.proof = prove(
928 &mut Transcript::new(NAMESPACE),
929 self.setup,
930 &self.claim,
931 self.witness.clone(),
932 &Sequential,
933 )
934 .expect("proving should work after tweaking the public claim");
935 }
936
937 fn increase_length(&mut self) {
938 self.honest = false;
939 let longer_log_len = self
940 .claim
941 .log_len
942 .checked_add(1)
943 .expect("test vectors should support doubling the witness length");
944 let longer_len = 1usize
945 .checked_shl(u32::from(longer_log_len))
946 .expect("witness length should fit into usize");
947 self.witness.a.resize_with(longer_len, F::zero);
948 self.witness.b.resize_with(longer_len, F::zero);
949
950 // Padding with zeros preserves the commitment and product, but the
951 // regenerated proof is now bound to a different claimed length.
952 let longer_claim = Claim {
953 log_len: longer_log_len,
954 ..self.claim
955 };
956 self.proof = prove(
957 &mut Transcript::new(NAMESPACE),
958 self.setup,
959 &longer_claim,
960 self.witness.clone(),
961 &Sequential,
962 )
963 .expect("proving should work after increasing the witness length");
964 }
965
966 fn tweak_l_r_coms<'b>(&mut self, u: &mut Unstructured<'b>) -> arbitrary::Result<()> {
967 let Some(last_round) = self.proof.l_r_coms.len().checked_sub(1) else {
968 return Ok(());
969 };
970 let round = u.int_in_range(0..=last_round)?;
971 let tweak_left = u.arbitrary::<bool>()?;
972 let delta = u.arbitrary::<G>()?;
973 if delta == G::zero() {
974 return Ok(());
975 }
976
977 self.honest = false;
978 let (l, r) = &mut self.proof.l_r_coms[round];
979 if tweak_left {
980 *l += δ
981 } else {
982 *r += δ
983 }
984 Ok(())
985 }
986
987 #[allow(clippy::missing_const_for_fn)]
988 fn honest(&self) -> bool {
989 self.honest
990 }
991
992 fn verify(self) -> bool {
993 let ns = if self.bad_namespace {
994 BAD_NAMESPACE
995 } else {
996 NAMESPACE
997 };
998 let setup = self.setup;
999 let claim = self.claim;
1000 let proof = self.proof;
1001 setup
1002 .eval(
1003 |vs| super::verify(&mut Transcript::new(ns), vs, &claim, proof),
1004 &Sequential,
1005 )
1006 .map(|g| g == G::zero())
1007 .unwrap_or(false)
1008 }
1009 }
1010
1011 #[derive(Debug)]
1012 pub struct Plan {
1013 y: F,
1014 a: Vec<F>,
1015 b: Vec<F>,
1016 }
1017
1018 impl<'a> Arbitrary<'a> for Plan {
1019 fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
1020 let lg_len = u.int_in_range(0..=MAX_VECTOR_LG)?;
1021 let len = 1usize << lg_len;
1022 let y = u.arbitrary()?;
1023 let a = (0..len)
1024 .map(|_| u.arbitrary())
1025 .collect::<arbitrary::Result<Vec<_>>>()?;
1026 let b = (0..len)
1027 .map(|_| u.arbitrary())
1028 .collect::<arbitrary::Result<Vec<_>>>()?;
1029 Ok(Self { y, a, b })
1030 }
1031 }
1032
1033 impl Plan {
1034 pub fn run(self, u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
1035 let setup = test_setup();
1036 let mut prover = Prover::new(setup, self.y, &self.a, &self.b);
1037 // is the prover going to be malicious at all?
1038 if u.arbitrary::<bool>()? {
1039 match u.arbitrary::<u8>()? {
1040 x if x < 64 => prover.tweak_product(u.arbitrary::<F>()?),
1041 x if x < 128 => prover.increase_length(),
1042 x if x < 192 => prover.tweak_l_r_coms(u)?,
1043 _ => prover.bad_namespace(),
1044 }
1045 }
1046 match (prover.honest(), prover.verify()) {
1047 (true, true) | (false, false) => {}
1048 (true, false) => panic!("prover honest, but proof didn't verify"),
1049 (false, true) => panic!("prover malicious, but proof verifies!!!"),
1050 }
1051 Ok(())
1052 }
1053 }
1054
1055 #[cfg(test)]
1056 mod test {
1057 use super::*;
1058
1059 fn assert_setup_roundtrip(setup: &Setup<G>) {
1060 let encoded = setup.encode();
1061 let decoded: Setup<G> = Setup::decode_cfg(encoded.clone(), &(setup.g.len(), ()))
1062 .expect("setup should decode with its own length bound");
1063 assert_eq!(setup, &decoded);
1064 assert_eq!(decoded.encode(), encoded);
1065 }
1066
1067 fn assert_claim_roundtrip(claim: &Claim<F, G>) {
1068 let encoded = claim.encode();
1069 let decoded: Claim<F, G> = Claim::decode_cfg(encoded.clone(), &((), ()))
1070 .expect("claim should decode with unit cfg");
1071 assert_eq!(claim, &decoded);
1072 assert_eq!(decoded.encode(), encoded);
1073 }
1074
1075 fn assert_proof_roundtrip(proof: &Proof<F, G>) {
1076 let max_len = if proof.l_r_coms.is_empty() {
1077 0
1078 } else {
1079 1usize
1080 .checked_shl(proof.l_r_coms.len() as u32)
1081 .expect("proof arbitrary bounds rounds to fit in usize")
1082 };
1083 let encoded = proof.encode();
1084 let decoded: Proof<F, G> = Proof::decode_cfg(encoded.clone(), &(max_len, ((), ())))
1085 .expect("proof should decode with a matching round bound");
1086 assert_eq!(proof, &decoded);
1087 assert_eq!(decoded.encode(), encoded);
1088 }
1089
1090 #[test]
1091 fn test_codec_roundtrip() {
1092 commonware_invariants::minifuzz::test(|u| {
1093 assert_setup_roundtrip(&u.arbitrary::<Setup<G>>()?);
1094 assert_claim_roundtrip(&u.arbitrary::<Claim<F, G>>()?);
1095 assert_proof_roundtrip(&u.arbitrary::<Proof<F, G>>()?);
1096 Ok(())
1097 });
1098 }
1099
1100 #[test]
1101 fn test_fuzz() {
1102 commonware_invariants::minifuzz::test(|u| u.arbitrary::<Plan>()?.run(u));
1103 }
1104
1105 #[test]
1106 fn prover_tweaks_cover_edge_paths() {
1107 let setup = test_setup();
1108
1109 let mut honest = Prover::new(setup, F::from(1u8), &[F::from(3u8)], &[F::from(4u8)]);
1110 honest.tweak_product(F::zero());
1111 honest
1112 .tweak_l_r_coms(&mut Unstructured::new(&[]))
1113 .expect("single-round proof should no-op before reading fuzz input");
1114 assert!(honest.honest());
1115 assert!(honest.verify());
1116
1117 type Tweak = Box<dyn FnOnce(&mut Prover<'static>)>;
1118 let failures: [Tweak; 4] = [
1119 Box::new(|p| p.tweak_product(F::from(1u8))),
1120 Box::new(|p| p.increase_length()),
1121 Box::new(|p| {
1122 p.tweak_l_r_coms(&mut Unstructured::new(&[1, 1, 0, 0, 0, 0, 0, 0, 0]))
1123 .expect("structured fuzz input should mutate a proof round");
1124 }),
1125 Box::new(|p| p.bad_namespace()),
1126 ];
1127 for tweak in failures {
1128 let mut prover = Prover::new(
1129 setup,
1130 F::from(1u8),
1131 &[F::from(3u8), F::from(5u8)],
1132 &[F::from(4u8), F::from(6u8)],
1133 );
1134 tweak(&mut prover);
1135 assert!(!prover.honest());
1136 assert!(!prover.verify());
1137 }
1138 }
1139 }
1140}