1use 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
62enum VerificationNeeded {
63 V1DecoyCheck,
65 SemanticAndContextual,
67 Contextual,
69 None,
71}
72
73pub const fn start_tx_verification() -> PrepTransactions {
75 PrepTransactions {
76 txs: vec![],
77 prepped_txs: vec![],
78 }
79}
80
81pub struct PrepTransactions {
87 prepped_txs: Vec<TransactionVerificationData>,
88 txs: Vec<Transaction>,
89}
90
91impl PrepTransactions {
92 #[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 #[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 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
131pub struct VerificationWanted {
133 prepped_txs: Vec<TransactionVerificationData>,
134}
135
136impl VerificationWanted {
137 pub fn only_semantic(self, hf: HardFork) -> SemanticVerification {
143 SemanticVerification {
144 prepped_txs: self.prepped_txs,
145 hf,
146 }
147 }
148
149 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
174pub struct SemanticVerification {
178 prepped_txs: Vec<TransactionVerificationData>,
179 hf: HardFork,
180}
181
182impl SemanticVerification {
183 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 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
212pub 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 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
282pub(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
319async 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
354fn 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 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 verification_needed.push(VerificationNeeded::SemanticAndContextual);
375 continue;
376 }
377 CachedVerificationState::OnlySemantic(hf) => {
378 if current_hf != *hf {
379 verification_needed.push(VerificationNeeded::SemanticAndContextual);
381 continue;
382 }
383 verification_needed.push(VerificationNeeded::Contextual);
385 continue;
386 }
387 CachedVerificationState::ValidAtHashAndHF { block_hash, hf } => {
388 if current_hf != *hf {
389 verification_needed.push(VerificationNeeded::SemanticAndContextual);
391 continue;
392 }
393
394 if !hashes_in_main_chain.contains(block_hash) {
395 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 verification_needed.push(VerificationNeeded::SemanticAndContextual);
409 continue;
410 }
411
412 if !hashes_in_main_chain.contains(block_hash) {
413 verification_needed.push(VerificationNeeded::Contextual);
416 continue;
417 }
418
419 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 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
442async 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 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#[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 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 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 assert_eq!(fee, tx.fee);
521 }
522
523 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 CachedVerificationState::ValidAtHashAndHF {
547 block_hash: top_hash,
548 hf,
549 }
550 } else {
551 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 CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock {
564 block_hash: top_hash,
565 hf,
566 time_lock: Timelock::Time(*time),
567 }
568 } else {
569 CachedVerificationState::ValidAtHashAndHF {
571 block_hash: top_hash,
572 hf,
573 }
574 }
575 }
576 });
577
578 Ok(txs)
579 })
580 .await
581}