monero_bulletproofs/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc = include_str!("../README.md")]
3#![deny(missing_docs)]
4#![cfg_attr(not(feature = "std"), no_std)]
5#![allow(non_snake_case)]
6
7use std_shims::{
8  vec,
9  vec::Vec,
10  io::{self, Read, Write},
11};
12
13use rand_core::{RngCore, CryptoRng};
14use zeroize::Zeroizing;
15
16use curve25519_dalek::edwards::CompressedEdwardsY;
17
18use monero_io::*;
19pub use monero_generators::MAX_COMMITMENTS;
20use monero_primitives::Commitment;
21
22pub(crate) mod scalar_vector;
23pub(crate) mod point_vector;
24
25pub(crate) mod core;
26use crate::core::LOG_COMMITMENT_BITS;
27
28pub(crate) mod batch_verifier;
29use batch_verifier::{BulletproofsBatchVerifier, BulletproofsPlusBatchVerifier};
30pub use batch_verifier::BatchVerifier;
31
32pub(crate) mod original;
33use crate::original::{
34  IpProof, AggregateRangeStatement as OriginalStatement, AggregateRangeWitness as OriginalWitness,
35  AggregateRangeProof as OriginalProof,
36};
37
38pub(crate) mod plus;
39use crate::plus::{
40  WipProof, AggregateRangeStatement as PlusStatement, AggregateRangeWitness as PlusWitness,
41  AggregateRangeProof as PlusProof,
42};
43
44#[cfg(test)]
45mod tests;
46
47/// An error from proving/verifying Bulletproofs(+).
48#[derive(Clone, Copy, PartialEq, Eq, Debug)]
49#[cfg_attr(feature = "std", derive(thiserror::Error))]
50pub enum BulletproofError {
51  /// Proving/verifying a Bulletproof(+) range proof with no commitments.
52  #[cfg_attr(feature = "std", error("no commitments to prove the range for"))]
53  NoCommitments,
54  /// Proving/verifying a Bulletproof(+) range proof with more commitments than supported.
55  #[cfg_attr(feature = "std", error("too many commitments to prove the range for"))]
56  TooManyCommitments,
57}
58
59/// A Bulletproof(+).
60///
61/// This encapsulates either a Bulletproof or a Bulletproof+.
62#[allow(clippy::large_enum_variant)]
63#[derive(Clone, PartialEq, Eq, Debug)]
64pub enum Bulletproof {
65  /// A Bulletproof.
66  Original(OriginalProof),
67  /// A Bulletproof+.
68  Plus(PlusProof),
69}
70
71impl Bulletproof {
72  fn bp_fields(plus: bool) -> usize {
73    if plus {
74      6
75    } else {
76      9
77    }
78  }
79
80  /// Calculate the weight penalty for the Bulletproof(+).
81  ///
82  /// Bulletproofs(+) are logarithmically sized yet linearly timed. Evaluating by their size alone
83  /// accordingly doesn't properly represent the burden of the proof. Monero 'claws back' some of
84  /// the weight lost by using a proof smaller than it is fast to compensate for this.
85  // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
86  //   src/cryptonote_basic/cryptonote_format_utils.cpp#L106-L124
87  pub fn calculate_bp_clawback(plus: bool, n_outputs: usize) -> (usize, usize) {
88    #[allow(non_snake_case)]
89    let mut LR_len = 0;
90    let mut n_padded_outputs = 1;
91    while n_padded_outputs < n_outputs {
92      LR_len += 1;
93      n_padded_outputs = 1 << LR_len;
94    }
95    LR_len += LOG_COMMITMENT_BITS;
96
97    let mut bp_clawback = 0;
98    if n_padded_outputs > 2 {
99      let fields = Bulletproof::bp_fields(plus);
100      let base = ((fields + (2 * (LOG_COMMITMENT_BITS + 1))) * 32) / 2;
101      let size = (fields + (2 * LR_len)) * 32;
102      bp_clawback = ((base * n_padded_outputs) - size) * 4 / 5;
103    }
104
105    (bp_clawback, LR_len)
106  }
107
108  /// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof.
109  pub fn prove<R: RngCore + CryptoRng>(
110    rng: &mut R,
111    outputs: Vec<Commitment>,
112  ) -> Result<Bulletproof, BulletproofError> {
113    if outputs.is_empty() {
114      Err(BulletproofError::NoCommitments)?;
115    }
116    if outputs.len() > MAX_COMMITMENTS {
117      Err(BulletproofError::TooManyCommitments)?;
118    }
119    let commitments = outputs.iter().map(Commitment::calculate).collect::<Vec<_>>();
120    Ok(Bulletproof::Original(
121      OriginalStatement::new(&commitments)
122        .unwrap()
123        .prove(rng, OriginalWitness::new(outputs).unwrap())
124        .unwrap(),
125    ))
126  }
127
128  /// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof+.
129  pub fn prove_plus<R: RngCore + CryptoRng>(
130    rng: &mut R,
131    outputs: Vec<Commitment>,
132  ) -> Result<Bulletproof, BulletproofError> {
133    if outputs.is_empty() {
134      Err(BulletproofError::NoCommitments)?;
135    }
136    if outputs.len() > MAX_COMMITMENTS {
137      Err(BulletproofError::TooManyCommitments)?;
138    }
139    let commitments = outputs.iter().map(Commitment::calculate).collect::<Vec<_>>();
140    Ok(Bulletproof::Plus(
141      PlusStatement::new(&commitments)
142        .unwrap()
143        .prove(rng, &Zeroizing::new(PlusWitness::new(outputs).unwrap()))
144        .unwrap(),
145    ))
146  }
147
148  /// Verify the given Bulletproof(+).
149  #[must_use]
150  pub fn verify<R: RngCore + CryptoRng>(
151    &self,
152    rng: &mut R,
153    commitments: &[CompressedEdwardsY],
154  ) -> bool {
155    let Some(commitments) =
156      commitments.iter().map(|p| decompress_point(*p)).collect::<Option<Vec<_>>>()
157    else {
158      return false;
159    };
160
161    match self {
162      Bulletproof::Original(bp) => {
163        let mut verifier = BulletproofsBatchVerifier::default();
164        let Some(statement) = OriginalStatement::new(&commitments) else {
165          return false;
166        };
167        if !statement.verify(rng, &mut verifier, bp.clone()) {
168          return false;
169        }
170        verifier.verify()
171      }
172      Bulletproof::Plus(bp) => {
173        let mut verifier = BulletproofsPlusBatchVerifier::default();
174        let Some(statement) = PlusStatement::new(&commitments) else {
175          return false;
176        };
177        if !statement.verify(rng, &mut verifier, bp.clone()) {
178          return false;
179        }
180        verifier.verify()
181      }
182    }
183  }
184
185  /// Accumulate the verification for the given Bulletproof(+) into the specified BatchVerifier.
186  ///
187  /// Returns false if the Bulletproof(+) isn't sane, leaving the BatchVerifier in an undefined
188  /// state.
189  ///
190  /// Returns true if the Bulletproof(+) is sane, regardless of its validity.
191  ///
192  /// The BatchVerifier must have its verification function executed to actually verify this proof.
193  #[must_use]
194  pub fn batch_verify<R: RngCore + CryptoRng>(
195    &self,
196    rng: &mut R,
197    verifier: &mut BatchVerifier,
198    commitments: &[CompressedEdwardsY],
199  ) -> bool {
200    let Some(commitments) =
201      commitments.iter().map(|p| decompress_point(*p)).collect::<Option<Vec<_>>>()
202    else {
203      return false;
204    };
205
206    match self {
207      Bulletproof::Original(bp) => {
208        let Some(statement) = OriginalStatement::new(&commitments) else {
209          return false;
210        };
211        statement.verify(rng, &mut verifier.original, bp.clone())
212      }
213      Bulletproof::Plus(bp) => {
214        let Some(statement) = PlusStatement::new(&commitments) else {
215          return false;
216        };
217        statement.verify(rng, &mut verifier.plus, bp.clone())
218      }
219    }
220  }
221
222  fn write_core<W: Write, F: Fn(&[CompressedEdwardsY], &mut W) -> io::Result<()>>(
223    &self,
224    w: &mut W,
225    specific_write_vec: F,
226  ) -> io::Result<()> {
227    match self {
228      Bulletproof::Original(bp) => {
229        write_compressed_point(&bp.A, w)?;
230        write_compressed_point(&bp.S, w)?;
231        write_compressed_point(&bp.T1, w)?;
232        write_compressed_point(&bp.T2, w)?;
233        write_scalar(&bp.tau_x, w)?;
234        write_scalar(&bp.mu, w)?;
235        specific_write_vec(&bp.ip.L, w)?;
236        specific_write_vec(&bp.ip.R, w)?;
237        write_scalar(&bp.ip.a, w)?;
238        write_scalar(&bp.ip.b, w)?;
239        write_scalar(&bp.t_hat, w)
240      }
241
242      Bulletproof::Plus(bp) => {
243        write_compressed_point(&bp.A, w)?;
244        write_compressed_point(&bp.wip.A, w)?;
245        write_compressed_point(&bp.wip.B, w)?;
246        write_scalar(&bp.wip.r_answer, w)?;
247        write_scalar(&bp.wip.s_answer, w)?;
248        write_scalar(&bp.wip.delta_answer, w)?;
249        specific_write_vec(&bp.wip.L, w)?;
250        specific_write_vec(&bp.wip.R, w)
251      }
252    }
253  }
254
255  /// Write a Bulletproof(+) for the message signed by a transaction's signature.
256  ///
257  /// This has a distinct encoding from the standard encoding.
258  pub fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
259    self.write_core(w, |points, w| write_raw_vec(write_compressed_point, points, w))
260  }
261
262  /// Write a Bulletproof(+).
263  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
264    self.write_core(w, |points, w| write_vec(write_compressed_point, points, w))
265  }
266
267  /// Serialize a Bulletproof(+) to a `Vec<u8>`.
268  pub fn serialize(&self) -> Vec<u8> {
269    let mut serialized = vec![];
270    self.write(&mut serialized).unwrap();
271    serialized
272  }
273
274  /// Read a Bulletproof.
275  pub fn read<R: Read>(r: &mut R) -> io::Result<Bulletproof> {
276    Ok(Bulletproof::Original(OriginalProof {
277      A: read_compressed_point(r)?,
278      S: read_compressed_point(r)?,
279      T1: read_compressed_point(r)?,
280      T2: read_compressed_point(r)?,
281      tau_x: read_scalar(r)?,
282      mu: read_scalar(r)?,
283      ip: IpProof {
284        L: read_vec(read_compressed_point, r)?,
285        R: read_vec(read_compressed_point, r)?,
286        a: read_scalar(r)?,
287        b: read_scalar(r)?,
288      },
289      t_hat: read_scalar(r)?,
290    }))
291  }
292
293  /// Read a Bulletproof+.
294  pub fn read_plus<R: Read>(r: &mut R) -> io::Result<Bulletproof> {
295    Ok(Bulletproof::Plus(PlusProof {
296      A: read_compressed_point(r)?,
297      wip: WipProof {
298        A: read_compressed_point(r)?,
299        B: read_compressed_point(r)?,
300        r_answer: read_scalar(r)?,
301        s_answer: read_scalar(r)?,
302        delta_answer: read_scalar(r)?,
303        L: read_vec(read_compressed_point, r)?,
304        R: read_vec(read_compressed_point, r)?,
305      },
306    }))
307  }
308}