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