monero_oxide/
ring_signatures.rs

1use std_shims::{
2  io::{self, *},
3  vec::Vec,
4};
5
6use zeroize::Zeroize;
7
8use crate::{io::*, ed25519::*};
9
10#[cfg_attr(not(test), expect(clippy::cfg_not_test))]
11#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
12pub(crate) struct Signature {
13  #[cfg(test)]
14  pub(crate) c: Scalar,
15  #[cfg(test)]
16  pub(crate) s: Scalar,
17  #[cfg(not(test))]
18  c: Scalar,
19  #[cfg(not(test))]
20  s: Scalar,
21}
22
23impl Signature {
24  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
25    self.c.write(w)?;
26    self.s.write(w)?;
27    Ok(())
28  }
29
30  fn read<R: Read>(r: &mut R) -> io::Result<Signature> {
31    Ok(Signature { c: Scalar::read(r)?, s: Scalar::read(r)? })
32  }
33}
34
35/// A ring signature.
36///
37/// This was used by the original Cryptonote transaction protocol and was deprecated with RingCT.
38#[cfg_attr(not(test), expect(clippy::cfg_not_test))]
39#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
40pub struct RingSignature {
41  #[cfg(test)]
42  pub(crate) sigs: Vec<Signature>,
43  #[cfg(not(test))]
44  sigs: Vec<Signature>,
45}
46
47impl RingSignature {
48  /// Write the RingSignature.
49  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
50    for sig in &self.sigs {
51      sig.write(w)?;
52    }
53    Ok(())
54  }
55
56  /// Read a RingSignature.
57  pub fn read<R: Read>(members: usize, r: &mut R) -> io::Result<RingSignature> {
58    Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? })
59  }
60
61  /// Verify the ring signature.
62  ///
63  /// WARNING: This follows the Fiat-Shamir transcript format used by the Monero protocol, which
64  /// makes assumptions on what has already been transcripted and bound to within `msg_hash`. Do
65  /// not use this if you don't know what you're doing.
66  pub fn verify(
67    &self,
68    msg_hash: &[u8; 32],
69    ring: &[CompressedPoint],
70    key_image: &CompressedPoint,
71  ) -> bool {
72    if ring.len() != self.sigs.len() {
73      return false;
74    }
75
76    let Some(key_image) = key_image.decompress() else {
77      return false;
78    };
79    let Some(key_image) = key_image.key_image() else {
80      return false;
81    };
82
83    let mut buf = Vec::with_capacity(32 + (2 * 32 * ring.len()));
84    buf.extend_from_slice(msg_hash);
85
86    let mut sum = curve25519_dalek::Scalar::ZERO;
87    for (ring_member, sig) in ring.iter().zip(&self.sigs) {
88      /*
89        The traditional Schnorr signature is:
90          r = sample()
91          c = H(r G || m)
92          s = r - c x
93        Verified as:
94          s G + c A == R
95
96        Each ring member here performs a dual-Schnorr signature for:
97          s G + c A
98          s HtP(A) + c K
99        Where the transcript is pushed both these values, r G, r HtP(A) for the real spend.
100        This also serves as a DLEq proof between the key and the key image.
101
102        Checking sum(c) == H(transcript) acts a disjunction, where any one of the `c`s can be
103        modified to cause the intended sum, if and only if a corresponding `s` value is known.
104      */
105
106      let Some(decomp_ring_member) = ring_member.decompress() else {
107        return false;
108      };
109
110      #[expect(non_snake_case)]
111      let Li = curve25519_dalek::EdwardsPoint::vartime_double_scalar_mul_basepoint(
112        &sig.c.into(),
113        &decomp_ring_member.into(),
114        &sig.s.into(),
115      );
116      buf.extend_from_slice(Li.compress().as_bytes());
117      #[expect(non_snake_case)]
118      let Ri = (sig.s.into() * Point::biased_hash(ring_member.to_bytes()).into()) +
119        (sig.c.into() * key_image);
120      buf.extend_from_slice(Ri.compress().as_bytes());
121
122      sum += sig.c.into();
123    }
124    Scalar::from(sum) == Scalar::hash(buf)
125  }
126}