cuprate_consensus_rules/
transactions.rs

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
15// re-export.
16pub 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    //-------------------------------------------------------- OUTPUTS
37    #[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    //-------------------------------------------------------- INPUTS
54    #[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    //-------------------------------------------------------- Ring Signatures
77    #[error("Ring signature incorrect.")]
78    RingSignatureIncorrect,
79    //-------------------------------------------------------- RingCT
80    #[error("RingCT Error: {0}.")]
81    RingCTError(#[from] RingCTError),
82}
83
84//----------------------------------------------------------------------------------------------------------- OUTPUTS
85
86/// Checks the output keys are canonically encoded points.
87///
88/// <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-keys-canonical>
89fn 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
99/// Checks the output types are allowed for the given hard-fork.
100///
101/// This is also used during miner-tx verification.
102///
103/// <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-type>
104/// <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#output-type>
105pub(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
125/// Checks the individual outputs amount for version 1 txs.
126///
127/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
128fn 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
140/// Checks the individual outputs amount for version 2 txs.
141///
142/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
143const fn check_output_amount_v2(amount: u64) -> Result<(), TransactionError> {
144    if amount == 0 {
145        Ok(())
146    } else {
147        Err(TransactionError::NonZeroOutputForV2)
148    }
149}
150
151/// Sums the outputs, checking for overflow and other consensus rules.
152///
153/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
154/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#outputs-must-not-overflow>
155fn 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
177/// Checks the number of outputs is allowed.
178///
179/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#2-outputs>
180/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs.html#max-outputs>
181/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs+.html#max-outputs>
182fn 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
203/// Checks the outputs against all output consensus rules, returning the sum of the output amounts.
204///
205/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html>
206/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs.html#max-outputs>
207/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs+.html#max-outputs>
208fn 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
221//----------------------------------------------------------------------------------------------------------- TIME LOCKS
222
223/// Checks if an outputs unlock time has passed.
224///
225/// <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html>
226pub 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
243/// Returns if a locked output, which uses a block height, can be spent.
244///
245/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#block-height>
246const fn check_block_time_lock(unlock_height: usize, current_chain_height: usize) -> bool {
247    // current_chain_height = 1 + top height
248    unlock_height <= current_chain_height
249}
250
251/// Returns if a locked output, which uses a block height, can be spent.
252///
253/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#timestamp>
254const 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
262/// Checks all the time locks are unlocked.
263///
264/// `current_time_lock_timestamp` must be: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#getting-the-current-time>
265///
266/// <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html>
267/// <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#the-output-must-not-be-locked>
268fn 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
289//----------------------------------------------------------------------------------------------------------- INPUTS
290
291/// Checks the decoys are allowed.
292///
293/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#minimum-decoys>
294/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#equal-number-of-decoys>
295pub fn check_decoy_info(decoy_info: &DecoyInfo, hf: HardFork) -> Result<(), TransactionError> {
296    if hf == HardFork::V15 {
297        // Hard-fork 15 allows both v14 and v16 rules
298        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        // Only allow rings without enough decoys if there aren't enough decoys to mix with.
306        if decoy_info.not_mixable == 0 {
307            return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
308        }
309        // Only allow upto 1 mixable input with unmixable inputs.
310        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        // From V8 enforce the minimum used number of rings is the default minimum.
315        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
316    }
317
318    // From v12 all inputs must have the same number of decoys.
319    if hf >= HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys {
320        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
321    }
322
323    Ok(())
324}
325
326/// Checks the inputs key images for torsion.
327///
328/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#torsion-free-key-image>
329fn check_key_images(input: &Input) -> Result<(), TransactionError> {
330    match input {
331        Input::ToKey { key_image, .. } => {
332            // this happens in monero-serai but we may as well duplicate the check.
333            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
346/// Checks that the input is of type [`Input::ToKey`] aka `txin_to_key`.
347///
348/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#input-type>
349const fn check_input_type(input: &Input) -> Result<(), TransactionError> {
350    match input {
351        Input::ToKey { .. } => Ok(()),
352        Input::Gen(_) => Err(TransactionError::IncorrectInputType),
353    }
354}
355
356/// Checks that the input has decoys.
357///
358/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#no-empty-decoys>
359fn 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
372/// Checks that the ring members for the input are unique after hard-fork 6.
373///
374/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#unique-ring-members>
375fn 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
392/// Checks that from hf 7 the inputs are sorted by key image.
393///
394/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#sorted-inputs>
395fn 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
413/// Checks the youngest output is at least 10 blocks old.
414///
415/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#10-block-lock>
416fn 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
435/// Sums the inputs checking for overflow.
436///
437/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#inputs-must-not-overflow>
438fn 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
454/// Checks the inputs semantically validity, returning the sum of the inputs.
455///
456/// Semantic rules are rules that don't require blockchain context, the hard-fork does not require blockchain context as:
457/// - The tx-pool will use the current hard-fork
458/// - When syncing the hard-fork is in the block header.
459fn check_inputs_semantics(inputs: &[Input], hf: HardFork) -> Result<u64, TransactionError> {
460    // <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#no-empty-inputs>
461    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
477/// Checks the inputs contextual validity.
478///
479/// Contextual rules are rules that require blockchain context to check.
480///
481/// This function does not check signatures or for duplicate key-images.
482fn check_inputs_contextual(
483    inputs: &[Input],
484    tx_ring_members_info: &TxRingMembersInfo,
485    current_chain_height: usize,
486    hf: HardFork,
487) -> Result<(), TransactionError> {
488    // This rule is not contained in monero-core explicitly, but it is enforced by how Monero picks ring members.
489    // When picking ring members monerod will only look in the DB at past blocks so an output has to be younger
490    // than this transaction to be used in this tx.
491    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
515//----------------------------------------------------------------------------------------------------------- OVERALL
516
517/// Checks the version is in the allowed range.
518///
519/// <https://monero-book.cuprate.org/consensus_rules/transactions.html#version>
520fn 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        // This will only happen for hard-fork 1 when only RingSignatures are allowed.
537        if version != TxVersion::RingSignatures {
538            return Err(TransactionError::TransactionVersionInvalid);
539        }
540    }
541
542    Ok(())
543}
544
545/// Returns the default maximum tx version for the given hard-fork.
546fn max_tx_version(hf: HardFork) -> TxVersion {
547    if hf <= HardFork::V3 {
548        TxVersion::RingSignatures
549    } else {
550        TxVersion::RingCT
551    }
552}
553
554/// Returns the default minimum tx version for the given hard-fork.
555fn 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
567/// Checks the transaction is semantically valid.
568///
569/// Semantic rules are rules that don't require blockchain context, the hard-fork does not require blockchain context as:
570/// - The tx-pool will use the current hard-fork
571/// - When syncing the hard-fork is in the block header.
572///
573/// To fully verify a transaction this must be accompanied by [`check_transaction_contextual`]
574///
575pub 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    // <https://monero-book.cuprate.org/consensus_rules/transactions.html#transaction-size>
584    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
631/// Checks the transaction is contextually valid.
632///
633/// To fully verify a transaction this must be accompanied by [`check_transaction_semantic`].
634///
635/// This function also does _not_ check for duplicate key-images: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#unique-key-image>.
636///
637/// `current_time_lock_timestamp` must be: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#getting-the-current-time>.
638pub 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            // This will only return None on v2 miner txs.
669            &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}