1use 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
60enum VerificationNeeded {
61 V1DecoyCheck,
63 SemanticAndContextual,
65 Contextual,
67 None,
69}
70
71pub const fn start_tx_verification() -> PrepTransactions {
73 PrepTransactions {
74 txs: vec![],
75 prepped_txs: vec![],
76 }
77}
78
79pub struct PrepTransactions {
85 prepped_txs: Vec<TransactionVerificationData>,
86 txs: Vec<Transaction>,
87}
88
89impl PrepTransactions {
90 #[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 #[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 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
129pub struct VerificationWanted {
131 prepped_txs: Vec<TransactionVerificationData>,
132}
133
134impl VerificationWanted {
135 pub fn only_semantic(self, hf: HardFork) -> SemanticVerification {
141 SemanticVerification {
142 prepped_txs: self.prepped_txs,
143 hf,
144 }
145 }
146
147 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
170pub struct SemanticVerification {
174 prepped_txs: Vec<TransactionVerificationData>,
175 hf: HardFork,
176}
177
178impl SemanticVerification {
179 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 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
208pub 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 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
271pub(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
308async 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
343fn 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 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 verification_needed.push(VerificationNeeded::SemanticAndContextual);
364 continue;
365 }
366 CachedVerificationState::OnlySemantic(hf) => {
367 if current_hf != *hf {
368 verification_needed.push(VerificationNeeded::SemanticAndContextual);
370 continue;
371 }
372 verification_needed.push(VerificationNeeded::Contextual);
374 continue;
375 }
376 CachedVerificationState::ValidAtHashAndHF { block_hash, hf } => {
377 if current_hf != *hf {
378 verification_needed.push(VerificationNeeded::SemanticAndContextual);
380 continue;
381 }
382
383 if !hashes_in_main_chain.contains(block_hash) {
384 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 verification_needed.push(VerificationNeeded::SemanticAndContextual);
398 continue;
399 }
400
401 if !hashes_in_main_chain.contains(block_hash) {
402 verification_needed.push(VerificationNeeded::Contextual);
405 continue;
406 }
407
408 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 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
431async 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 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
449async 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 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 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 assert_eq!(fee, tx.fee);
506 }
507
508 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 CachedVerificationState::ValidAtHashAndHF {
534 block_hash: top_hash,
535 hf,
536 }
537 } else {
538 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 CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock {
551 block_hash: top_hash,
552 hf,
553 time_lock: Timelock::Time(*time),
554 }
555 } else {
556 CachedVerificationState::ValidAtHashAndHF {
558 block_hash: top_hash,
559 hf,
560 }
561 }
562 }
563 });
564
565 Ok(txs)
566 })
567 .await
568}