1use curve25519_dalek::traits::IsIdentity;
2use monero_oxide::{
3 ed25519::Point,
4 ringct::RctType,
5 transaction::{Input, Output, Timelock, Transaction},
6};
7
8use crate::{
9 batch_verifier::BatchVerifier, blocks::penalty_free_zone, is_decomposed_amount, HardFork,
10};
11
12pub use cuprate_types::TxVersion;
14
15mod contextual_data;
16mod ring_ct;
17mod ring_signatures;
18#[cfg(test)]
19mod tests;
20
21pub use contextual_data::*;
22pub use ring_ct::RingCTError;
23
24const MAX_BULLETPROOFS_OUTPUTS: usize = 16;
25const MAX_TX_BLOB_SIZE: usize = 1_000_000;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
28pub enum TransactionError {
29 #[error("The transaction's version is incorrect.")]
30 TransactionVersionInvalid,
31 #[error("The transaction is too big.")]
32 TooBig,
33 #[error("Output is not a valid point.")]
35 OutputNotValidPoint,
36 #[error("The transaction has an invalid output type.")]
37 OutputTypeInvalid,
38 #[error("The transaction is v1 with a 0 amount output.")]
39 ZeroOutputForV1,
40 #[error("The transaction is v2 with a non 0 amount output.")]
41 NonZeroOutputForV2,
42 #[error("The transaction has an output which is not decomposed.")]
43 AmountNotDecomposed,
44 #[error("The transaction's outputs overflow.")]
45 OutputsOverflow,
46 #[error("The transaction's outputs are too much.")]
47 OutputsTooHigh,
48 #[error("The transaction has too many outputs.")]
49 InvalidNumberOfOutputs,
50 #[error("One or more inputs don't have the expected number of decoys.")]
52 InputDoesNotHaveExpectedNumbDecoys,
53 #[error("The transaction has more than one mixable input with unmixable inputs.")]
54 MoreThanOneMixableInputWithUnmixable,
55 #[error("The key-image is not in the prime sub-group.")]
56 KeyImageIsNotInPrimeSubGroup,
57 #[error("Key-image is already spent.")]
58 KeyImageSpent,
59 #[error("The input is not the expected type.")]
60 IncorrectInputType,
61 #[error("The transaction has a duplicate ring member.")]
62 DuplicateRingMember,
63 #[error("The transaction inputs are not ordered.")]
64 InputsAreNotOrdered,
65 #[error("The transaction spends a decoy which is too young.")]
66 OneOrMoreRingMembersLocked,
67 #[error("The transaction inputs overflow.")]
68 InputsOverflow,
69 #[error("The transaction has no inputs.")]
70 NoInputs,
71 #[error("Ring member not in database or is not valid.")]
72 RingMemberNotFoundOrInvalid,
73 #[error("Ring signature incorrect.")]
75 RingSignatureIncorrect,
76 #[error("RingCT Error: {0}.")]
78 RingCTError(#[from] RingCTError),
79}
80
81fn check_output_keys(outputs: &[Output]) -> Result<(), TransactionError> {
87 for out in outputs {
88 if out.key.decompress().is_none() {
89 return Err(TransactionError::OutputNotValidPoint);
90 }
91 }
92
93 Ok(())
94}
95
96pub(crate) fn check_output_types(outputs: &[Output], hf: HardFork) -> Result<(), TransactionError> {
103 if hf == HardFork::V15 {
104 for outs in outputs.windows(2) {
105 if outs[0].view_tag.is_some() != outs[1].view_tag.is_some() {
106 return Err(TransactionError::OutputTypeInvalid);
107 }
108 }
109 return Ok(());
110 }
111
112 for out in outputs {
113 if hf <= HardFork::V14 && out.view_tag.is_some()
114 || hf >= HardFork::V16 && out.view_tag.is_none()
115 {
116 return Err(TransactionError::OutputTypeInvalid);
117 }
118 }
119 Ok(())
120}
121
122fn check_output_amount_v1(amount: u64, hf: HardFork) -> Result<(), TransactionError> {
126 if amount == 0 {
127 return Err(TransactionError::ZeroOutputForV1);
128 }
129
130 if hf >= HardFork::V2 && !is_decomposed_amount(&amount) {
131 return Err(TransactionError::AmountNotDecomposed);
132 }
133
134 Ok(())
135}
136
137const fn check_output_amount_v2(amount: u64) -> Result<(), TransactionError> {
141 if amount == 0 {
142 Ok(())
143 } else {
144 Err(TransactionError::NonZeroOutputForV2)
145 }
146}
147
148fn sum_outputs(
153 outputs: &[Output],
154 hf: HardFork,
155 tx_version: TxVersion,
156) -> Result<u64, TransactionError> {
157 let mut sum: u64 = 0;
158
159 for out in outputs {
160 let raw_amount = out.amount.unwrap_or(0);
161
162 match tx_version {
163 TxVersion::RingSignatures => check_output_amount_v1(raw_amount, hf)?,
164 TxVersion::RingCT => check_output_amount_v2(raw_amount)?,
165 }
166 sum = sum
167 .checked_add(raw_amount)
168 .ok_or(TransactionError::OutputsOverflow)?;
169 }
170
171 Ok(sum)
172}
173
174fn check_number_of_outputs(
180 outputs: usize,
181 hf: HardFork,
182 tx_version: TxVersion,
183 bp_or_bpp: bool,
184) -> Result<(), TransactionError> {
185 if tx_version == TxVersion::RingSignatures {
186 return Ok(());
187 }
188
189 if hf >= HardFork::V12 && outputs < 2 {
190 return Err(TransactionError::InvalidNumberOfOutputs);
191 }
192
193 if bp_or_bpp && outputs > MAX_BULLETPROOFS_OUTPUTS {
194 Err(TransactionError::InvalidNumberOfOutputs)
195 } else {
196 Ok(())
197 }
198}
199
200fn check_outputs_semantics(
206 outputs: &[Output],
207 hf: HardFork,
208 tx_version: TxVersion,
209 bp_or_bpp: bool,
210) -> Result<u64, TransactionError> {
211 check_output_types(outputs, hf)?;
212 check_output_keys(outputs)?;
213 check_number_of_outputs(outputs.len(), hf, tx_version, bp_or_bpp)?;
214
215 sum_outputs(outputs, hf, tx_version)
216}
217
218pub const fn output_unlocked(
224 time_lock: &Timelock,
225 current_chain_height: usize,
226 current_time_lock_timestamp: u64,
227 hf: HardFork,
228) -> bool {
229 match *time_lock {
230 Timelock::None => true,
231 Timelock::Block(unlock_height) => {
232 check_block_time_lock(unlock_height, current_chain_height)
233 }
234 Timelock::Time(unlock_time) => {
235 check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf)
236 }
237 }
238}
239
240const fn check_block_time_lock(unlock_height: usize, current_chain_height: usize) -> bool {
244 unlock_height <= current_chain_height
246}
247
248const fn check_timestamp_time_lock(
252 unlock_timestamp: u64,
253 current_time_lock_timestamp: u64,
254 hf: HardFork,
255) -> bool {
256 current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
257}
258
259fn check_all_time_locks(
266 time_locks: &[Timelock],
267 current_chain_height: usize,
268 current_time_lock_timestamp: u64,
269 hf: HardFork,
270) -> Result<(), TransactionError> {
271 time_locks.iter().try_for_each(|time_lock| {
272 if output_unlocked(
273 time_lock,
274 current_chain_height,
275 current_time_lock_timestamp,
276 hf,
277 ) {
278 Ok(())
279 } else {
280 tracing::debug!("Transaction invalid: one or more inputs locked, lock: {time_lock:?}.");
281 Err(TransactionError::OneOrMoreRingMembersLocked)
282 }
283 })
284}
285
286pub fn check_decoy_info(decoy_info: &DecoyInfo, hf: HardFork) -> Result<(), TransactionError> {
293 if hf == HardFork::V15 {
294 return check_decoy_info(decoy_info, HardFork::V14)
296 .or_else(|_| check_decoy_info(decoy_info, HardFork::V16));
297 }
298
299 let current_minimum_decoys = minimum_decoys(hf);
300
301 if decoy_info.min_decoys < current_minimum_decoys {
302 if decoy_info.not_mixable == 0 {
304 return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
305 }
306 if decoy_info.mixable > 1 {
308 return Err(TransactionError::MoreThanOneMixableInputWithUnmixable);
309 }
310 } else if hf >= HardFork::V8 && decoy_info.min_decoys != current_minimum_decoys {
311 return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
313 }
314
315 if hf >= HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys {
317 return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
318 }
319
320 Ok(())
321}
322
323fn check_key_images(input: &Input) -> Result<(), TransactionError> {
327 match input {
328 Input::ToKey { key_image, .. } => {
329 if !key_image.decompress().as_ref().is_some_and(|p| {
331 let p = Point::into(*p);
332
333 p.is_torsion_free() && !p.is_identity()
334 }) {
335 return Err(TransactionError::KeyImageIsNotInPrimeSubGroup);
336 }
337 }
338 Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
339 }
340
341 Ok(())
342}
343
344const fn check_input_type(input: &Input) -> Result<(), TransactionError> {
348 match input {
349 Input::ToKey { .. } => Ok(()),
350 Input::Gen(_) => Err(TransactionError::IncorrectInputType),
351 }
352}
353
354const fn check_input_has_decoys(input: &Input) -> Result<(), TransactionError> {
358 match input {
359 Input::ToKey { key_offsets, .. } => {
360 if key_offsets.is_empty() {
361 Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys)
362 } else {
363 Ok(())
364 }
365 }
366 Input::Gen(_) => Err(TransactionError::IncorrectInputType),
367 }
368}
369
370fn check_ring_members_unique(input: &Input, hf: HardFork) -> Result<(), TransactionError> {
374 if hf >= HardFork::V6 {
375 match input {
376 Input::ToKey { key_offsets, .. } => key_offsets.iter().skip(1).try_for_each(|offset| {
377 if *offset == 0 {
378 Err(TransactionError::DuplicateRingMember)
379 } else {
380 Ok(())
381 }
382 }),
383 Input::Gen(_) => Err(TransactionError::IncorrectInputType),
384 }
385 } else {
386 Ok(())
387 }
388}
389
390fn check_inputs_sorted(inputs: &[Input], hf: HardFork) -> Result<(), TransactionError> {
394 let get_ki = |inp: &Input| match inp {
395 Input::ToKey { key_image, .. } => Ok(key_image.to_bytes()),
396 Input::Gen(_) => Err(TransactionError::IncorrectInputType),
397 };
398
399 if hf >= HardFork::V7 {
400 for inps in inputs.windows(2) {
401 if get_ki(&inps[0])? <= get_ki(&inps[1])? {
402 return Err(TransactionError::InputsAreNotOrdered);
403 }
404 }
405 }
406
407 Ok(())
408}
409
410fn check_10_block_lock(
414 youngest_used_out_height: usize,
415 current_chain_height: usize,
416 hf: HardFork,
417) -> Result<(), TransactionError> {
418 if hf >= HardFork::V12 {
419 if youngest_used_out_height + 10 > current_chain_height {
420 tracing::debug!(
421 "Transaction invalid: One or more ring members younger than 10 blocks."
422 );
423 Err(TransactionError::OneOrMoreRingMembersLocked)
424 } else {
425 Ok(())
426 }
427 } else {
428 Ok(())
429 }
430}
431
432fn sum_inputs_check_overflow(inputs: &[Input]) -> Result<u64, TransactionError> {
436 let mut sum: u64 = 0;
437 for inp in inputs {
438 match inp {
439 Input::ToKey { amount, .. } => {
440 sum = sum
441 .checked_add(amount.unwrap_or(0))
442 .ok_or(TransactionError::InputsOverflow)?;
443 }
444 Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
445 }
446 }
447
448 Ok(sum)
449}
450
451fn check_inputs_semantics(inputs: &[Input], hf: HardFork) -> Result<u64, TransactionError> {
457 if inputs.is_empty() {
459 return Err(TransactionError::NoInputs);
460 }
461
462 for input in inputs {
463 check_input_type(input)?;
464 check_input_has_decoys(input)?;
465
466 check_ring_members_unique(input, hf)?;
467 }
468
469 check_inputs_sorted(inputs, hf)?;
470
471 sum_inputs_check_overflow(inputs)
472}
473
474fn check_inputs_contextual(
480 inputs: &[Input],
481 tx_ring_members_info: &TxRingMembersInfo,
482 current_chain_height: usize,
483 hf: HardFork,
484) -> Result<(), TransactionError> {
485 check_10_block_lock(
486 tx_ring_members_info.youngest_used_out_height,
487 current_chain_height,
488 hf,
489 )?;
490
491 if let Some(decoys_info) = &tx_ring_members_info.decoy_info {
492 check_decoy_info(decoys_info, hf)?;
493 } else {
494 assert_eq!(hf, HardFork::V1);
495 }
496
497 for input in inputs {
498 check_key_images(input)?;
499 }
500
501 Ok(())
502}
503
504fn check_tx_version(
510 decoy_info: &Option<DecoyInfo>,
511 version: TxVersion,
512 hf: HardFork,
513) -> Result<(), TransactionError> {
514 if let Some(decoy_info) = decoy_info {
515 let max = max_tx_version(hf);
516 if version > max {
517 return Err(TransactionError::TransactionVersionInvalid);
518 }
519
520 let min = min_tx_version(hf);
521 if version < min && decoy_info.not_mixable == 0 {
522 return Err(TransactionError::TransactionVersionInvalid);
523 }
524 } else {
525 if version != TxVersion::RingSignatures {
527 return Err(TransactionError::TransactionVersionInvalid);
528 }
529 }
530
531 Ok(())
532}
533
534fn max_tx_version(hf: HardFork) -> TxVersion {
536 if hf <= HardFork::V3 {
537 TxVersion::RingSignatures
538 } else {
539 TxVersion::RingCT
540 }
541}
542
543fn min_tx_version(hf: HardFork) -> TxVersion {
545 if hf >= HardFork::V6 {
546 TxVersion::RingCT
547 } else {
548 TxVersion::RingSignatures
549 }
550}
551
552fn transaction_weight_limit(hf: HardFork) -> usize {
553 penalty_free_zone(hf) / 2 - 600
554}
555
556pub fn check_transaction_semantic(
565 tx: &Transaction,
566 tx_blob_size: usize,
567 tx_weight: usize,
568 tx_hash: &[u8; 32],
569 hf: HardFork,
570 verifier: impl BatchVerifier,
571) -> Result<u64, TransactionError> {
572 if tx_blob_size > MAX_TX_BLOB_SIZE
574 || (hf >= HardFork::V8 && tx_weight > transaction_weight_limit(hf))
575 {
576 return Err(TransactionError::TooBig);
577 }
578
579 let tx_version =
580 TxVersion::from_raw(tx.version()).ok_or(TransactionError::TransactionVersionInvalid)?;
581
582 let bp_or_bpp = match tx {
583 Transaction::V2 {
584 proofs: Some(proofs),
585 ..
586 } => match proofs.rct_type() {
587 RctType::AggregateMlsagBorromean | RctType::MlsagBorromean => false,
588 RctType::MlsagBulletproofs
589 | RctType::MlsagBulletproofsCompactAmount
590 | RctType::ClsagBulletproof
591 | RctType::ClsagBulletproofPlus => true,
592 },
593 Transaction::V2 { proofs: None, .. } | Transaction::V1 { .. } => false,
594 };
595
596 let outputs_sum = check_outputs_semantics(&tx.prefix().outputs, hf, tx_version, bp_or_bpp)?;
597 let inputs_sum = check_inputs_semantics(&tx.prefix().inputs, hf)?;
598
599 let fee = match tx {
600 Transaction::V1 { .. } => {
601 if outputs_sum >= inputs_sum {
602 return Err(TransactionError::OutputsTooHigh);
603 }
604 inputs_sum - outputs_sum
605 }
606 Transaction::V2 { proofs, .. } => {
607 let proofs = proofs
608 .as_ref()
609 .ok_or(TransactionError::TransactionVersionInvalid)?;
610
611 ring_ct::ring_ct_semantic_checks(proofs, tx_hash, verifier, hf)?;
612
613 proofs.base.fee
614 }
615 };
616
617 Ok(fee)
618}
619
620pub fn check_transaction_contextual(
628 tx: &Transaction,
629 tx_ring_members_info: &TxRingMembersInfo,
630 current_chain_height: usize,
631 current_time_lock_timestamp: u64,
632 hf: HardFork,
633) -> Result<(), TransactionError> {
634 let tx_version =
635 TxVersion::from_raw(tx.version()).ok_or(TransactionError::TransactionVersionInvalid)?;
636
637 check_inputs_contextual(
638 &tx.prefix().inputs,
639 tx_ring_members_info,
640 current_chain_height,
641 hf,
642 )?;
643 check_tx_version(&tx_ring_members_info.decoy_info, tx_version, hf)?;
644
645 check_all_time_locks(
646 &tx_ring_members_info.time_locked_outs,
647 current_chain_height,
648 current_time_lock_timestamp,
649 hf,
650 )?;
651
652 match &tx {
653 Transaction::V1 { prefix, signatures } => ring_signatures::check_input_signatures(
654 &prefix.inputs,
655 signatures,
656 &tx_ring_members_info.rings,
657 &tx.signature_hash()
659 .ok_or(TransactionError::TransactionVersionInvalid)?,
660 ),
661 Transaction::V2 { prefix, proofs } => Ok(ring_ct::check_input_signatures(
662 &tx.signature_hash()
663 .ok_or(TransactionError::TransactionVersionInvalid)?,
664 &prefix.inputs,
665 proofs
666 .as_ref()
667 .ok_or(TransactionError::TransactionVersionInvalid)?,
668 &tx_ring_members_info.rings,
669 )?),
670 }
671}