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