monero_borromean/
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 core::fmt::Debug;
8use std_shims::{
9  io::{self, Read, Write},
10  vec::Vec,
11};
12
13use zeroize::Zeroize;
14
15use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint, edwards::CompressedEdwardsY};
16
17use monero_io::*;
18use monero_generators::H_pow_2;
19use monero_primitives::{keccak256_to_scalar, UnreducedScalar};
20
21// 64 Borromean ring signatures, as needed for a 64-bit range proof.
22//
23// s0 and s1 are stored as `UnreducedScalar`s due to Monero not requiring they were reduced.
24// `UnreducedScalar` preserves their original byte encoding and implements a custom reduction
25// algorithm which was in use.
26#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
27struct BorromeanSignatures {
28  s0: [UnreducedScalar; 64],
29  s1: [UnreducedScalar; 64],
30  ee: Scalar,
31}
32
33impl BorromeanSignatures {
34  // Read a set of BorromeanSignatures.
35  fn read<R: Read>(r: &mut R) -> io::Result<BorromeanSignatures> {
36    Ok(BorromeanSignatures {
37      s0: read_array(UnreducedScalar::read, r)?,
38      s1: read_array(UnreducedScalar::read, r)?,
39      ee: read_scalar(r)?,
40    })
41  }
42
43  // Write the set of BorromeanSignatures.
44  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
45    for s0 in &self.s0 {
46      s0.write(w)?;
47    }
48    for s1 in &self.s1 {
49      s1.write(w)?;
50    }
51    write_scalar(&self.ee, w)
52  }
53
54  fn verify(&self, keys_a: &[EdwardsPoint], keys_b: &[EdwardsPoint]) -> bool {
55    let mut transcript = [0; 2048];
56
57    for i in 0 .. 64 {
58      #[allow(non_snake_case)]
59      let LL = EdwardsPoint::vartime_double_scalar_mul_basepoint(
60        &self.ee,
61        &keys_a[i],
62        &self.s0[i].recover_monero_slide_scalar(),
63      );
64      #[allow(non_snake_case)]
65      let LV = EdwardsPoint::vartime_double_scalar_mul_basepoint(
66        &keccak256_to_scalar(LL.compress().as_bytes()),
67        &keys_b[i],
68        &self.s1[i].recover_monero_slide_scalar(),
69      );
70      transcript[(i * 32) .. ((i + 1) * 32)].copy_from_slice(LV.compress().as_bytes());
71    }
72
73    keccak256_to_scalar(transcript) == self.ee
74  }
75}
76
77/// A range proof premised on Borromean ring signatures.
78#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
79pub struct BorromeanRange {
80  sigs: BorromeanSignatures,
81  bit_commitments: [CompressedEdwardsY; 64],
82}
83
84impl BorromeanRange {
85  /// Read a BorromeanRange proof.
86  pub fn read<R: Read>(r: &mut R) -> io::Result<BorromeanRange> {
87    Ok(BorromeanRange {
88      sigs: BorromeanSignatures::read(r)?,
89      bit_commitments: read_array(read_compressed_point, r)?,
90    })
91  }
92
93  /// Write the BorromeanRange proof.
94  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
95    self.sigs.write(w)?;
96    write_raw_vec(|p, w| w.write_all(&p.0), &self.bit_commitments, w)
97  }
98
99  /// Verify the commitment contains a 64-bit value.
100  #[must_use]
101  pub fn verify(&self, commitment: &CompressedEdwardsY) -> bool {
102    let Some(bit_commitments) =
103      self.bit_commitments.iter().copied().map(decompress_point).collect::<Option<Vec<_>>>()
104    else {
105      return false;
106    };
107
108    if &bit_commitments.iter().sum::<EdwardsPoint>().compress() != commitment {
109      return false;
110    }
111
112    #[allow(non_snake_case)]
113    let H_pow_2 = H_pow_2();
114    let mut commitments_sub_one = [EdwardsPoint::identity(); 64];
115    for i in 0 .. 64 {
116      commitments_sub_one[i] = bit_commitments[i] - H_pow_2[i];
117    }
118
119    self.sigs.verify(&bit_commitments, &commitments_sub_one)
120  }
121}