Skip to main content

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