cuprate_consensus_rules/transactions/
ring_ct.rs1use 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
20const 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
48fn 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
72fn 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
112fn 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
166pub(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}