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