Skip to main content

cuprate_consensus_rules/
transactions.rs

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
12// re-export.
13pub 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    //-------------------------------------------------------- OUTPUTS
34    #[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    //-------------------------------------------------------- INPUTS
51    #[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    //-------------------------------------------------------- Ring Signatures
74    #[error("Ring signature incorrect.")]
75    RingSignatureIncorrect,
76    //-------------------------------------------------------- RingCT
77    #[error("RingCT Error: {0}.")]
78    RingCTError(#[from] RingCTError),
79    #[error("Transaction batch verification failed.")]
80    BatchVerificationFailed,
81}
82
83//----------------------------------------------------------------------------------------------------------- OUTPUTS
84
85/// Checks the output keys are canonically encoded points.
86///
87/// <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-keys-canonical>
88fn 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
98/// Checks the output types are allowed for the given hard-fork.
99///
100/// This is also used during miner-tx verification.
101///
102/// <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-type>
103/// <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#output-type>
104pub(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
124/// Checks the individual outputs amount for version 1 txs.
125///
126/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
127fn 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
139/// Checks the individual outputs amount for version 2 txs.
140///
141/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
142const fn check_output_amount_v2(amount: u64) -> Result<(), TransactionError> {
143    if amount == 0 {
144        Ok(())
145    } else {
146        Err(TransactionError::NonZeroOutputForV2)
147    }
148}
149
150/// Sums the outputs, checking for overflow and other consensus rules.
151///
152/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
153/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#outputs-must-not-overflow>
154fn 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
176/// Checks the number of outputs is allowed.
177///
178/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#2-outputs>
179/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs.html#max-outputs>
180/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs+.html#max-outputs>
181fn 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
202/// Checks the outputs against all output consensus rules, returning the sum of the output amounts.
203///
204/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html>
205/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs.html#max-outputs>
206/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs+.html#max-outputs>
207fn 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
220//----------------------------------------------------------------------------------------------------------- TIME LOCKS
221
222/// Checks if an outputs unlock time has passed.
223///
224/// <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html>
225pub 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
242/// Returns if a locked output, which uses a block height, can be spent.
243///
244/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#block-height>
245const fn check_block_time_lock(unlock_height: usize, current_chain_height: usize) -> bool {
246    // current_chain_height = 1 + top height
247    unlock_height <= current_chain_height
248}
249
250/// Returns if a locked output, which uses a block height, can be spent.
251///
252/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#timestamp>
253const 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
261/// Checks all the time locks are unlocked.
262///
263/// `current_time_lock_timestamp` must be: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#getting-the-current-time>
264///
265/// <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html>
266/// <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#the-output-must-not-be-locked>
267fn 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
288//----------------------------------------------------------------------------------------------------------- INPUTS
289
290/// Checks the decoys are allowed.
291///
292/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#minimum-decoys>
293/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#equal-number-of-decoys>
294pub fn check_decoy_info(decoy_info: &DecoyInfo, hf: HardFork) -> Result<(), TransactionError> {
295    if hf == HardFork::V15 {
296        // Hard-fork 15 allows both v14 and v16 rules
297        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        // Only allow rings without enough decoys if there aren't enough decoys to mix with.
305        if decoy_info.not_mixable == 0 {
306            return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
307        }
308        // Only allow upto 1 mixable input with unmixable inputs.
309        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        // From V8 enforce the minimum used number of rings is the default minimum.
314        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
315    }
316
317    // From v12 all inputs must have the same number of decoys.
318    if hf >= HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys {
319        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
320    }
321
322    Ok(())
323}
324
325/// Checks the inputs key images for torsion.
326///
327/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#torsion-free-key-image>
328fn check_key_images(input: &Input) -> Result<(), TransactionError> {
329    match input {
330        Input::ToKey { key_image, .. } => {
331            // this happens in monero-oxide but we may as well duplicate the check.
332            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
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>
359const 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
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            if get_ki(&inps[0])? <= get_ki(&inps[1])? {
404                return Err(TransactionError::InputsAreNotOrdered);
405            }
406        }
407    }
408
409    Ok(())
410}
411
412/// Checks the youngest output is at least 10 blocks old.
413///
414/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#10-block-lock>
415fn 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
434/// Sums the inputs checking for overflow.
435///
436/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#inputs-must-not-overflow>
437fn 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
453/// Checks the inputs semantically validity, returning the sum of the inputs.
454///
455/// Semantic rules are rules that don't require blockchain context, the hard-fork does not require blockchain context as:
456/// - The tx-pool will use the current hard-fork
457/// - When syncing the hard-fork is in the block header.
458fn check_inputs_semantics(inputs: &[Input], hf: HardFork) -> Result<u64, TransactionError> {
459    // <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#no-empty-inputs>
460    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
476/// Checks the inputs contextual validity.
477///
478/// Contextual rules are rules that require blockchain context to check.
479///
480/// This function does not check signatures or for duplicate key-images.
481fn 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
506//----------------------------------------------------------------------------------------------------------- OVERALL
507
508/// Checks the version is in the allowed range.
509///
510/// <https://monero-book.cuprate.org/consensus_rules/transactions.html#version>
511fn 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        // This will only happen for hard-fork 1 when only RingSignatures are allowed.
528        if version != TxVersion::RingSignatures {
529            return Err(TransactionError::TransactionVersionInvalid);
530        }
531    }
532
533    Ok(())
534}
535
536/// Returns the default maximum tx version for the given hard-fork.
537fn max_tx_version(hf: HardFork) -> TxVersion {
538    if hf <= HardFork::V3 {
539        TxVersion::RingSignatures
540    } else {
541        TxVersion::RingCT
542    }
543}
544
545/// Returns the default minimum tx version for the given hard-fork.
546fn 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
558/// Checks the transaction is semantically valid.
559///
560/// Semantic rules are rules that don't require blockchain context, the hard-fork does not require blockchain context as:
561/// - The tx-pool will use the current hard-fork
562/// - When syncing the hard-fork is in the block header.
563///
564/// To fully verify a transaction this must be accompanied by [`check_transaction_contextual`]
565///
566pub 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    // <https://monero-book.cuprate.org/consensus_rules/transactions.html#transaction-size>
575    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
622/// Checks the transaction is contextually valid.
623///
624/// To fully verify a transaction this must be accompanied by [`check_transaction_semantic`].
625///
626/// This function also does _not_ check for duplicate key-images: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#unique-key-image>.
627///
628/// `current_time_lock_timestamp` must be: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#getting-the-current-time>.
629pub 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            // This will only return None on v2 miner txs.
660            &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}