1use std::cmp::Ordering;
2
3use curve25519_dalek::EdwardsPoint;
4use monero_serai::{
5 io::decompress_point,
6 ringct::RctType,
7 transaction::{Input, Output, Timelock, Transaction},
8};
9
10use crate::{
11 batch_verifier::BatchVerifier, blocks::penalty_free_zone, check_point_canonically_encoded,
12 is_decomposed_amount, HardFork,
13};
14
15pub use cuprate_types::TxVersion;
17
18mod contextual_data;
19mod ring_ct;
20mod ring_signatures;
21#[cfg(test)]
22mod tests;
23
24pub use contextual_data::*;
25pub use ring_ct::RingCTError;
26
27const MAX_BULLETPROOFS_OUTPUTS: usize = 16;
28const MAX_TX_BLOB_SIZE: usize = 1_000_000;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
31pub enum TransactionError {
32 #[error("The transactions version is incorrect.")]
33 TransactionVersionInvalid,
34 #[error("The transactions is too big.")]
35 TooBig,
36 #[error("Output is not a valid point.")]
38 OutputNotValidPoint,
39 #[error("The transaction has an invalid output type.")]
40 OutputTypeInvalid,
41 #[error("The transaction is v1 with a 0 amount output.")]
42 ZeroOutputForV1,
43 #[error("The transaction is v2 with a non 0 amount output.")]
44 NonZeroOutputForV2,
45 #[error("The transaction has an output which is not decomposed.")]
46 AmountNotDecomposed,
47 #[error("The transactions outputs overflow.")]
48 OutputsOverflow,
49 #[error("The transactions outputs too much.")]
50 OutputsTooHigh,
51 #[error("The transactions has too many outputs.")]
52 InvalidNumberOfOutputs,
53 #[error("One or more inputs don't have the expected number of decoys.")]
55 InputDoesNotHaveExpectedNumbDecoys,
56 #[error("The transaction has more than one mixable input with unmixable inputs.")]
57 MoreThanOneMixableInputWithUnmixable,
58 #[error("The key-image is not in the prime sub-group.")]
59 KeyImageIsNotInPrimeSubGroup,
60 #[error("Key-image is already spent.")]
61 KeyImageSpent,
62 #[error("The input is not the expected type.")]
63 IncorrectInputType,
64 #[error("The transaction has a duplicate ring member.")]
65 DuplicateRingMember,
66 #[error("The transaction inputs are not ordered.")]
67 InputsAreNotOrdered,
68 #[error("The transaction spends a decoy which is too young.")]
69 OneOrMoreRingMembersLocked,
70 #[error("The transaction inputs overflow.")]
71 InputsOverflow,
72 #[error("The transaction has no inputs.")]
73 NoInputs,
74 #[error("Ring member not in database or is not valid.")]
75 RingMemberNotFoundOrInvalid,
76 #[error("Ring signature incorrect.")]
78 RingSignatureIncorrect,
79 #[error("RingCT Error: {0}.")]
81 RingCTError(#[from] RingCTError),
82}
83
84fn check_output_keys(outputs: &[Output]) -> Result<(), TransactionError> {
90 for out in outputs {
91 if !check_point_canonically_encoded(&out.key) {
92 return Err(TransactionError::OutputNotValidPoint);
93 }
94 }
95
96 Ok(())
97}
98
99pub(crate) fn check_output_types(outputs: &[Output], hf: HardFork) -> Result<(), TransactionError> {
106 if hf == HardFork::V15 {
107 for outs in outputs.windows(2) {
108 if outs[0].view_tag.is_some() != outs[1].view_tag.is_some() {
109 return Err(TransactionError::OutputTypeInvalid);
110 }
111 }
112 return Ok(());
113 }
114
115 for out in outputs {
116 if hf <= HardFork::V14 && out.view_tag.is_some()
117 || hf >= HardFork::V16 && out.view_tag.is_none()
118 {
119 return Err(TransactionError::OutputTypeInvalid);
120 }
121 }
122 Ok(())
123}
124
125fn check_output_amount_v1(amount: u64, hf: HardFork) -> Result<(), TransactionError> {
129 if amount == 0 {
130 return Err(TransactionError::ZeroOutputForV1);
131 }
132
133 if hf >= HardFork::V2 && !is_decomposed_amount(&amount) {
134 return Err(TransactionError::AmountNotDecomposed);
135 }
136
137 Ok(())
138}
139
140const fn check_output_amount_v2(amount: u64) -> Result<(), TransactionError> {
144 if amount == 0 {
145 Ok(())
146 } else {
147 Err(TransactionError::NonZeroOutputForV2)
148 }
149}
150
151fn sum_outputs(
156 outputs: &[Output],
157 hf: HardFork,
158 tx_version: TxVersion,
159) -> Result<u64, TransactionError> {
160 let mut sum: u64 = 0;
161
162 for out in outputs {
163 let raw_amount = out.amount.unwrap_or(0);
164
165 match tx_version {
166 TxVersion::RingSignatures => check_output_amount_v1(raw_amount, hf)?,
167 TxVersion::RingCT => check_output_amount_v2(raw_amount)?,
168 }
169 sum = sum
170 .checked_add(raw_amount)
171 .ok_or(TransactionError::OutputsOverflow)?;
172 }
173
174 Ok(sum)
175}
176
177fn check_number_of_outputs(
183 outputs: usize,
184 hf: HardFork,
185 tx_version: TxVersion,
186 bp_or_bpp: bool,
187) -> Result<(), TransactionError> {
188 if tx_version == TxVersion::RingSignatures {
189 return Ok(());
190 }
191
192 if hf >= HardFork::V12 && outputs < 2 {
193 return Err(TransactionError::InvalidNumberOfOutputs);
194 }
195
196 if bp_or_bpp && outputs > MAX_BULLETPROOFS_OUTPUTS {
197 Err(TransactionError::InvalidNumberOfOutputs)
198 } else {
199 Ok(())
200 }
201}
202
203fn check_outputs_semantics(
209 outputs: &[Output],
210 hf: HardFork,
211 tx_version: TxVersion,
212 bp_or_bpp: bool,
213) -> Result<u64, TransactionError> {
214 check_output_types(outputs, hf)?;
215 check_output_keys(outputs)?;
216 check_number_of_outputs(outputs.len(), hf, tx_version, bp_or_bpp)?;
217
218 sum_outputs(outputs, hf, tx_version)
219}
220
221pub const fn output_unlocked(
227 time_lock: &Timelock,
228 current_chain_height: usize,
229 current_time_lock_timestamp: u64,
230 hf: HardFork,
231) -> bool {
232 match *time_lock {
233 Timelock::None => true,
234 Timelock::Block(unlock_height) => {
235 check_block_time_lock(unlock_height, current_chain_height)
236 }
237 Timelock::Time(unlock_time) => {
238 check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf)
239 }
240 }
241}
242
243const fn check_block_time_lock(unlock_height: usize, current_chain_height: usize) -> bool {
247 unlock_height <= current_chain_height
249}
250
251const fn check_timestamp_time_lock(
255 unlock_timestamp: u64,
256 current_time_lock_timestamp: u64,
257 hf: HardFork,
258) -> bool {
259 current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
260}
261
262fn check_all_time_locks(
269 time_locks: &[Timelock],
270 current_chain_height: usize,
271 current_time_lock_timestamp: u64,
272 hf: HardFork,
273) -> Result<(), TransactionError> {
274 time_locks.iter().try_for_each(|time_lock| {
275 if output_unlocked(
276 time_lock,
277 current_chain_height,
278 current_time_lock_timestamp,
279 hf,
280 ) {
281 Ok(())
282 } else {
283 tracing::debug!("Transaction invalid: one or more inputs locked, lock: {time_lock:?}.");
284 Err(TransactionError::OneOrMoreRingMembersLocked)
285 }
286 })
287}
288
289pub fn check_decoy_info(decoy_info: &DecoyInfo, hf: HardFork) -> Result<(), TransactionError> {
296 if hf == HardFork::V15 {
297 return check_decoy_info(decoy_info, HardFork::V14)
299 .or_else(|_| check_decoy_info(decoy_info, HardFork::V16));
300 }
301
302 let current_minimum_decoys = minimum_decoys(hf);
303
304 if decoy_info.min_decoys < current_minimum_decoys {
305 if decoy_info.not_mixable == 0 {
307 return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
308 }
309 if decoy_info.mixable > 1 {
311 return Err(TransactionError::MoreThanOneMixableInputWithUnmixable);
312 }
313 } else if hf >= HardFork::V8 && decoy_info.min_decoys != current_minimum_decoys {
314 return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
316 }
317
318 if hf >= HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys {
320 return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
321 }
322
323 Ok(())
324}
325
326fn check_key_images(input: &Input) -> Result<(), TransactionError> {
330 match input {
331 Input::ToKey { key_image, .. } => {
332 if !decompress_point(*key_image)
334 .as_ref()
335 .is_some_and(EdwardsPoint::is_torsion_free)
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
356fn 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 match get_ki(&inps[0])?.cmp(&get_ki(&inps[1])?) {
404 Ordering::Greater => (),
405 _ => return Err(TransactionError::InputsAreNotOrdered),
406 }
407 }
408 }
409
410 Ok(())
411}
412
413fn check_10_block_lock(
417 youngest_used_out_height: usize,
418 current_chain_height: usize,
419 hf: HardFork,
420) -> Result<(), TransactionError> {
421 if hf >= HardFork::V12 {
422 if youngest_used_out_height + 10 > current_chain_height {
423 tracing::debug!(
424 "Transaction invalid: One or more ring members younger than 10 blocks."
425 );
426 Err(TransactionError::OneOrMoreRingMembersLocked)
427 } else {
428 Ok(())
429 }
430 } else {
431 Ok(())
432 }
433}
434
435fn sum_inputs_check_overflow(inputs: &[Input]) -> Result<u64, TransactionError> {
439 let mut sum: u64 = 0;
440 for inp in inputs {
441 match inp {
442 Input::ToKey { amount, .. } => {
443 sum = sum
444 .checked_add(amount.unwrap_or(0))
445 .ok_or(TransactionError::InputsOverflow)?;
446 }
447 Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
448 }
449 }
450
451 Ok(sum)
452}
453
454fn check_inputs_semantics(inputs: &[Input], hf: HardFork) -> Result<u64, TransactionError> {
460 if inputs.is_empty() {
462 return Err(TransactionError::NoInputs);
463 }
464
465 for input in inputs {
466 check_input_type(input)?;
467 check_input_has_decoys(input)?;
468
469 check_ring_members_unique(input, hf)?;
470 }
471
472 check_inputs_sorted(inputs, hf)?;
473
474 sum_inputs_check_overflow(inputs)
475}
476
477fn check_inputs_contextual(
483 inputs: &[Input],
484 tx_ring_members_info: &TxRingMembersInfo,
485 current_chain_height: usize,
486 hf: HardFork,
487) -> Result<(), TransactionError> {
488 if tx_ring_members_info.youngest_used_out_height >= current_chain_height {
492 tracing::debug!("Transaction invalid: One or more ring members too young.");
493 return Err(TransactionError::OneOrMoreRingMembersLocked);
494 }
495
496 check_10_block_lock(
497 tx_ring_members_info.youngest_used_out_height,
498 current_chain_height,
499 hf,
500 )?;
501
502 if let Some(decoys_info) = &tx_ring_members_info.decoy_info {
503 check_decoy_info(decoys_info, hf)?;
504 } else {
505 assert_eq!(hf, HardFork::V1);
506 }
507
508 for input in inputs {
509 check_key_images(input)?;
510 }
511
512 Ok(())
513}
514
515fn check_tx_version(
521 decoy_info: &Option<DecoyInfo>,
522 version: TxVersion,
523 hf: HardFork,
524) -> Result<(), TransactionError> {
525 if let Some(decoy_info) = decoy_info {
526 let max = max_tx_version(hf);
527 if version > max {
528 return Err(TransactionError::TransactionVersionInvalid);
529 }
530
531 let min = min_tx_version(hf);
532 if version < min && decoy_info.not_mixable == 0 {
533 return Err(TransactionError::TransactionVersionInvalid);
534 }
535 } else {
536 if version != TxVersion::RingSignatures {
538 return Err(TransactionError::TransactionVersionInvalid);
539 }
540 }
541
542 Ok(())
543}
544
545fn max_tx_version(hf: HardFork) -> TxVersion {
547 if hf <= HardFork::V3 {
548 TxVersion::RingSignatures
549 } else {
550 TxVersion::RingCT
551 }
552}
553
554fn min_tx_version(hf: HardFork) -> TxVersion {
556 if hf >= HardFork::V6 {
557 TxVersion::RingCT
558 } else {
559 TxVersion::RingSignatures
560 }
561}
562
563fn transaction_weight_limit(hf: HardFork) -> usize {
564 penalty_free_zone(hf) / 2 - 600
565}
566
567pub fn check_transaction_semantic(
576 tx: &Transaction,
577 tx_blob_size: usize,
578 tx_weight: usize,
579 tx_hash: &[u8; 32],
580 hf: HardFork,
581 verifier: impl BatchVerifier,
582) -> Result<u64, TransactionError> {
583 if tx_blob_size > MAX_TX_BLOB_SIZE
585 || (hf >= HardFork::V8 && tx_weight > transaction_weight_limit(hf))
586 {
587 return Err(TransactionError::TooBig);
588 }
589
590 let tx_version =
591 TxVersion::from_raw(tx.version()).ok_or(TransactionError::TransactionVersionInvalid)?;
592
593 let bp_or_bpp = match tx {
594 Transaction::V2 {
595 proofs: Some(proofs),
596 ..
597 } => match proofs.rct_type() {
598 RctType::AggregateMlsagBorromean | RctType::MlsagBorromean => false,
599 RctType::MlsagBulletproofs
600 | RctType::MlsagBulletproofsCompactAmount
601 | RctType::ClsagBulletproof
602 | RctType::ClsagBulletproofPlus => true,
603 },
604 Transaction::V2 { proofs: None, .. } | Transaction::V1 { .. } => false,
605 };
606
607 let outputs_sum = check_outputs_semantics(&tx.prefix().outputs, hf, tx_version, bp_or_bpp)?;
608 let inputs_sum = check_inputs_semantics(&tx.prefix().inputs, hf)?;
609
610 let fee = match tx {
611 Transaction::V1 { .. } => {
612 if outputs_sum >= inputs_sum {
613 return Err(TransactionError::OutputsTooHigh);
614 }
615 inputs_sum - outputs_sum
616 }
617 Transaction::V2 { proofs, .. } => {
618 let proofs = proofs
619 .as_ref()
620 .ok_or(TransactionError::TransactionVersionInvalid)?;
621
622 ring_ct::ring_ct_semantic_checks(proofs, tx_hash, verifier, hf)?;
623
624 proofs.base.fee
625 }
626 };
627
628 Ok(fee)
629}
630
631pub fn check_transaction_contextual(
639 tx: &Transaction,
640 tx_ring_members_info: &TxRingMembersInfo,
641 current_chain_height: usize,
642 current_time_lock_timestamp: u64,
643 hf: HardFork,
644) -> Result<(), TransactionError> {
645 let tx_version =
646 TxVersion::from_raw(tx.version()).ok_or(TransactionError::TransactionVersionInvalid)?;
647
648 check_inputs_contextual(
649 &tx.prefix().inputs,
650 tx_ring_members_info,
651 current_chain_height,
652 hf,
653 )?;
654 check_tx_version(&tx_ring_members_info.decoy_info, tx_version, hf)?;
655
656 check_all_time_locks(
657 &tx_ring_members_info.time_locked_outs,
658 current_chain_height,
659 current_time_lock_timestamp,
660 hf,
661 )?;
662
663 match &tx {
664 Transaction::V1 { prefix, signatures } => ring_signatures::check_input_signatures(
665 &prefix.inputs,
666 signatures,
667 &tx_ring_members_info.rings,
668 &tx.signature_hash()
670 .ok_or(TransactionError::TransactionVersionInvalid)?,
671 ),
672 Transaction::V2 { prefix, proofs } => Ok(ring_ct::check_input_signatures(
673 &tx.signature_hash()
674 .ok_or(TransactionError::TransactionVersionInvalid)?,
675 &prefix.inputs,
676 proofs
677 .as_ref()
678 .ok_or(TransactionError::TransactionVersionInvalid)?,
679 &tx_ring_members_info.rings,
680 )?),
681 }
682}