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}
80
81//----------------------------------------------------------------------------------------------------------- OUTPUTS
82
83/// Checks the output keys are canonically encoded points.
84///
85/// <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-keys-canonical>
86fn check_output_keys(outputs: &[Output]) -> Result<(), TransactionError> {
87    for out in outputs {
88        if out.key.decompress().is_none() {
89            return Err(TransactionError::OutputNotValidPoint);
90        }
91    }
92
93    Ok(())
94}
95
96/// Checks the output types are allowed for the given hard-fork.
97///
98/// This is also used during miner-tx verification.
99///
100/// <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-type>
101/// <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#output-type>
102pub(crate) fn check_output_types(outputs: &[Output], hf: HardFork) -> Result<(), TransactionError> {
103    if hf == HardFork::V15 {
104        for outs in outputs.windows(2) {
105            if outs[0].view_tag.is_some() != outs[1].view_tag.is_some() {
106                return Err(TransactionError::OutputTypeInvalid);
107            }
108        }
109        return Ok(());
110    }
111
112    for out in outputs {
113        if hf <= HardFork::V14 && out.view_tag.is_some()
114            || hf >= HardFork::V16 && out.view_tag.is_none()
115        {
116            return Err(TransactionError::OutputTypeInvalid);
117        }
118    }
119    Ok(())
120}
121
122/// Checks the individual outputs amount for version 1 txs.
123///
124/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
125fn check_output_amount_v1(amount: u64, hf: HardFork) -> Result<(), TransactionError> {
126    if amount == 0 {
127        return Err(TransactionError::ZeroOutputForV1);
128    }
129
130    if hf >= HardFork::V2 && !is_decomposed_amount(&amount) {
131        return Err(TransactionError::AmountNotDecomposed);
132    }
133
134    Ok(())
135}
136
137/// Checks the individual outputs amount for version 2 txs.
138///
139/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
140const fn check_output_amount_v2(amount: u64) -> Result<(), TransactionError> {
141    if amount == 0 {
142        Ok(())
143    } else {
144        Err(TransactionError::NonZeroOutputForV2)
145    }
146}
147
148/// Sums the outputs, checking for overflow and other consensus rules.
149///
150/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#output-amount>
151/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#outputs-must-not-overflow>
152fn sum_outputs(
153    outputs: &[Output],
154    hf: HardFork,
155    tx_version: TxVersion,
156) -> Result<u64, TransactionError> {
157    let mut sum: u64 = 0;
158
159    for out in outputs {
160        let raw_amount = out.amount.unwrap_or(0);
161
162        match tx_version {
163            TxVersion::RingSignatures => check_output_amount_v1(raw_amount, hf)?,
164            TxVersion::RingCT => check_output_amount_v2(raw_amount)?,
165        }
166        sum = sum
167            .checked_add(raw_amount)
168            .ok_or(TransactionError::OutputsOverflow)?;
169    }
170
171    Ok(sum)
172}
173
174/// Checks the number of outputs is allowed.
175///
176/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html#2-outputs>
177/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs.html#max-outputs>
178/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs+.html#max-outputs>
179fn check_number_of_outputs(
180    outputs: usize,
181    hf: HardFork,
182    tx_version: TxVersion,
183    bp_or_bpp: bool,
184) -> Result<(), TransactionError> {
185    if tx_version == TxVersion::RingSignatures {
186        return Ok(());
187    }
188
189    if hf >= HardFork::V12 && outputs < 2 {
190        return Err(TransactionError::InvalidNumberOfOutputs);
191    }
192
193    if bp_or_bpp && outputs > MAX_BULLETPROOFS_OUTPUTS {
194        Err(TransactionError::InvalidNumberOfOutputs)
195    } else {
196        Ok(())
197    }
198}
199
200/// Checks the outputs against all output consensus rules, returning the sum of the output amounts.
201///
202/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/outputs.html>
203/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs.html#max-outputs>
204/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/ring_ct/bulletproofs+.html#max-outputs>
205fn check_outputs_semantics(
206    outputs: &[Output],
207    hf: HardFork,
208    tx_version: TxVersion,
209    bp_or_bpp: bool,
210) -> Result<u64, TransactionError> {
211    check_output_types(outputs, hf)?;
212    check_output_keys(outputs)?;
213    check_number_of_outputs(outputs.len(), hf, tx_version, bp_or_bpp)?;
214
215    sum_outputs(outputs, hf, tx_version)
216}
217
218//----------------------------------------------------------------------------------------------------------- TIME LOCKS
219
220/// Checks if an outputs unlock time has passed.
221///
222/// <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html>
223pub const fn output_unlocked(
224    time_lock: &Timelock,
225    current_chain_height: usize,
226    current_time_lock_timestamp: u64,
227    hf: HardFork,
228) -> bool {
229    match *time_lock {
230        Timelock::None => true,
231        Timelock::Block(unlock_height) => {
232            check_block_time_lock(unlock_height, current_chain_height)
233        }
234        Timelock::Time(unlock_time) => {
235            check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf)
236        }
237    }
238}
239
240/// Returns if a locked output, which uses a block height, can be spent.
241///
242/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#block-height>
243const fn check_block_time_lock(unlock_height: usize, current_chain_height: usize) -> bool {
244    // current_chain_height = 1 + top height
245    unlock_height <= current_chain_height
246}
247
248/// Returns if a locked output, which uses a block height, can be spent.
249///
250/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#timestamp>
251const fn check_timestamp_time_lock(
252    unlock_timestamp: u64,
253    current_time_lock_timestamp: u64,
254    hf: HardFork,
255) -> bool {
256    current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
257}
258
259/// Checks all the time locks are unlocked.
260///
261/// `current_time_lock_timestamp` must be: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#getting-the-current-time>
262///
263/// <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html>
264/// <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#the-output-must-not-be-locked>
265fn check_all_time_locks(
266    time_locks: &[Timelock],
267    current_chain_height: usize,
268    current_time_lock_timestamp: u64,
269    hf: HardFork,
270) -> Result<(), TransactionError> {
271    time_locks.iter().try_for_each(|time_lock| {
272        if output_unlocked(
273            time_lock,
274            current_chain_height,
275            current_time_lock_timestamp,
276            hf,
277        ) {
278            Ok(())
279        } else {
280            tracing::debug!("Transaction invalid: one or more inputs locked, lock: {time_lock:?}.");
281            Err(TransactionError::OneOrMoreRingMembersLocked)
282        }
283    })
284}
285
286//----------------------------------------------------------------------------------------------------------- INPUTS
287
288/// Checks the decoys are allowed.
289///
290/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#minimum-decoys>
291/// &&   <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#equal-number-of-decoys>
292pub fn check_decoy_info(decoy_info: &DecoyInfo, hf: HardFork) -> Result<(), TransactionError> {
293    if hf == HardFork::V15 {
294        // Hard-fork 15 allows both v14 and v16 rules
295        return check_decoy_info(decoy_info, HardFork::V14)
296            .or_else(|_| check_decoy_info(decoy_info, HardFork::V16));
297    }
298
299    let current_minimum_decoys = minimum_decoys(hf);
300
301    if decoy_info.min_decoys < current_minimum_decoys {
302        // Only allow rings without enough decoys if there aren't enough decoys to mix with.
303        if decoy_info.not_mixable == 0 {
304            return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
305        }
306        // Only allow upto 1 mixable input with unmixable inputs.
307        if decoy_info.mixable > 1 {
308            return Err(TransactionError::MoreThanOneMixableInputWithUnmixable);
309        }
310    } else if hf >= HardFork::V8 && decoy_info.min_decoys != current_minimum_decoys {
311        // From V8 enforce the minimum used number of rings is the default minimum.
312        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
313    }
314
315    // From v12 all inputs must have the same number of decoys.
316    if hf >= HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys {
317        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
318    }
319
320    Ok(())
321}
322
323/// Checks the inputs key images for torsion.
324///
325/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#torsion-free-key-image>
326fn check_key_images(input: &Input) -> Result<(), TransactionError> {
327    match input {
328        Input::ToKey { key_image, .. } => {
329            // this happens in monero-oxide but we may as well duplicate the check.
330            if !key_image.decompress().as_ref().is_some_and(|p| {
331                let p = Point::into(*p);
332
333                p.is_torsion_free() && !p.is_identity()
334            }) {
335                return Err(TransactionError::KeyImageIsNotInPrimeSubGroup);
336            }
337        }
338        Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
339    }
340
341    Ok(())
342}
343
344/// Checks that the input is of type [`Input::ToKey`] aka `txin_to_key`.
345///
346/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#input-type>
347const fn check_input_type(input: &Input) -> Result<(), TransactionError> {
348    match input {
349        Input::ToKey { .. } => Ok(()),
350        Input::Gen(_) => Err(TransactionError::IncorrectInputType),
351    }
352}
353
354/// Checks that the input has decoys.
355///
356/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#no-empty-decoys>
357const fn check_input_has_decoys(input: &Input) -> Result<(), TransactionError> {
358    match input {
359        Input::ToKey { key_offsets, .. } => {
360            if key_offsets.is_empty() {
361                Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys)
362            } else {
363                Ok(())
364            }
365        }
366        Input::Gen(_) => Err(TransactionError::IncorrectInputType),
367    }
368}
369
370/// Checks that the ring members for the input are unique after hard-fork 6.
371///
372/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#unique-ring-members>
373fn check_ring_members_unique(input: &Input, hf: HardFork) -> Result<(), TransactionError> {
374    if hf >= HardFork::V6 {
375        match input {
376            Input::ToKey { key_offsets, .. } => key_offsets.iter().skip(1).try_for_each(|offset| {
377                if *offset == 0 {
378                    Err(TransactionError::DuplicateRingMember)
379                } else {
380                    Ok(())
381                }
382            }),
383            Input::Gen(_) => Err(TransactionError::IncorrectInputType),
384        }
385    } else {
386        Ok(())
387    }
388}
389
390/// Checks that from hf 7 the inputs are sorted by key image.
391///
392/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#sorted-inputs>
393fn check_inputs_sorted(inputs: &[Input], hf: HardFork) -> Result<(), TransactionError> {
394    let get_ki = |inp: &Input| match inp {
395        Input::ToKey { key_image, .. } => Ok(key_image.to_bytes()),
396        Input::Gen(_) => Err(TransactionError::IncorrectInputType),
397    };
398
399    if hf >= HardFork::V7 {
400        for inps in inputs.windows(2) {
401            if get_ki(&inps[0])? <= get_ki(&inps[1])? {
402                return Err(TransactionError::InputsAreNotOrdered);
403            }
404        }
405    }
406
407    Ok(())
408}
409
410/// Checks the youngest output is at least 10 blocks old.
411///
412/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#10-block-lock>
413fn check_10_block_lock(
414    youngest_used_out_height: usize,
415    current_chain_height: usize,
416    hf: HardFork,
417) -> Result<(), TransactionError> {
418    if hf >= HardFork::V12 {
419        if youngest_used_out_height + 10 > current_chain_height {
420            tracing::debug!(
421                "Transaction invalid: One or more ring members younger than 10 blocks."
422            );
423            Err(TransactionError::OneOrMoreRingMembersLocked)
424        } else {
425            Ok(())
426        }
427    } else {
428        Ok(())
429    }
430}
431
432/// Sums the inputs checking for overflow.
433///
434/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#inputs-must-not-overflow>
435fn sum_inputs_check_overflow(inputs: &[Input]) -> Result<u64, TransactionError> {
436    let mut sum: u64 = 0;
437    for inp in inputs {
438        match inp {
439            Input::ToKey { amount, .. } => {
440                sum = sum
441                    .checked_add(amount.unwrap_or(0))
442                    .ok_or(TransactionError::InputsOverflow)?;
443            }
444            Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
445        }
446    }
447
448    Ok(sum)
449}
450
451/// Checks the inputs semantically validity, returning the sum of the inputs.
452///
453/// Semantic rules are rules that don't require blockchain context, the hard-fork does not require blockchain context as:
454/// - The tx-pool will use the current hard-fork
455/// - When syncing the hard-fork is in the block header.
456fn check_inputs_semantics(inputs: &[Input], hf: HardFork) -> Result<u64, TransactionError> {
457    // <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#no-empty-inputs>
458    if inputs.is_empty() {
459        return Err(TransactionError::NoInputs);
460    }
461
462    for input in inputs {
463        check_input_type(input)?;
464        check_input_has_decoys(input)?;
465
466        check_ring_members_unique(input, hf)?;
467    }
468
469    check_inputs_sorted(inputs, hf)?;
470
471    sum_inputs_check_overflow(inputs)
472}
473
474/// Checks the inputs contextual validity.
475///
476/// Contextual rules are rules that require blockchain context to check.
477///
478/// This function does not check signatures or for duplicate key-images.
479fn check_inputs_contextual(
480    inputs: &[Input],
481    tx_ring_members_info: &TxRingMembersInfo,
482    current_chain_height: usize,
483    hf: HardFork,
484) -> Result<(), TransactionError> {
485    check_10_block_lock(
486        tx_ring_members_info.youngest_used_out_height,
487        current_chain_height,
488        hf,
489    )?;
490
491    if let Some(decoys_info) = &tx_ring_members_info.decoy_info {
492        check_decoy_info(decoys_info, hf)?;
493    } else {
494        assert_eq!(hf, HardFork::V1);
495    }
496
497    for input in inputs {
498        check_key_images(input)?;
499    }
500
501    Ok(())
502}
503
504//----------------------------------------------------------------------------------------------------------- OVERALL
505
506/// Checks the version is in the allowed range.
507///
508/// <https://monero-book.cuprate.org/consensus_rules/transactions.html#version>
509fn check_tx_version(
510    decoy_info: &Option<DecoyInfo>,
511    version: TxVersion,
512    hf: HardFork,
513) -> Result<(), TransactionError> {
514    if let Some(decoy_info) = decoy_info {
515        let max = max_tx_version(hf);
516        if version > max {
517            return Err(TransactionError::TransactionVersionInvalid);
518        }
519
520        let min = min_tx_version(hf);
521        if version < min && decoy_info.not_mixable == 0 {
522            return Err(TransactionError::TransactionVersionInvalid);
523        }
524    } else {
525        // This will only happen for hard-fork 1 when only RingSignatures are allowed.
526        if version != TxVersion::RingSignatures {
527            return Err(TransactionError::TransactionVersionInvalid);
528        }
529    }
530
531    Ok(())
532}
533
534/// Returns the default maximum tx version for the given hard-fork.
535fn max_tx_version(hf: HardFork) -> TxVersion {
536    if hf <= HardFork::V3 {
537        TxVersion::RingSignatures
538    } else {
539        TxVersion::RingCT
540    }
541}
542
543/// Returns the default minimum tx version for the given hard-fork.
544fn min_tx_version(hf: HardFork) -> TxVersion {
545    if hf >= HardFork::V6 {
546        TxVersion::RingCT
547    } else {
548        TxVersion::RingSignatures
549    }
550}
551
552fn transaction_weight_limit(hf: HardFork) -> usize {
553    penalty_free_zone(hf) / 2 - 600
554}
555
556/// Checks the transaction is semantically valid.
557///
558/// Semantic rules are rules that don't require blockchain context, the hard-fork does not require blockchain context as:
559/// - The tx-pool will use the current hard-fork
560/// - When syncing the hard-fork is in the block header.
561///
562/// To fully verify a transaction this must be accompanied by [`check_transaction_contextual`]
563///
564pub fn check_transaction_semantic(
565    tx: &Transaction,
566    tx_blob_size: usize,
567    tx_weight: usize,
568    tx_hash: &[u8; 32],
569    hf: HardFork,
570    verifier: impl BatchVerifier,
571) -> Result<u64, TransactionError> {
572    // <https://monero-book.cuprate.org/consensus_rules/transactions.html#transaction-size>
573    if tx_blob_size > MAX_TX_BLOB_SIZE
574        || (hf >= HardFork::V8 && tx_weight > transaction_weight_limit(hf))
575    {
576        return Err(TransactionError::TooBig);
577    }
578
579    let tx_version =
580        TxVersion::from_raw(tx.version()).ok_or(TransactionError::TransactionVersionInvalid)?;
581
582    let bp_or_bpp = match tx {
583        Transaction::V2 {
584            proofs: Some(proofs),
585            ..
586        } => match proofs.rct_type() {
587            RctType::AggregateMlsagBorromean | RctType::MlsagBorromean => false,
588            RctType::MlsagBulletproofs
589            | RctType::MlsagBulletproofsCompactAmount
590            | RctType::ClsagBulletproof
591            | RctType::ClsagBulletproofPlus => true,
592        },
593        Transaction::V2 { proofs: None, .. } | Transaction::V1 { .. } => false,
594    };
595
596    let outputs_sum = check_outputs_semantics(&tx.prefix().outputs, hf, tx_version, bp_or_bpp)?;
597    let inputs_sum = check_inputs_semantics(&tx.prefix().inputs, hf)?;
598
599    let fee = match tx {
600        Transaction::V1 { .. } => {
601            if outputs_sum >= inputs_sum {
602                return Err(TransactionError::OutputsTooHigh);
603            }
604            inputs_sum - outputs_sum
605        }
606        Transaction::V2 { proofs, .. } => {
607            let proofs = proofs
608                .as_ref()
609                .ok_or(TransactionError::TransactionVersionInvalid)?;
610
611            ring_ct::ring_ct_semantic_checks(proofs, tx_hash, verifier, hf)?;
612
613            proofs.base.fee
614        }
615    };
616
617    Ok(fee)
618}
619
620/// Checks the transaction is contextually valid.
621///
622/// To fully verify a transaction this must be accompanied by [`check_transaction_semantic`].
623///
624/// This function also does _not_ check for duplicate key-images: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#unique-key-image>.
625///
626/// `current_time_lock_timestamp` must be: <https://monero-book.cuprate.org/consensus_rules/transactions/unlock_time.html#getting-the-current-time>.
627pub fn check_transaction_contextual(
628    tx: &Transaction,
629    tx_ring_members_info: &TxRingMembersInfo,
630    current_chain_height: usize,
631    current_time_lock_timestamp: u64,
632    hf: HardFork,
633) -> Result<(), TransactionError> {
634    let tx_version =
635        TxVersion::from_raw(tx.version()).ok_or(TransactionError::TransactionVersionInvalid)?;
636
637    check_inputs_contextual(
638        &tx.prefix().inputs,
639        tx_ring_members_info,
640        current_chain_height,
641        hf,
642    )?;
643    check_tx_version(&tx_ring_members_info.decoy_info, tx_version, hf)?;
644
645    check_all_time_locks(
646        &tx_ring_members_info.time_locked_outs,
647        current_chain_height,
648        current_time_lock_timestamp,
649        hf,
650    )?;
651
652    match &tx {
653        Transaction::V1 { prefix, signatures } => ring_signatures::check_input_signatures(
654            &prefix.inputs,
655            signatures,
656            &tx_ring_members_info.rings,
657            // This will only return None on v2 miner txs.
658            &tx.signature_hash()
659                .ok_or(TransactionError::TransactionVersionInvalid)?,
660        ),
661        Transaction::V2 { prefix, proofs } => Ok(ring_ct::check_input_signatures(
662            &tx.signature_hash()
663                .ok_or(TransactionError::TransactionVersionInvalid)?,
664            &prefix.inputs,
665            proofs
666                .as_ref()
667                .ok_or(TransactionError::TransactionVersionInvalid)?,
668            &tx_ring_members_info.rings,
669        )?),
670    }
671}