cuprate_consensus/
transactions.rs

1//! # Transaction Verifier Service.
2//!
3//! This module contains the transaction validation interface, which can be accessed with [`start_tx_verification`].
4//!
5//! Transaction verification states will be cached to prevent doing the expensive checks multiple times.
6//!
7//! ## Example Semantic Verification
8//!
9//! ```rust
10//! # use cuprate_test_utils::data::TX_E2D393;
11//! # use monero_serai::transaction::Transaction;
12//! use cuprate_consensus::{transactions::start_tx_verification, HardFork, batch_verifier::MultiThreadedBatchVerifier};
13//!
14//! # fn main() -> Result<(), tower::BoxError> {
15//! # let tx = Transaction::read(&mut TX_E2D393).unwrap();
16//! let batch_verifier = MultiThreadedBatchVerifier::new(rayon::current_num_threads());
17//!
18//! let tx = start_tx_verification()
19//!              .append_txs(vec![tx])
20//!              .prepare()?
21//!              .only_semantic(HardFork::V9)
22//!              .queue(&batch_verifier)?;
23//!
24//! assert!(batch_verifier.verify());
25//! Ok(())
26//! # }
27//! ```
28use std::collections::HashSet;
29
30use monero_serai::transaction::{Input, Timelock, Transaction};
31use rayon::prelude::*;
32use tower::ServiceExt;
33
34use cuprate_consensus_rules::{
35    transactions::{
36        check_decoy_info, check_transaction_contextual, check_transaction_semantic,
37        output_unlocked, TransactionError,
38    },
39    ConsensusError, HardFork,
40};
41use cuprate_helper::asynch::rayon_spawn_async;
42use cuprate_types::{
43    blockchain::{BlockchainReadRequest, BlockchainResponse},
44    output_cache::OutputCache,
45    CachedVerificationState, TransactionVerificationData, TxVersion,
46};
47
48use crate::{
49    batch_verifier::MultiThreadedBatchVerifier,
50    block::BatchPrepareCache,
51    transactions::contextual_data::{batch_get_decoy_info, batch_get_ring_member_info},
52    Database, ExtendedConsensusError,
53};
54
55pub mod contextual_data;
56mod free;
57
58pub use free::new_tx_verification_data;
59
60/// An enum representing the type of validation that needs to be completed for this transaction.
61#[derive(Debug, Copy, Clone, Eq, PartialEq)]
62enum VerificationNeeded {
63    /// Decoy check on a v1 transaction.
64    V1DecoyCheck,
65    /// Both semantic validation and contextual validation are needed.
66    SemanticAndContextual,
67    /// Only contextual validation is needed.
68    Contextual,
69    /// No verification needed.
70    None,
71}
72
73/// Start the transaction verification process.
74pub const fn start_tx_verification() -> PrepTransactions {
75    PrepTransactions {
76        txs: vec![],
77        prepped_txs: vec![],
78    }
79}
80
81/// The preparation phase of transaction verification.
82///
83/// The order of transactions will be kept throughout the verification process, transactions
84/// inserted with [`PrepTransactions::append_prepped_txs`] will be put before transactions given
85/// in [`PrepTransactions::append_txs`]
86pub struct PrepTransactions {
87    prepped_txs: Vec<TransactionVerificationData>,
88    txs: Vec<Transaction>,
89}
90
91impl PrepTransactions {
92    /// Append some new transactions to prepare.
93    #[must_use]
94    pub fn append_txs(mut self, mut txs: Vec<Transaction>) -> Self {
95        self.txs.append(&mut txs);
96
97        self
98    }
99
100    /// Append some already prepped transactions.
101    #[must_use]
102    pub fn append_prepped_txs(mut self, mut txs: Vec<TransactionVerificationData>) -> Self {
103        self.prepped_txs.append(&mut txs);
104
105        self
106    }
107
108    /// Prepare the transactions and advance to the next step: [`VerificationWanted`].
109    ///
110    /// # [`rayon`]
111    ///
112    /// This function will use [`rayon`] to parallelize the preparation process, so should not be called
113    /// in an async function, unless all the transactions given were already prepared, i.e. [`Self::append_prepped_txs`].
114    pub fn prepare(mut self) -> Result<VerificationWanted, ConsensusError> {
115        if !self.txs.is_empty() {
116            self.prepped_txs.append(
117                &mut self
118                    .txs
119                    .into_par_iter()
120                    .map(new_tx_verification_data)
121                    .collect::<Result<_, _>>()?,
122            );
123        }
124
125        Ok(VerificationWanted {
126            prepped_txs: self.prepped_txs,
127        })
128    }
129}
130
131/// The step where the type of verification is decided.
132pub struct VerificationWanted {
133    prepped_txs: Vec<TransactionVerificationData>,
134}
135
136impl VerificationWanted {
137    /// Only semantic verification.
138    ///
139    /// Semantic verification is verification that can done without other blockchain data. The [`HardFork`]
140    /// is technically other blockchain data but excluding it reduces the amount of things that can be checked
141    /// significantly, and it is easy to get compared to other blockchain data needed for contextual validation.
142    pub fn only_semantic(self, hf: HardFork) -> SemanticVerification {
143        SemanticVerification {
144            prepped_txs: self.prepped_txs,
145            hf,
146        }
147    }
148
149    /// Full verification.
150    ///
151    /// Fully verify the transactions, all checks will be performed, if they were already performed then they
152    /// won't be done again unless necessary.
153    pub fn full<D: Database>(
154        self,
155        current_chain_height: usize,
156        top_hash: [u8; 32],
157        time_for_time_lock: u64,
158        hf: HardFork,
159        database: D,
160        batch_prep_cache: Option<&BatchPrepareCache>,
161    ) -> FullVerification<D> {
162        FullVerification {
163            prepped_txs: self.prepped_txs,
164            current_chain_height,
165            top_hash,
166            time_for_time_lock,
167            hf,
168            database,
169            batch_prep_cache,
170        }
171    }
172}
173
174/// Semantic transaction verification.
175///
176/// [`VerificationWanted::only_semantic`]
177pub struct SemanticVerification {
178    prepped_txs: Vec<TransactionVerificationData>,
179    hf: HardFork,
180}
181
182impl SemanticVerification {
183    /// Perform the semantic checks and queue any checks that can be batched into the batch verifier.
184    ///
185    /// If this function returns [`Ok`] the transaction(s) could still be semantically invalid,
186    /// [`MultiThreadedBatchVerifier::verify`] must be called on the `batch_verifier` after.
187    pub fn queue(
188        mut self,
189        batch_verifier: &MultiThreadedBatchVerifier,
190    ) -> Result<Vec<TransactionVerificationData>, ConsensusError> {
191        self.prepped_txs.par_iter_mut().try_for_each(|tx| {
192            let fee = check_transaction_semantic(
193                &tx.tx,
194                tx.tx_blob.len(),
195                tx.tx_weight,
196                &tx.tx_hash,
197                self.hf,
198                batch_verifier,
199            )?;
200            // make sure we calculated the right fee.
201            assert_eq!(fee, tx.fee);
202
203            tx.cached_verification_state = CachedVerificationState::OnlySemantic(self.hf);
204
205            Ok::<_, ConsensusError>(())
206        })?;
207
208        Ok(self.prepped_txs)
209    }
210}
211
212/// Full transaction verification.
213///
214/// [`VerificationWanted::full`]
215pub struct FullVerification<'a, D> {
216    prepped_txs: Vec<TransactionVerificationData>,
217
218    current_chain_height: usize,
219    top_hash: [u8; 32],
220    time_for_time_lock: u64,
221    hf: HardFork,
222    database: D,
223    batch_prep_cache: Option<&'a BatchPrepareCache>,
224}
225
226impl<D: Database + Clone> FullVerification<'_, D> {
227    /// Fully verify each transaction.
228    pub async fn verify(
229        mut self,
230    ) -> Result<Vec<TransactionVerificationData>, ExtendedConsensusError> {
231        if self
232            .batch_prep_cache
233            .is_none_or(|c| !c.key_images_spent_checked)
234        {
235            check_kis_unique(self.prepped_txs.iter(), &mut self.database).await?;
236        }
237
238        let hashes_in_main_chain =
239            hashes_referenced_in_main_chain(&self.prepped_txs, &mut self.database).await?;
240
241        let (verification_needed, any_v1_decoy_check_needed) = verification_needed(
242            &self.prepped_txs,
243            &hashes_in_main_chain,
244            self.hf,
245            self.current_chain_height,
246            self.time_for_time_lock,
247        )?;
248
249        if any_v1_decoy_check_needed {
250            verify_transactions_decoy_info(
251                self.prepped_txs
252                    .iter()
253                    .zip(verification_needed.iter())
254                    .filter_map(|(tx, needed)| {
255                        if *needed == VerificationNeeded::V1DecoyCheck {
256                            Some(tx)
257                        } else {
258                            None
259                        }
260                    }),
261                self.hf,
262                self.database.clone(),
263                self.batch_prep_cache.map(|c| &c.output_cache),
264            )
265            .await?;
266        }
267
268        verify_transactions(
269            self.prepped_txs,
270            verification_needed,
271            self.current_chain_height,
272            self.top_hash,
273            self.time_for_time_lock,
274            self.hf,
275            self.database,
276            self.batch_prep_cache.map(|c| &c.output_cache),
277        )
278        .await
279    }
280}
281
282/// Check that each key image used in each transaction is unique in the whole chain.
283pub(crate) async fn check_kis_unique<D: Database>(
284    mut txs: impl Iterator<Item = &TransactionVerificationData>,
285    database: &mut D,
286) -> Result<(), ExtendedConsensusError> {
287    let mut spent_kis = HashSet::with_capacity(txs.size_hint().1.unwrap_or(0) * 2);
288
289    txs.try_for_each(|tx| {
290        tx.tx.prefix().inputs.iter().try_for_each(|input| {
291            if let Input::ToKey { key_image, .. } = input {
292                if !spent_kis.insert(key_image.0) {
293                    tracing::debug!("Duplicate key image found in batch.");
294                    return Err(ConsensusError::Transaction(TransactionError::KeyImageSpent));
295                }
296            }
297
298            Ok(())
299        })
300    })?;
301
302    let BlockchainResponse::KeyImagesSpent(kis_spent) = database
303        .ready()
304        .await?
305        .call(BlockchainReadRequest::KeyImagesSpent(spent_kis))
306        .await?
307    else {
308        panic!("Database sent incorrect response!");
309    };
310
311    if kis_spent {
312        tracing::debug!("One or more key images in batch already spent.");
313        return Err(ConsensusError::Transaction(TransactionError::KeyImageSpent).into());
314    }
315
316    Ok(())
317}
318
319/// Returns a [`HashSet`] of all the hashes referenced in each transaction's [`CachedVerificationState`], that
320/// are also in the main chain.
321async fn hashes_referenced_in_main_chain<D: Database>(
322    txs: &[TransactionVerificationData],
323    database: &mut D,
324) -> Result<HashSet<[u8; 32]>, ExtendedConsensusError> {
325    let mut verified_at_block_hashes = txs
326        .iter()
327        .filter_map(|txs| txs.cached_verification_state.verified_at_block_hash())
328        .collect::<HashSet<_>>();
329
330    tracing::trace!(
331        "Verified at hashes len: {}.",
332        verified_at_block_hashes.len()
333    );
334
335    if !verified_at_block_hashes.is_empty() {
336        tracing::trace!("Filtering block hashes not in the main chain.");
337
338        let BlockchainResponse::FilterUnknownHashes(known_hashes) = database
339            .ready()
340            .await?
341            .call(BlockchainReadRequest::FilterUnknownHashes(
342                verified_at_block_hashes,
343            ))
344            .await?
345        else {
346            panic!("Database returned wrong response!");
347        };
348        verified_at_block_hashes = known_hashes;
349    }
350
351    Ok(verified_at_block_hashes)
352}
353
354/// Returns a list of [`VerificationNeeded`] for each transaction passed in. The returned
355/// [`Vec`] will be the same length as the inputted transactions.
356///
357/// A [`bool`] is also returned, which will be true if any transactions need [`VerificationNeeded::V1DecoyCheck`].
358fn verification_needed(
359    txs: &[TransactionVerificationData],
360    hashes_in_main_chain: &HashSet<[u8; 32]>,
361    current_hf: HardFork,
362    current_chain_height: usize,
363    time_for_time_lock: u64,
364) -> Result<(Vec<VerificationNeeded>, bool), ConsensusError> {
365    // txs needing full validation: semantic and/or contextual
366    let mut verification_needed = Vec::with_capacity(txs.len());
367
368    let mut any_v1_decoy_checks = false;
369
370    for tx in txs {
371        match &tx.cached_verification_state {
372            CachedVerificationState::NotVerified => {
373                // Tx not verified at all need all checks.
374                verification_needed.push(VerificationNeeded::SemanticAndContextual);
375                continue;
376            }
377            CachedVerificationState::OnlySemantic(hf) => {
378                if current_hf != *hf {
379                    // HF changed must do semantic checks again.
380                    verification_needed.push(VerificationNeeded::SemanticAndContextual);
381                    continue;
382                }
383                // Tx already semantically valid for this HF only contextual checks needed.
384                verification_needed.push(VerificationNeeded::Contextual);
385                continue;
386            }
387            CachedVerificationState::ValidAtHashAndHF { block_hash, hf } => {
388                if current_hf != *hf {
389                    // HF changed must do all checks again.
390                    verification_needed.push(VerificationNeeded::SemanticAndContextual);
391                    continue;
392                }
393
394                if !hashes_in_main_chain.contains(block_hash) {
395                    // The block we know this transaction was valid at is no longer in the chain do
396                    // contextual checks again.
397                    verification_needed.push(VerificationNeeded::Contextual);
398                    continue;
399                }
400            }
401            CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock {
402                block_hash,
403                hf,
404                time_lock,
405            } => {
406                if current_hf != *hf {
407                    // HF changed must do all checks again.
408                    verification_needed.push(VerificationNeeded::SemanticAndContextual);
409                    continue;
410                }
411
412                if !hashes_in_main_chain.contains(block_hash) {
413                    // The block we know this transaction was valid at is no longer in the chain do
414                    // contextual checks again.
415                    verification_needed.push(VerificationNeeded::Contextual);
416                    continue;
417                }
418
419                // If the time lock is still locked then the transaction is invalid.
420                // Time is not monotonic in Monero so these can become invalid with new blocks.
421                if !output_unlocked(time_lock, current_chain_height, time_for_time_lock, *hf) {
422                    return Err(ConsensusError::Transaction(
423                        TransactionError::OneOrMoreRingMembersLocked,
424                    ));
425                }
426            }
427        }
428
429        if tx.version == TxVersion::RingSignatures {
430            // v1 txs always need at least decoy checks as they can become invalid with new blocks.
431            verification_needed.push(VerificationNeeded::V1DecoyCheck);
432            any_v1_decoy_checks = true;
433            continue;
434        }
435
436        verification_needed.push(VerificationNeeded::None);
437    }
438
439    Ok((verification_needed, any_v1_decoy_checks))
440}
441
442/// Do [`VerificationNeeded::V1DecoyCheck`] on each tx passed in.
443async fn verify_transactions_decoy_info<D: Database>(
444    txs: impl Iterator<Item = &TransactionVerificationData> + Clone,
445    hf: HardFork,
446    database: D,
447    output_cache: Option<&OutputCache>,
448) -> Result<(), ExtendedConsensusError> {
449    // Decoy info is not validated for V1 txs.
450    if hf == HardFork::V1 {
451        return Ok(());
452    }
453
454    batch_get_decoy_info(txs, hf, database, output_cache)
455        .await?
456        .try_for_each(|decoy_info| decoy_info.and_then(|di| Ok(check_decoy_info(&di, hf)?)))?;
457
458    Ok(())
459}
460
461/// Do [`VerificationNeeded::Contextual`] or [`VerificationNeeded::SemanticAndContextual`].
462///
463/// The inputs to this function are the txs wanted to be verified and a list of [`VerificationNeeded`],
464/// if any other [`VerificationNeeded`] is specified other than [`VerificationNeeded::Contextual`] or
465/// [`VerificationNeeded::SemanticAndContextual`], nothing will be verified for that tx.
466#[expect(clippy::too_many_arguments)]
467async fn verify_transactions<D>(
468    mut txs: Vec<TransactionVerificationData>,
469    verification_needed: Vec<VerificationNeeded>,
470    current_chain_height: usize,
471    top_hash: [u8; 32],
472    current_time_lock_timestamp: u64,
473    hf: HardFork,
474    database: D,
475    output_cache: Option<&OutputCache>,
476) -> Result<Vec<TransactionVerificationData>, ExtendedConsensusError>
477where
478    D: Database,
479{
480    /// A filter each tx not [`VerificationNeeded::Contextual`] or
481    /// [`VerificationNeeded::SemanticAndContextual`]
482    const fn tx_filter<T>((_, needed): &(T, &VerificationNeeded)) -> bool {
483        matches!(
484            needed,
485            VerificationNeeded::Contextual | VerificationNeeded::SemanticAndContextual
486        )
487    }
488
489    let txs_ring_member_info = batch_get_ring_member_info(
490        txs.iter()
491            .zip(verification_needed.iter())
492            .filter(tx_filter)
493            .map(|(tx, _)| tx),
494        hf,
495        database,
496        output_cache,
497    )
498    .await?;
499
500    rayon_spawn_async(move || {
501        let batch_verifier = MultiThreadedBatchVerifier::new(rayon::current_num_threads());
502
503        txs.iter()
504            .zip(verification_needed.iter())
505            .filter(tx_filter)
506            .zip(txs_ring_member_info.iter())
507            .par_bridge()
508            .try_for_each(|((tx, verification_needed), ring)| {
509                // do semantic validation if needed.
510                if *verification_needed == VerificationNeeded::SemanticAndContextual {
511                    let fee = check_transaction_semantic(
512                        &tx.tx,
513                        tx.tx_blob.len(),
514                        tx.tx_weight,
515                        &tx.tx_hash,
516                        hf,
517                        &batch_verifier,
518                    )?;
519                    // make sure we calculated the right fee.
520                    assert_eq!(fee, tx.fee);
521                }
522
523                // Both variants of `VerificationNeeded` require contextual validation.
524                check_transaction_contextual(
525                    &tx.tx,
526                    ring,
527                    current_chain_height,
528                    current_time_lock_timestamp,
529                    hf,
530                )?;
531
532                Ok::<_, ConsensusError>(())
533            })?;
534
535        if !batch_verifier.verify() {
536            return Err(ExtendedConsensusError::OneOrMoreBatchVerificationStatementsInvalid);
537        }
538
539        txs.iter_mut()
540            .zip(verification_needed.iter())
541            .filter(tx_filter)
542            .zip(txs_ring_member_info)
543            .for_each(|((tx, _), ring)| {
544                tx.cached_verification_state = if ring.time_locked_outs.is_empty() {
545                    // no outputs with time-locks used.
546                    CachedVerificationState::ValidAtHashAndHF {
547                        block_hash: top_hash,
548                        hf,
549                    }
550                } else {
551                    // an output with a time-lock was used, check if it was time-based.
552                    let youngest_timebased_lock = ring
553                        .time_locked_outs
554                        .iter()
555                        .filter_map(|lock| match lock {
556                            Timelock::Time(time) => Some(time),
557                            _ => None,
558                        })
559                        .min();
560
561                    if let Some(time) = youngest_timebased_lock {
562                        // time-based lock used.
563                        CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock {
564                            block_hash: top_hash,
565                            hf,
566                            time_lock: Timelock::Time(*time),
567                        }
568                    } else {
569                        // no time-based locked output was used.
570                        CachedVerificationState::ValidAtHashAndHF {
571                            block_hash: top_hash,
572                            hf,
573                        }
574                    }
575                }
576            });
577
578        Ok(txs)
579    })
580    .await
581}