monero_borromean/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![cfg_attr(not(feature = "std"), no_std)]
4#![allow(non_snake_case)]
5
6use core::fmt::Debug;
7use std_shims::{
8  sync::LazyLock,
9  io::{self, Read, Write},
10  vec::Vec,
11};
12
13use zeroize::Zeroize;
14
15use curve25519_dalek::{traits::Identity as _, EdwardsPoint, edwards::CompressedEdwardsY};
16
17use monero_io::*;
18use monero_ed25519::{UnreducedScalar, Scalar, Point, CompressedPoint};
19
20static H_POW_2_CELL: LazyLock<[EdwardsPoint; 64]> = LazyLock::new(|| {
21  #[allow(non_snake_case)]
22  let H = CompressedEdwardsY(monero_ed25519::CompressedPoint::H.to_bytes()).decompress().unwrap();
23  let mut res = [H; 64];
24  for i in 1 .. 64 {
25    res[i] = res[i - 1] + res[i - 1];
26  }
27  res
28});
29/// Monero's `H` generator, multiplied by 2**i for i in 1 ..= 64.
30///
31/// This table is useful when working with amounts, which are u64s.
32#[allow(non_snake_case)]
33fn H_pow_2() -> &'static [EdwardsPoint; 64] {
34  &H_POW_2_CELL
35}
36
37// 64 Borromean ring signatures, as needed for a 64-bit range proof.
38//
39// s0 and s1 are stored as `UnreducedScalar`s due to Monero not requiring they were reduced.
40// `UnreducedScalar` preserves their original byte encoding and implements a custom reduction
41// algorithm which was in use.
42#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
43struct BorromeanSignatures {
44  s0: [UnreducedScalar; 64],
45  s1: [UnreducedScalar; 64],
46  ee: Scalar,
47}
48
49impl BorromeanSignatures {
50  // Read a set of BorromeanSignatures.
51  fn read<R: Read>(r: &mut R) -> io::Result<BorromeanSignatures> {
52    Ok(BorromeanSignatures {
53      s0: read_array(UnreducedScalar::read, r)?,
54      s1: read_array(UnreducedScalar::read, r)?,
55      ee: Scalar::read(r)?,
56    })
57  }
58
59  // Write the set of BorromeanSignatures.
60  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
61    for s0 in &self.s0 {
62      s0.write(w)?;
63    }
64    for s1 in &self.s1 {
65      s1.write(w)?;
66    }
67    self.ee.write(w)
68  }
69
70  fn verify(&self, keys_a: &[EdwardsPoint], keys_b: &[EdwardsPoint]) -> bool {
71    let mut transcript = [0; 2048];
72
73    for i in 0 .. 64 {
74      #[allow(non_snake_case)]
75      let LL = EdwardsPoint::vartime_double_scalar_mul_basepoint(
76        &self.ee.into(),
77        &keys_a[i],
78        &self.s0[i].ref10_slide_scalar_vartime().into(),
79      );
80      #[allow(non_snake_case)]
81      let LV = EdwardsPoint::vartime_double_scalar_mul_basepoint(
82        &Scalar::hash(LL.compress().to_bytes()).into(),
83        &keys_b[i],
84        &self.s1[i].ref10_slide_scalar_vartime().into(),
85      );
86      transcript[(i * 32) .. ((i + 1) * 32)].copy_from_slice(&LV.compress().to_bytes());
87    }
88
89    Scalar::hash(transcript) == self.ee
90  }
91}
92
93/// A range proof premised on Borromean ring signatures.
94#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
95pub struct BorromeanRange {
96  sigs: BorromeanSignatures,
97  bit_commitments: [CompressedPoint; 64],
98}
99
100impl BorromeanRange {
101  /// Read a BorromeanRange proof.
102  pub fn read<R: Read>(r: &mut R) -> io::Result<BorromeanRange> {
103    Ok(BorromeanRange {
104      sigs: BorromeanSignatures::read(r)?,
105      bit_commitments: read_array(CompressedPoint::read, r)?,
106    })
107  }
108
109  /// Write the BorromeanRange proof.
110  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
111    self.sigs.write(w)?;
112    write_raw_vec(CompressedPoint::write, &self.bit_commitments, w)
113  }
114
115  /// Verify the commitment contains a 64-bit value.
116  #[must_use]
117  pub fn verify(&self, commitment: &CompressedPoint) -> bool {
118    let Some(bit_commitments) = self
119      .bit_commitments
120      .iter()
121      .map(|compressed| compressed.decompress().map(Point::into))
122      .collect::<Option<Vec<_>>>()
123    else {
124      return false;
125    };
126
127    if bit_commitments.iter().sum::<EdwardsPoint>().compress().0 != commitment.to_bytes() {
128      return false;
129    }
130
131    #[allow(non_snake_case)]
132    let H_pow_2 = H_pow_2();
133    let mut commitments_sub_one = [EdwardsPoint::identity(); 64];
134    for i in 0 .. 64 {
135      commitments_sub_one[i] = bit_commitments[i] - H_pow_2[i];
136    }
137
138    self.sigs.verify(&bit_commitments, &commitments_sub_one)
139  }
140}