cuprate_consensus_rules/transactions/
ring_ct.rs1use 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
19const 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
44fn 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
68fn 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
106fn 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
160pub(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}