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 #[error("Transaction batch verification failed.")]
80 BatchVerificationFailed,
81}
82
83fn check_output_keys(outputs: &[Output]) -> Result<(), TransactionError> {
89 for out in outputs {
90 if out.key.decompress().is_none() {
91 return Err(TransactionError::OutputNotValidPoint);
92 }
93 }
94
95 Ok(())
96}
97
98pub(crate) fn check_output_types(outputs: &[Output], hf: HardFork) -> Result<(), TransactionError> {
105 if hf == HardFork::V15 {
106 for outs in outputs.windows(2) {
107 if outs[0].view_tag.is_some() != outs[1].view_tag.is_some() {
108 return Err(TransactionError::OutputTypeInvalid);
109 }
110 }
111 return Ok(());
112 }
113
114 for out in outputs {
115 if hf <= HardFork::V14 && out.view_tag.is_some()
116 || hf >= HardFork::V16 && out.view_tag.is_none()
117 {
118 return Err(TransactionError::OutputTypeInvalid);
119 }
120 }
121 Ok(())
122}
123
124fn check_output_amount_v1(amount: u64, hf: HardFork) -> Result<(), TransactionError> {
128 if amount == 0 {
129 return Err(TransactionError::ZeroOutputForV1);
130 }
131
132 if hf >= HardFork::V2 && !is_decomposed_amount(&amount) {
133 return Err(TransactionError::AmountNotDecomposed);
134 }
135
136 Ok(())
137}
138
139const fn check_output_amount_v2(amount: u64) -> Result<(), TransactionError> {
143 if amount == 0 {
144 Ok(())
145 } else {
146 Err(TransactionError::NonZeroOutputForV2)
147 }
148}
149
150fn sum_outputs(
155 outputs: &[Output],
156 hf: HardFork,
157 tx_version: TxVersion,
158) -> Result<u64, TransactionError> {
159 let mut sum: u64 = 0;
160
161 for out in outputs {
162 let raw_amount = out.amount.unwrap_or(0);
163
164 match tx_version {
165 TxVersion::RingSignatures => check_output_amount_v1(raw_amount, hf)?,
166 TxVersion::RingCT => check_output_amount_v2(raw_amount)?,
167 }
168 sum = sum
169 .checked_add(raw_amount)
170 .ok_or(TransactionError::OutputsOverflow)?;
171 }
172
173 Ok(sum)
174}
175
176fn check_number_of_outputs(
182 outputs: usize,
183 hf: HardFork,
184 tx_version: TxVersion,
185 bp_or_bpp: bool,
186) -> Result<(), TransactionError> {
187 if tx_version == TxVersion::RingSignatures {
188 return Ok(());
189 }
190
191 if hf >= HardFork::V12 && outputs < 2 {
192 return Err(TransactionError::InvalidNumberOfOutputs);
193 }
194
195 if bp_or_bpp && outputs > MAX_BULLETPROOFS_OUTPUTS {
196 Err(TransactionError::InvalidNumberOfOutputs)
197 } else {
198 Ok(())
199 }
200}
201
202fn check_outputs_semantics(
208 outputs: &[Output],
209 hf: HardFork,
210 tx_version: TxVersion,
211 bp_or_bpp: bool,
212) -> Result<u64, TransactionError> {
213 check_output_types(outputs, hf)?;
214 check_output_keys(outputs)?;
215 check_number_of_outputs(outputs.len(), hf, tx_version, bp_or_bpp)?;
216
217 sum_outputs(outputs, hf, tx_version)
218}
219
220pub const fn output_unlocked(
226 time_lock: &Timelock,
227 current_chain_height: usize,
228 current_time_lock_timestamp: u64,
229 hf: HardFork,
230) -> bool {
231 match *time_lock {
232 Timelock::None => true,
233 Timelock::Block(unlock_height) => {
234 check_block_time_lock(unlock_height, current_chain_height)
235 }
236 Timelock::Time(unlock_time) => {
237 check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf)
238 }
239 }
240}
241
242const fn check_block_time_lock(unlock_height: usize, current_chain_height: usize) -> bool {
246 unlock_height <= current_chain_height
248}
249
250const fn check_timestamp_time_lock(
254 unlock_timestamp: u64,
255 current_time_lock_timestamp: u64,
256 hf: HardFork,
257) -> bool {
258 current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
259}
260
261fn check_all_time_locks(
268 time_locks: &[Timelock],
269 current_chain_height: usize,
270 current_time_lock_timestamp: u64,
271 hf: HardFork,
272) -> Result<(), TransactionError> {
273 time_locks.iter().try_for_each(|time_lock| {
274 if output_unlocked(
275 time_lock,
276 current_chain_height,
277 current_time_lock_timestamp,
278 hf,
279 ) {
280 Ok(())
281 } else {
282 tracing::debug!("Transaction invalid: one or more inputs locked, lock: {time_lock:?}.");
283 Err(TransactionError::OneOrMoreRingMembersLocked)
284 }
285 })
286}
287
288pub fn check_decoy_info(decoy_info: &DecoyInfo, hf: HardFork) -> Result<(), TransactionError> {
295 if hf == HardFork::V15 {
296 return check_decoy_info(decoy_info, HardFork::V14)
298 .or_else(|_| check_decoy_info(decoy_info, HardFork::V16));
299 }
300
301 let current_minimum_decoys = minimum_decoys(hf);
302
303 if decoy_info.min_decoys < current_minimum_decoys {
304 if decoy_info.not_mixable == 0 {
306 return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
307 }
308 if decoy_info.mixable > 1 {
310 return Err(TransactionError::MoreThanOneMixableInputWithUnmixable);
311 }
312 } else if hf >= HardFork::V8 && decoy_info.min_decoys != current_minimum_decoys {
313 return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
315 }
316
317 if hf >= HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys {
319 return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
320 }
321
322 Ok(())
323}
324
325fn check_key_images(input: &Input) -> Result<(), TransactionError> {
329 match input {
330 Input::ToKey { key_image, .. } => {
331 if !key_image.decompress().as_ref().is_some_and(|p| {
333 let p = Point::into(*p);
334
335 p.is_torsion_free() && !p.is_identity()
336 }) {
337 return Err(TransactionError::KeyImageIsNotInPrimeSubGroup);
338 }
339 }
340 Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
341 }
342
343 Ok(())
344}
345
346const fn check_input_type(input: &Input) -> Result<(), TransactionError> {
350 match input {
351 Input::ToKey { .. } => Ok(()),
352 Input::Gen(_) => Err(TransactionError::IncorrectInputType),
353 }
354}
355
356const fn check_input_has_decoys(input: &Input) -> Result<(), TransactionError> {
360 match input {
361 Input::ToKey { key_offsets, .. } => {
362 if key_offsets.is_empty() {
363 Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys)
364 } else {
365 Ok(())
366 }
367 }
368 Input::Gen(_) => Err(TransactionError::IncorrectInputType),
369 }
370}
371
372fn check_ring_members_unique(input: &Input, hf: HardFork) -> Result<(), TransactionError> {
376 if hf >= HardFork::V6 {
377 match input {
378 Input::ToKey { key_offsets, .. } => key_offsets.iter().skip(1).try_for_each(|offset| {
379 if *offset == 0 {
380 Err(TransactionError::DuplicateRingMember)
381 } else {
382 Ok(())
383 }
384 }),
385 Input::Gen(_) => Err(TransactionError::IncorrectInputType),
386 }
387 } else {
388 Ok(())
389 }
390}
391
392fn check_inputs_sorted(inputs: &[Input], hf: HardFork) -> Result<(), TransactionError> {
396 let get_ki = |inp: &Input| match inp {
397 Input::ToKey { key_image, .. } => Ok(key_image.to_bytes()),
398 Input::Gen(_) => Err(TransactionError::IncorrectInputType),
399 };
400
401 if hf >= HardFork::V7 {
402 for inps in inputs.windows(2) {
403 if get_ki(&inps[0])? <= get_ki(&inps[1])? {
404 return Err(TransactionError::InputsAreNotOrdered);
405 }
406 }
407 }
408
409 Ok(())
410}
411
412fn check_10_block_lock(
416 youngest_used_out_height: usize,
417 current_chain_height: usize,
418 hf: HardFork,
419) -> Result<(), TransactionError> {
420 if hf >= HardFork::V12 {
421 if youngest_used_out_height + 10 > current_chain_height {
422 tracing::debug!(
423 "Transaction invalid: One or more ring members younger than 10 blocks."
424 );
425 Err(TransactionError::OneOrMoreRingMembersLocked)
426 } else {
427 Ok(())
428 }
429 } else {
430 Ok(())
431 }
432}
433
434fn sum_inputs_check_overflow(inputs: &[Input]) -> Result<u64, TransactionError> {
438 let mut sum: u64 = 0;
439 for inp in inputs {
440 match inp {
441 Input::ToKey { amount, .. } => {
442 sum = sum
443 .checked_add(amount.unwrap_or(0))
444 .ok_or(TransactionError::InputsOverflow)?;
445 }
446 Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
447 }
448 }
449
450 Ok(sum)
451}
452
453fn check_inputs_semantics(inputs: &[Input], hf: HardFork) -> Result<u64, TransactionError> {
459 if inputs.is_empty() {
461 return Err(TransactionError::NoInputs);
462 }
463
464 for input in inputs {
465 check_input_type(input)?;
466 check_input_has_decoys(input)?;
467
468 check_ring_members_unique(input, hf)?;
469 }
470
471 check_inputs_sorted(inputs, hf)?;
472
473 sum_inputs_check_overflow(inputs)
474}
475
476fn check_inputs_contextual(
482 inputs: &[Input],
483 tx_ring_members_info: &TxRingMembersInfo,
484 current_chain_height: usize,
485 hf: HardFork,
486) -> Result<(), TransactionError> {
487 check_10_block_lock(
488 tx_ring_members_info.youngest_used_out_height,
489 current_chain_height,
490 hf,
491 )?;
492
493 if let Some(decoys_info) = &tx_ring_members_info.decoy_info {
494 check_decoy_info(decoys_info, hf)?;
495 } else {
496 assert_eq!(hf, HardFork::V1);
497 }
498
499 for input in inputs {
500 check_key_images(input)?;
501 }
502
503 Ok(())
504}
505
506fn check_tx_version(
512 decoy_info: &Option<DecoyInfo>,
513 version: TxVersion,
514 hf: HardFork,
515) -> Result<(), TransactionError> {
516 if let Some(decoy_info) = decoy_info {
517 let max = max_tx_version(hf);
518 if version > max {
519 return Err(TransactionError::TransactionVersionInvalid);
520 }
521
522 let min = min_tx_version(hf);
523 if version < min && decoy_info.not_mixable == 0 {
524 return Err(TransactionError::TransactionVersionInvalid);
525 }
526 } else {
527 if version != TxVersion::RingSignatures {
529 return Err(TransactionError::TransactionVersionInvalid);
530 }
531 }
532
533 Ok(())
534}
535
536fn max_tx_version(hf: HardFork) -> TxVersion {
538 if hf <= HardFork::V3 {
539 TxVersion::RingSignatures
540 } else {
541 TxVersion::RingCT
542 }
543}
544
545fn min_tx_version(hf: HardFork) -> TxVersion {
547 if hf >= HardFork::V6 {
548 TxVersion::RingCT
549 } else {
550 TxVersion::RingSignatures
551 }
552}
553
554fn transaction_weight_limit(hf: HardFork) -> usize {
555 penalty_free_zone(hf) / 2 - 600
556}
557
558pub fn check_transaction_semantic(
567 tx: &Transaction,
568 tx_blob_size: usize,
569 tx_weight: usize,
570 tx_hash: &[u8; 32],
571 hf: HardFork,
572 verifier: impl BatchVerifier,
573) -> Result<u64, TransactionError> {
574 if tx_blob_size > MAX_TX_BLOB_SIZE
576 || (hf >= HardFork::V8 && tx_weight > transaction_weight_limit(hf))
577 {
578 return Err(TransactionError::TooBig);
579 }
580
581 let tx_version =
582 TxVersion::from_raw(tx.version()).ok_or(TransactionError::TransactionVersionInvalid)?;
583
584 let bp_or_bpp = match tx {
585 Transaction::V2 {
586 proofs: Some(proofs),
587 ..
588 } => match proofs.rct_type() {
589 RctType::AggregateMlsagBorromean | RctType::MlsagBorromean => false,
590 RctType::MlsagBulletproofs
591 | RctType::MlsagBulletproofsCompactAmount
592 | RctType::ClsagBulletproof
593 | RctType::ClsagBulletproofPlus => true,
594 },
595 Transaction::V2 { proofs: None, .. } | Transaction::V1 { .. } => false,
596 };
597
598 let outputs_sum = check_outputs_semantics(&tx.prefix().outputs, hf, tx_version, bp_or_bpp)?;
599 let inputs_sum = check_inputs_semantics(&tx.prefix().inputs, hf)?;
600
601 let fee = match tx {
602 Transaction::V1 { .. } => {
603 if outputs_sum >= inputs_sum {
604 return Err(TransactionError::OutputsTooHigh);
605 }
606 inputs_sum - outputs_sum
607 }
608 Transaction::V2 { proofs, .. } => {
609 let proofs = proofs
610 .as_ref()
611 .ok_or(TransactionError::TransactionVersionInvalid)?;
612
613 ring_ct::ring_ct_semantic_checks(proofs, tx_hash, verifier, hf)?;
614
615 proofs.base.fee
616 }
617 };
618
619 Ok(fee)
620}
621
622pub fn check_transaction_contextual(
630 tx: &Transaction,
631 tx_ring_members_info: &TxRingMembersInfo,
632 current_chain_height: usize,
633 current_time_lock_timestamp: u64,
634 hf: HardFork,
635) -> Result<(), TransactionError> {
636 let tx_version =
637 TxVersion::from_raw(tx.version()).ok_or(TransactionError::TransactionVersionInvalid)?;
638
639 check_inputs_contextual(
640 &tx.prefix().inputs,
641 tx_ring_members_info,
642 current_chain_height,
643 hf,
644 )?;
645 check_tx_version(&tx_ring_members_info.decoy_info, tx_version, hf)?;
646
647 check_all_time_locks(
648 &tx_ring_members_info.time_locked_outs,
649 current_chain_height,
650 current_time_lock_timestamp,
651 hf,
652 )?;
653
654 match &tx {
655 Transaction::V1 { prefix, signatures } => ring_signatures::check_input_signatures(
656 &prefix.inputs,
657 signatures,
658 &tx_ring_members_info.rings,
659 &tx.signature_hash()
661 .ok_or(TransactionError::TransactionVersionInvalid)?,
662 ),
663 Transaction::V2 { prefix, proofs } => Ok(ring_ct::check_input_signatures(
664 &tx.signature_hash()
665 .ok_or(TransactionError::TransactionVersionInvalid)?,
666 &prefix.inputs,
667 proofs
668 .as_ref()
669 .ok_or(TransactionError::TransactionVersionInvalid)?,
670 &tx_ring_members_info.rings,
671 )?),
672 }
673}