monero_bulletproofs/plus/
aggregate_range_proof.rs

1use std_shims::{vec, vec::Vec};
2
3use rand_core::{RngCore, CryptoRng};
4use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
5
6use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint};
7
8use monero_io::CompressedPoint;
9use monero_primitives::{INV_EIGHT, Commitment, keccak256_to_scalar};
10
11use crate::{
12  batch_verifier::BulletproofsPlusBatchVerifier,
13  core::{MAX_COMMITMENTS, COMMITMENT_BITS, multiexp, multiexp_vartime},
14  plus::{
15    ScalarVector, PointVector, GeneratorsList, BpPlusGenerators,
16    transcript::*,
17    weighted_inner_product::{WipStatement, WipWitness, WipProof},
18    padded_pow_of_2, u64_decompose,
19  },
20};
21
22// Figure 3 of the Bulletproofs+ Paper
23#[derive(Clone, Debug)]
24pub(crate) struct AggregateRangeStatement<'a> {
25  generators: BpPlusGenerators,
26  V: &'a [EdwardsPoint],
27}
28
29#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
30pub(crate) struct AggregateRangeWitness(Vec<Commitment>);
31
32impl AggregateRangeWitness {
33  pub(crate) fn new(commitments: Vec<Commitment>) -> Option<Self> {
34    if commitments.is_empty() || (commitments.len() > MAX_COMMITMENTS) {
35      return None;
36    }
37
38    Some(AggregateRangeWitness(commitments))
39  }
40}
41
42/// Internal structure representing a Bulletproof+, as defined by Monero..
43#[doc(hidden)]
44#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
45pub struct AggregateRangeProof {
46  pub(crate) A: CompressedPoint,
47  pub(crate) wip: WipProof,
48}
49
50struct AHatComputation {
51  y: Scalar,
52  d_descending_y_plus_z: ScalarVector,
53  y_mn_plus_one: Scalar,
54  z: Scalar,
55  z_pow: ScalarVector,
56  A_hat: EdwardsPoint,
57}
58
59impl<'a> AggregateRangeStatement<'a> {
60  pub(crate) fn new(V: &'a [EdwardsPoint]) -> Option<Self> {
61    if V.is_empty() || (V.len() > MAX_COMMITMENTS) {
62      return None;
63    }
64
65    Some(Self { generators: BpPlusGenerators::new(), V })
66  }
67
68  fn transcript_A(transcript: &mut Scalar, A: CompressedPoint) -> (Scalar, Scalar) {
69    let y = keccak256_to_scalar([transcript.to_bytes(), A.to_bytes()].concat());
70    let z = keccak256_to_scalar(y.to_bytes());
71    *transcript = z;
72    (y, z)
73  }
74
75  fn d_j(j: usize, m: usize) -> ScalarVector {
76    let mut d_j = Vec::with_capacity(m * COMMITMENT_BITS);
77    for _ in 0 .. (j - 1) * COMMITMENT_BITS {
78      d_j.push(Scalar::ZERO);
79    }
80    d_j.append(&mut ScalarVector::powers(Scalar::from(2u8), COMMITMENT_BITS).0);
81    for _ in 0 .. (m - j) * COMMITMENT_BITS {
82      d_j.push(Scalar::ZERO);
83    }
84    ScalarVector(d_j)
85  }
86
87  fn compute_A_hat(
88    mut V: PointVector,
89    generators: &BpPlusGenerators,
90    transcript: &mut Scalar,
91    A: CompressedPoint,
92  ) -> Option<AHatComputation> {
93    let (y, z) = Self::transcript_A(transcript, A);
94
95    let A = A.decompress().as_ref().map(EdwardsPoint::mul_by_cofactor)?;
96
97    while V.len() < padded_pow_of_2(V.len()) {
98      V.0.push(EdwardsPoint::identity());
99    }
100    let mn = V.len() * COMMITMENT_BITS;
101
102    // 2, 4, 6, 8... powers of z, of length equivalent to the amount of commitments
103    let mut z_pow = Vec::with_capacity(V.len());
104    // z**2
105    z_pow.push(z * z);
106
107    let mut d = ScalarVector::new(mn);
108    for j in 1 ..= V.len() {
109      z_pow.push(
110        *z_pow.last().expect("couldn't get last z_pow despite always being non-empty") * z_pow[0],
111      );
112      d = d + &(Self::d_j(j, V.len()) * (z_pow[j - 1]));
113    }
114
115    let mut ascending_y = ScalarVector(vec![y]);
116    for i in 1 .. d.len() {
117      ascending_y.0.push(ascending_y[i - 1] * y);
118    }
119    let y_pows = ascending_y.clone().sum();
120
121    let mut descending_y = ascending_y.clone();
122    descending_y.0.reverse();
123
124    let d_descending_y = d.clone() * &descending_y;
125    let d_descending_y_plus_z = d_descending_y + z;
126
127    let y_mn_plus_one = descending_y[0] * y;
128
129    let mut commitment_accum = EdwardsPoint::identity();
130    for (j, commitment) in V.0.iter().enumerate() {
131      commitment_accum += *commitment * z_pow[j];
132    }
133
134    let neg_z = -z;
135    let mut A_terms = Vec::with_capacity((generators.len() * 2) + 2);
136    for (i, d_y_z) in d_descending_y_plus_z.0.iter().enumerate() {
137      A_terms.push((neg_z, generators.generator(GeneratorsList::GBold, i)));
138      A_terms.push((*d_y_z, generators.generator(GeneratorsList::HBold, i)));
139    }
140    A_terms.push((y_mn_plus_one, commitment_accum));
141    A_terms.push((
142      ((y_pows * z) - (d.sum() * y_mn_plus_one * z) - (y_pows * (z * z))),
143      BpPlusGenerators::g(),
144    ));
145
146    Some(AHatComputation {
147      y,
148      d_descending_y_plus_z,
149      y_mn_plus_one,
150      z,
151      z_pow: ScalarVector(z_pow),
152      A_hat: A + multiexp_vartime(&A_terms),
153    })
154  }
155
156  pub(crate) fn prove<R: RngCore + CryptoRng>(
157    self,
158    rng: &mut R,
159    witness: &AggregateRangeWitness,
160  ) -> Option<AggregateRangeProof> {
161    // Check for consistency with the witness
162    if self.V.len() != witness.0.len() {
163      return None;
164    }
165    for (commitment, witness) in self.V.iter().zip(witness.0.iter()) {
166      if witness.calculate() != *commitment {
167        return None;
168      }
169    }
170
171    let Self { generators, V } = self;
172    // Monero expects all of these points to be torsion-free
173    // Generally, for Bulletproofs, it sends points * INV_EIGHT and then performs a torsion clear
174    // by multiplying by 8
175    // This also restores the original value due to the preprocessing
176    // Commitments aren't transmitted INV_EIGHT though, so this multiplies by INV_EIGHT to enable
177    // clearing its cofactor without mutating the value
178    // For some reason, these values are transcripted * INV_EIGHT, not as transmitted
179    let V = V.iter().map(|V| V * INV_EIGHT()).collect::<Vec<_>>();
180    let mut transcript = initial_transcript(V.iter());
181    let mut V = V.iter().map(EdwardsPoint::mul_by_cofactor).collect::<Vec<_>>();
182
183    // Pad V
184    while V.len() < padded_pow_of_2(V.len()) {
185      V.push(EdwardsPoint::identity());
186    }
187
188    let generators = generators.reduce(V.len() * COMMITMENT_BITS);
189
190    let mut d_js = Vec::with_capacity(V.len());
191    let mut a_l = ScalarVector(Vec::with_capacity(V.len() * COMMITMENT_BITS));
192    for j in 1 ..= V.len() {
193      d_js.push(Self::d_j(j, V.len()));
194      #[allow(clippy::map_unwrap_or)]
195      a_l.0.append(
196        &mut u64_decompose(
197          *witness.0.get(j - 1).map(|commitment| &commitment.amount).unwrap_or(&0),
198        )
199        .0,
200      );
201    }
202
203    let a_r = a_l.clone() - Scalar::ONE;
204
205    let alpha = Scalar::random(&mut *rng);
206
207    let mut A_terms = Vec::with_capacity((generators.len() * 2) + 1);
208    for (i, a_l) in a_l.0.iter().enumerate() {
209      A_terms.push((*a_l, generators.generator(GeneratorsList::GBold, i)));
210    }
211    for (i, a_r) in a_r.0.iter().enumerate() {
212      A_terms.push((*a_r, generators.generator(GeneratorsList::HBold, i)));
213    }
214    A_terms.push((alpha, BpPlusGenerators::h()));
215    let mut A = multiexp(&A_terms);
216    A_terms.zeroize();
217
218    // Multiply by INV_EIGHT per earlier commentary
219    A *= INV_EIGHT();
220
221    let A = CompressedPoint::from(A.compress());
222
223    let AHatComputation { y, d_descending_y_plus_z, y_mn_plus_one, z, z_pow, A_hat } =
224      Self::compute_A_hat(PointVector(V), &generators, &mut transcript, A)
225        .expect("A is a valid point as we just compressed it");
226
227    let a_l = a_l - z;
228    let a_r = a_r + &d_descending_y_plus_z;
229    let mut alpha = alpha;
230    for j in 1 ..= witness.0.len() {
231      alpha += z_pow[j - 1] * witness.0[j - 1].mask * y_mn_plus_one;
232    }
233
234    Some(AggregateRangeProof {
235      A,
236      wip: WipStatement::new(generators, A_hat, y)
237        .prove(
238          rng,
239          transcript,
240          &Zeroizing::new(
241            WipWitness::new(a_l, a_r, alpha)
242              .expect("Bulletproofs::Plus created an invalid WipWitness"),
243          ),
244        )
245        .expect("Bulletproof::Plus failed to prove the weighted inner-product"),
246    })
247  }
248
249  pub(crate) fn verify<R: RngCore + CryptoRng>(
250    self,
251    rng: &mut R,
252    verifier: &mut BulletproofsPlusBatchVerifier,
253    proof: AggregateRangeProof,
254  ) -> bool {
255    let Self { generators, V } = self;
256
257    let V = V.iter().map(|V| V * INV_EIGHT()).collect::<Vec<_>>();
258    let mut transcript = initial_transcript(V.iter());
259    let V = V.iter().map(EdwardsPoint::mul_by_cofactor).collect::<Vec<_>>();
260
261    let generators = generators.reduce(V.len() * COMMITMENT_BITS);
262
263    let Some(AHatComputation { y, A_hat, .. }) =
264      Self::compute_A_hat(PointVector(V), &generators, &mut transcript, proof.A)
265    else {
266      return false;
267    };
268    WipStatement::new(generators, A_hat, y).verify(rng, verifier, transcript, proof.wip)
269  }
270}