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