cuprate_consensus_rules/
transactions.rs

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
11// re-export.
12pub 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    //-------------------------------------------------------- OUTPUTS
33    #[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    //-------------------------------------------------------- INPUTS
50    #[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    //-------------------------------------------------------- Ring Signatures
73    #[error("Ring signature incorrect.")]
74    RingSignatureIncorrect,
75    //-------------------------------------------------------- RingCT
76    #[error("RingCT Error: {0}.")]
77    RingCTError(#[from] RingCTError),
78}
79
80//----------------------------------------------------------------------------------------------------------- OUTPUTS
81
82/// Checks the output keys are canonically encoded points.
83///
84/// <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-keys-canonical>
85fn 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
95/// Checks the output types are allowed for the given hard-fork.
96///
97/// This is also used during miner-tx verification.
98///
99/// <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-type>
100/// <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#output-type>
101pub(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
121/// Checks the individual outputs amount for version 1 txs.
122///
123/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
124fn 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
136/// Checks the individual outputs amount for version 2 txs.
137///
138/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
139const fn check_output_amount_v2(amount: u64) -> Result<(), TransactionError> {
140    if amount == 0 {
141        Ok(())
142    } else {
143        Err(TransactionError::NonZeroOutputForV2)
144    }
145}
146
147/// Sums the outputs, checking for overflow and other consensus rules.
148///
149/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
150/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#outputs-must-not-overflow>
151fn 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
173/// Checks the number of outputs is allowed.
174///
175/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#2-outputs>
176/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs.html#max-outputs>
177/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs+.html#max-outputs>
178fn 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
199/// Checks the outputs against all output consensus rules, returning the sum of the output amounts.
200///
201/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html>
202/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs.html#max-outputs>
203/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs+.html#max-outputs>
204fn 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
217//----------------------------------------------------------------------------------------------------------- TIME LOCKS
218
219/// Checks if an outputs unlock time has passed.
220///
221/// <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html>
222pub 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
239/// Returns if a locked output, which uses a block height, can be spent.
240///
241/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#block-height>
242const fn check_block_time_lock(unlock_height: usize, current_chain_height: usize) -> bool {
243    // current_chain_height = 1 + top height
244    unlock_height <= current_chain_height
245}
246
247/// Returns if a locked output, which uses a block height, can be spent.
248///
249/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#timestamp>
250const 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
258/// Checks all the time locks are unlocked.
259///
260/// `current_time_lock_timestamp` must be: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#getting-the-current-time>
261///
262/// <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html>
263/// <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#the-output-must-not-be-locked>
264fn 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
285//----------------------------------------------------------------------------------------------------------- INPUTS
286
287/// Checks the decoys are allowed.
288///
289/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#minimum-decoys>
290/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#equal-number-of-decoys>
291pub fn check_decoy_info(decoy_info: &DecoyInfo, hf: HardFork) -> Result<(), TransactionError> {
292    if hf == HardFork::V15 {
293        // Hard-fork 15 allows both v14 and v16 rules
294        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        // Only allow rings without enough decoys if there aren't enough decoys to mix with.
302        if decoy_info.not_mixable == 0 {
303            return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
304        }
305        // Only allow upto 1 mixable input with unmixable inputs.
306        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        // From V8 enforce the minimum used number of rings is the default minimum.
311        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
312    }
313
314    // From v12 all inputs must have the same number of decoys.
315    if hf >= HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys {
316        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
317    }
318
319    Ok(())
320}
321
322/// Checks the inputs key images for torsion.
323///
324/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#torsion-free-key-image>
325fn check_key_images(input: &Input) -> Result<(), TransactionError> {
326    match input {
327        Input::ToKey { key_image, .. } => {
328            // this happens in monero-oxide but we may as well duplicate the check.
329            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
343/// Checks that the input is of type [`Input::ToKey`] aka `txin_to_key`.
344///
345/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#input-type>
346const fn check_input_type(input: &Input) -> Result<(), TransactionError> {
347    match input {
348        Input::ToKey { .. } => Ok(()),
349        Input::Gen(_) => Err(TransactionError::IncorrectInputType),
350    }
351}
352
353/// Checks that the input has decoys.
354///
355/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#no-empty-decoys>
356const 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
369/// Checks that the ring members for the input are unique after hard-fork 6.
370///
371/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#unique-ring-members>
372fn 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
389/// Checks that from hf 7 the inputs are sorted by key image.
390///
391/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#sorted-inputs>
392fn 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
409/// Checks the youngest output is at least 10 blocks old.
410///
411/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#10-block-lock>
412fn 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
431/// Sums the inputs checking for overflow.
432///
433/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#inputs-must-not-overflow>
434fn 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
450/// Checks the inputs semantically validity, returning the sum of the inputs.
451///
452/// Semantic rules are rules that don't require blockchain context, the hard-fork does not require blockchain context as:
453/// - The tx-pool will use the current hard-fork
454/// - When syncing the hard-fork is in the block header.
455fn check_inputs_semantics(inputs: &[Input], hf: HardFork) -> Result<u64, TransactionError> {
456    // <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#no-empty-inputs>
457    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
473/// Checks the inputs contextual validity.
474///
475/// Contextual rules are rules that require blockchain context to check.
476///
477/// This function does not check signatures or for duplicate key-images.
478fn check_inputs_contextual(
479    inputs: &[Input],
480    tx_ring_members_info: &TxRingMembersInfo,
481    current_chain_height: usize,
482    hf: HardFork,
483) -> Result<(), TransactionError> {
484    // This rule is not contained in monero-core explicitly, but it is enforced by how Monero picks ring members.
485    // When picking ring members monerod will only look in the DB at past blocks so an output has to be younger
486    // than this transaction to be used in this tx.
487    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
511//----------------------------------------------------------------------------------------------------------- OVERALL
512
513/// Checks the version is in the allowed range.
514///
515/// <https://monero-book.cuprate.org/consensus_rules/transactions.html#version>
516fn 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        // This will only happen for hard-fork 1 when only RingSignatures are allowed.
533        if version != TxVersion::RingSignatures {
534            return Err(TransactionError::TransactionVersionInvalid);
535        }
536    }
537
538    Ok(())
539}
540
541/// Returns the default maximum tx version for the given hard-fork.
542fn max_tx_version(hf: HardFork) -> TxVersion {
543    if hf <= HardFork::V3 {
544        TxVersion::RingSignatures
545    } else {
546        TxVersion::RingCT
547    }
548}
549
550/// Returns the default minimum tx version for the given hard-fork.
551fn 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
563/// Checks the transaction is semantically valid.
564///
565/// Semantic rules are rules that don't require blockchain context, the hard-fork does not require blockchain context as:
566/// - The tx-pool will use the current hard-fork
567/// - When syncing the hard-fork is in the block header.
568///
569/// To fully verify a transaction this must be accompanied by [`check_transaction_contextual`]
570///
571pub 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    // <https://monero-book.cuprate.org/consensus_rules/transactions.html#transaction-size>
580    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
627/// Checks the transaction is contextually valid.
628///
629/// To fully verify a transaction this must be accompanied by [`check_transaction_semantic`].
630///
631/// This function also does _not_ check for duplicate key-images: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#unique-key-image>.
632///
633/// `current_time_lock_timestamp` must be: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#getting-the-current-time>.
634pub 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            // This will only return None on v2 miner txs.
665            &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}