cuprate_consensus_rules/transactions/
ring_ct.rs

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