Skip to main content

cuprate_consensus_rules/transactions/
ring_ct.rs

1use std::sync::LazyLock;
2
3use curve25519_dalek::{EdwardsPoint, Scalar};
4use hex_literal::hex;
5use monero_oxide::{
6    ed25519::{CompressedPoint, Point},
7    ringct::{
8        clsag::ClsagError,
9        mlsag::{AggregateRingMatrixBuilder, MlsagError, RingMatrix},
10        RctProofs, RctPrunable, RctType,
11    },
12    transaction::Input,
13};
14use rand::thread_rng;
15#[cfg(feature = "rayon")]
16use rayon::prelude::*;
17
18use crate::{batch_verifier::BatchVerifier, transactions::Rings, try_par_iter, HardFork};
19
20/// This constant contains the IDs of 2 transactions that should be allowed after the fork the ringCT
21/// type they used should be banned.
22const GRANDFATHERED_TRANSACTIONS: [[u8; 32]; 2] = [
23    hex!("c5151944f0583097ba0c88cd0f43e7fabb3881278aa2f73b3b0a007c5d34e910"),
24    hex!("6f2f117cde6fbcf8d4a6ef8974fcac744726574ac38cf25d3322c996b21edd4c"),
25];
26
27static H: LazyLock<EdwardsPoint> =
28    LazyLock::new(|| CompressedPoint::H.decompress().unwrap().into());
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
31pub enum RingCTError {
32    #[error("The RingCT type used is not allowed.")]
33    TypeNotAllowed,
34    #[error("RingCT simple: sum pseudo-outs does not equal outputs.")]
35    SimpleAmountDoNotBalance,
36    #[error("The borromean range proof is invalid.")]
37    BorromeanRangeInvalid,
38    #[error("The bulletproofs range proof is invalid.")]
39    BulletproofsRangeInvalid,
40    #[error("One or more input ring is invalid.")]
41    RingInvalid,
42    #[error("MLSAG Error: {0}.")]
43    MLSAGError(#[from] MlsagError),
44    #[error("CLSAG Error: {0}.")]
45    CLSAGError(#[from] ClsagError),
46}
47
48/// Checks the `RingCT` type is allowed for the current hard fork.
49///
50/// <https://monero-book.cuprate.org/consensus_rules/ring_ct.html#type>
51fn check_rct_type(ty: RctType, hf: HardFork, tx_hash: &[u8; 32]) -> Result<(), RingCTError> {
52    use HardFork as F;
53    use RctType as T;
54
55    match ty {
56        T::AggregateMlsagBorromean | T::MlsagBorromean if hf >= F::V4 && hf < F::V9 => Ok(()),
57        T::MlsagBulletproofs if hf >= F::V8 && hf < F::V11 => Ok(()),
58        T::MlsagBulletproofsCompactAmount if hf >= F::V10 && hf < F::V14 => Ok(()),
59        T::MlsagBulletproofsCompactAmount if GRANDFATHERED_TRANSACTIONS.contains(tx_hash) => Ok(()),
60        T::ClsagBulletproof if hf >= F::V13 && hf < F::V16 => Ok(()),
61        T::ClsagBulletproofPlus if hf >= F::V15 => Ok(()),
62
63        T::AggregateMlsagBorromean
64        | T::MlsagBorromean
65        | T::MlsagBulletproofs
66        | T::MlsagBulletproofsCompactAmount
67        | T::ClsagBulletproof
68        | T::ClsagBulletproofPlus => Err(RingCTError::TypeNotAllowed),
69    }
70}
71
72/// Checks that the pseudo-outs sum to the same point as the output commitments.
73///
74/// <https://monero-book.cuprate.org/consensus_rules/ring_ct.html#pseudo-outs-outpks-balance>
75fn simple_type_balances(rct_sig: &RctProofs) -> Result<(), RingCTError> {
76    let pseudo_outs = if rct_sig.rct_type() == RctType::MlsagBorromean {
77        &rct_sig.base.pseudo_outs
78    } else {
79        match &rct_sig.prunable {
80            RctPrunable::Clsag { pseudo_outs, .. }
81            | RctPrunable::MlsagBulletproofsCompactAmount { pseudo_outs, .. }
82            | RctPrunable::MlsagBulletproofs { pseudo_outs, .. } => pseudo_outs,
83            RctPrunable::MlsagBorromean { .. } => &rct_sig.base.pseudo_outs,
84            RctPrunable::AggregateMlsagBorromean { .. } => panic!("RingCT type is not simple!"),
85        }
86    };
87
88    let sum_inputs = pseudo_outs
89        .iter()
90        .map(CompressedPoint::decompress)
91        .map(|p| p.map(Point::into))
92        .sum::<Option<EdwardsPoint>>()
93        .ok_or(RingCTError::SimpleAmountDoNotBalance)?;
94
95    let sum_outputs = rct_sig
96        .base
97        .commitments
98        .iter()
99        .map(CompressedPoint::decompress)
100        .map(|p| p.map(Point::into))
101        .sum::<Option<EdwardsPoint>>()
102        .ok_or(RingCTError::SimpleAmountDoNotBalance)?
103        + Scalar::from(rct_sig.base.fee) * *H;
104
105    if sum_inputs == sum_outputs {
106        Ok(())
107    } else {
108        Err(RingCTError::SimpleAmountDoNotBalance)
109    }
110}
111
112/// Checks the outputs range proof(s)
113///
114/// <https://monero-book.cuprate.org/consensus_rules/ring_ct/borromean.html>
115/// <https://monero-book.cuprate.org/consensus_rules/ring_ct/bulletproofs.html>
116/// <https://monero-book.cuprate.org/consensus_rules/ring_ct/bulletproofs+.html>
117fn check_output_range_proofs(
118    proofs: &RctProofs,
119    mut verifier: impl BatchVerifier,
120) -> Result<(), RingCTError> {
121    let commitments = &proofs.base.commitments;
122
123    match &proofs.prunable {
124        RctPrunable::MlsagBorromean { borromean, .. }
125        | RctPrunable::AggregateMlsagBorromean { borromean, .. } => try_par_iter(borromean)
126            .zip(commitments)
127            .try_for_each(|(borro, commitment)| {
128                if borro.verify(commitment) {
129                    Ok(())
130                } else {
131                    Err(RingCTError::BorromeanRangeInvalid)
132                }
133            }),
134        RctPrunable::MlsagBulletproofs { bulletproof, .. }
135        | RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, .. }
136        | RctPrunable::Clsag { bulletproof, .. } => {
137            if verifier.queue_statement(|verifier| {
138                bulletproof.batch_verify(&mut thread_rng(), verifier, commitments)
139            }) {
140                Ok(())
141            } else {
142                Err(RingCTError::BulletproofsRangeInvalid)
143            }
144        }
145    }
146}
147
148pub(crate) fn ring_ct_semantic_checks(
149    proofs: &RctProofs,
150    tx_hash: &[u8; 32],
151    verifier: impl BatchVerifier,
152    hf: HardFork,
153) -> Result<(), RingCTError> {
154    let rct_type = proofs.rct_type();
155
156    check_rct_type(rct_type, hf, tx_hash)?;
157    check_output_range_proofs(proofs, verifier)?;
158
159    if rct_type != RctType::AggregateMlsagBorromean {
160        simple_type_balances(proofs)?;
161    }
162
163    Ok(())
164}
165
166/// Check the input signatures: MLSAG, CLSAG.
167///
168/// <https://monero-book.cuprate.org/consensus_rules/ring_ct/mlsag.html>
169/// <https://monero-book.cuprate.org/consensus_rules/ring_ct/clsag.html>
170pub(crate) fn check_input_signatures(
171    msg: &[u8; 32],
172    inputs: &[Input],
173    proofs: &RctProofs,
174    rings: &Rings,
175) -> Result<(), RingCTError> {
176    let Rings::RingCT(rings) = rings else {
177        panic!("Tried to verify RCT transaction without RCT ring");
178    };
179
180    if rings.is_empty() {
181        return Err(RingCTError::RingInvalid);
182    }
183
184    let pseudo_outs = match &proofs.prunable {
185        RctPrunable::MlsagBulletproofs { pseudo_outs, .. }
186        | RctPrunable::MlsagBulletproofsCompactAmount { pseudo_outs, .. }
187        | RctPrunable::Clsag { pseudo_outs, .. } => pseudo_outs.as_slice(),
188        RctPrunable::MlsagBorromean { .. } => proofs.base.pseudo_outs.as_slice(),
189        RctPrunable::AggregateMlsagBorromean { .. } => &[],
190    };
191
192    match &proofs.prunable {
193        RctPrunable::AggregateMlsagBorromean { mlsag, .. } => {
194            let key_images = inputs
195                .iter()
196                .map(|inp| {
197                    let Input::ToKey { key_image, .. } = inp else {
198                        panic!("How did we build a ring with no decoys?");
199                    };
200                    *key_image
201                })
202                .collect::<Vec<_>>();
203
204            let mut matrix =
205                AggregateRingMatrixBuilder::new(&proofs.base.commitments, proofs.base.fee)?;
206
207            rings.iter().try_for_each(|ring| matrix.push_ring(ring))?;
208
209            Ok(mlsag.verify(msg, &matrix.build()?, &key_images)?)
210        }
211        RctPrunable::MlsagBorromean { mlsags, .. }
212        | RctPrunable::MlsagBulletproofsCompactAmount { mlsags, .. }
213        | RctPrunable::MlsagBulletproofs { mlsags, .. } => try_par_iter(mlsags)
214            .zip(pseudo_outs)
215            .zip(inputs)
216            .zip(rings)
217            .try_for_each(|(((mlsag, pseudo_out), input), ring)| {
218                let Input::ToKey { key_image, .. } = input else {
219                    panic!("How did we build a ring with no decoys?");
220                };
221
222                Ok(mlsag.verify(
223                    msg,
224                    &RingMatrix::individual(ring, *pseudo_out)?,
225                    &[*key_image],
226                )?)
227            }),
228        RctPrunable::Clsag { clsags, .. } => try_par_iter(clsags)
229            .zip(pseudo_outs)
230            .zip(inputs)
231            .zip(rings)
232            .try_for_each(|(((clsags, pseudo_out), input), ring)| {
233                let Input::ToKey { key_image, .. } = input else {
234                    panic!("How did we build a ring with no decoys?");
235                };
236
237                Ok(clsags.verify(ring.clone(), key_image, pseudo_out, msg)?)
238            }),
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn grandfathered_bulletproofs2() {
248        assert!(check_rct_type(
249            RctType::MlsagBulletproofsCompactAmount,
250            HardFork::V14,
251            &[0; 32]
252        )
253        .is_err());
254
255        assert!(check_rct_type(
256            RctType::MlsagBulletproofsCompactAmount,
257            HardFork::V14,
258            &GRANDFATHERED_TRANSACTIONS[0]
259        )
260        .is_ok());
261        assert!(check_rct_type(
262            RctType::MlsagBulletproofsCompactAmount,
263            HardFork::V14,
264            &GRANDFATHERED_TRANSACTIONS[1]
265        )
266        .is_ok());
267    }
268}