Skip to main content

zebra_consensus/
transaction.rs

1//! Asynchronous verification of transactions.
2
3use std::{
4    collections::{HashMap, HashSet},
5    future::Future,
6    pin::Pin,
7    sync::Arc,
8    task::{Context, Poll},
9    time::Duration,
10};
11
12use chrono::{DateTime, Utc};
13use futures::{
14    stream::{FuturesUnordered, StreamExt},
15    FutureExt,
16};
17use tokio::sync::oneshot;
18use tower::{
19    buffer::Buffer,
20    timeout::{error::Elapsed, Timeout},
21    util::BoxService,
22    Service, ServiceExt,
23};
24use tracing::Instrument;
25
26use zcash_protocol::value::ZatBalance;
27
28use zebra_chain::{
29    amount::{Amount, NonNegative},
30    block,
31    parameters::{Network, NetworkUpgrade},
32    primitives::Groth16Proof,
33    serialization::DateTime32,
34    transaction::{
35        self, HashType, SigHash, Transaction, UnminedTx, UnminedTxId, VerifiedUnminedTx,
36    },
37    transparent,
38};
39
40use zebra_node_services::mempool;
41use zebra_script::{CachedFfiTransaction, Sigops};
42use zebra_state as zs;
43
44use crate::{error::TransactionError, primitives, script, BoxError};
45
46pub mod check;
47#[cfg(test)]
48mod tests;
49
50/// A timeout applied to UTXO lookup requests.
51///
52/// The exact value is non-essential, but this should be long enough to allow
53/// out-of-order verification of blocks (UTXOs are not required to be ready
54/// immediately) while being short enough to:
55///   * prune blocks that are too far in the future to be worth keeping in the
56///     queue,
57///   * fail blocks that reference invalid UTXOs, and
58///   * fail blocks that reference UTXOs from blocks that have temporarily failed
59///     to download, because a peer sent Zebra a bad list of block hashes. (The
60///     UTXO verification failure will restart the sync, and re-download the
61///     chain in the correct order.)
62const UTXO_LOOKUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(6 * 60);
63
64/// A timeout applied to output lookup requests sent to the mempool. This is shorter than the
65/// timeout for the state UTXO lookups because a block is likely to be mined every 75 seconds
66/// after Blossom is active, changing the best chain tip and requiring re-verification of transactions
67/// in the mempool.
68///
69/// This is how long Zebra will wait for an output to be added to the mempool before verification
70/// of the transaction that spends it will fail.
71const MEMPOOL_OUTPUT_LOOKUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
72
73/// How long to wait after responding to a mempool request with a transaction that creates new
74/// transparent outputs before polling the mempool service so that it will try adding the verified
75/// transaction and responding to any potential `AwaitOutput` requests.
76///
77/// This should be long enough for the mempool service's `Downloads` to finish processing the
78/// response from the transaction verifier.
79const POLL_MEMPOOL_DELAY: std::time::Duration = Duration::from_millis(50);
80
81/// Asynchronous transaction verification.
82///
83/// # Correctness
84///
85/// Transaction verification requests should be wrapped in a timeout, so that
86/// out-of-order and invalid requests do not hang indefinitely. See the [`router`](`crate::router`)
87/// module documentation for details.
88pub struct Verifier<ZS, Mempool> {
89    network: Network,
90    state: Timeout<ZS>,
91    // TODO: Use an enum so that this can either be Pending(oneshot::Receiver) or Initialized(MempoolService)
92    mempool: Option<Timeout<Mempool>>,
93    script_verifier: script::Verifier,
94    mempool_setup_rx: oneshot::Receiver<Mempool>,
95}
96
97impl<ZS, Mempool> Verifier<ZS, Mempool>
98where
99    ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
100    ZS::Future: Send + 'static,
101    Mempool: Service<mempool::Request, Response = mempool::Response, Error = BoxError>
102        + Send
103        + Clone
104        + 'static,
105    Mempool::Future: Send + 'static,
106{
107    /// Create a new transaction verifier.
108    pub fn new(network: &Network, state: ZS, mempool_setup_rx: oneshot::Receiver<Mempool>) -> Self {
109        Self {
110            network: network.clone(),
111            state: Timeout::new(state, UTXO_LOOKUP_TIMEOUT),
112            mempool: None,
113            script_verifier: script::Verifier,
114            mempool_setup_rx,
115        }
116    }
117}
118
119impl<ZS>
120    Verifier<
121        ZS,
122        Buffer<BoxService<mempool::Request, mempool::Response, BoxError>, mempool::Request>,
123    >
124where
125    ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
126    ZS::Future: Send + 'static,
127{
128    /// Create a new transaction verifier with a closed channel receiver for mempool setup for tests.
129    #[cfg(test)]
130    pub fn new_for_tests(network: &Network, state: ZS) -> Self {
131        Self {
132            network: network.clone(),
133            state: Timeout::new(state, UTXO_LOOKUP_TIMEOUT),
134            mempool: None,
135            script_verifier: script::Verifier,
136            mempool_setup_rx: oneshot::channel().1,
137        }
138    }
139}
140
141/// Specifies whether a transaction should be verified as part of a block or as
142/// part of the mempool.
143///
144/// Transaction verification has slightly different consensus rules, depending on
145/// whether the transaction is to be included in a block on in the mempool.
146#[derive(Clone, Debug, Eq, PartialEq)]
147pub enum Request {
148    /// Verify the supplied transaction as part of a block.
149    Block {
150        /// The transaction hash.
151        transaction_hash: transaction::Hash,
152        /// The transaction itself.
153        transaction: Arc<Transaction>,
154        /// Set of transaction hashes that create new transparent outputs.
155        known_outpoint_hashes: Arc<HashSet<transaction::Hash>>,
156        /// Additional UTXOs which are known at the time of verification.
157        known_utxos: Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>>,
158        /// The height of the block containing this transaction.
159        height: block::Height,
160        /// The time that the block was mined.
161        time: DateTime<Utc>,
162    },
163    /// Verify the supplied transaction as part of the mempool.
164    ///
165    /// Mempool transactions do not have any additional UTXOs.
166    ///
167    /// Note: coinbase transactions are invalid in the mempool
168    Mempool {
169        /// The transaction itself.
170        transaction: UnminedTx,
171        /// The height of the next block.
172        ///
173        /// The next block is the first block that could possibly contain a
174        /// mempool transaction.
175        height: block::Height,
176    },
177}
178
179/// The response type for the transaction verifier service.
180/// Responses identify the transaction that was verified.
181#[derive(Clone, Debug, PartialEq)]
182pub enum Response {
183    /// A response to a block transaction verification request.
184    Block {
185        /// The witnessed transaction ID for this transaction.
186        ///
187        /// [`Response::Block`] responses can be uniquely identified by
188        /// [`UnminedTxId::mined_id`], because the block's authorizing data root
189        /// will be checked during contextual validation.
190        tx_id: UnminedTxId,
191
192        /// The miner fee for this transaction.
193        ///
194        /// `None` for coinbase transactions.
195        ///
196        /// # Consensus
197        ///
198        /// > The remaining value in the transparent transaction value pool
199        /// > of a coinbase transaction is destroyed.
200        ///
201        /// <https://zips.z.cash/protocol/protocol.pdf#transactions>
202        miner_fee: Option<Amount<NonNegative>>,
203
204        /// The number of legacy signature operations in this transaction's
205        /// transparent inputs and outputs.
206        sigops: u32,
207    },
208
209    /// A response to a mempool transaction verification request.
210    Mempool {
211        /// The full content of the verified mempool transaction.
212        /// Also contains the transaction fee and other associated fields.
213        ///
214        /// Mempool transactions always have a transaction fee,
215        /// because coinbase transactions are rejected from the mempool.
216        ///
217        /// [`Response::Mempool`] responses are uniquely identified by the
218        /// [`UnminedTxId`] variant for their transaction version.
219        transaction: VerifiedUnminedTx,
220
221        /// A list of spent [`transparent::OutPoint`]s that were found in
222        /// the mempool's list of `created_outputs`.
223        ///
224        /// Used by the mempool to determine dependencies between transactions
225        /// in the mempool and to avoid adding transactions with missing spends
226        /// to its verified set.
227        spent_mempool_outpoints: Vec<transparent::OutPoint>,
228    },
229}
230
231#[cfg(any(test, feature = "proptest-impl"))]
232impl From<VerifiedUnminedTx> for Response {
233    fn from(transaction: VerifiedUnminedTx) -> Self {
234        Response::Mempool {
235            transaction,
236            spent_mempool_outpoints: Vec::new(),
237        }
238    }
239}
240
241impl Request {
242    /// The transaction to verify that's in this request.
243    pub fn transaction(&self) -> Arc<Transaction> {
244        match self {
245            Request::Block { transaction, .. } => transaction.clone(),
246            Request::Mempool { transaction, .. } => transaction.transaction.clone(),
247        }
248    }
249
250    /// The unverified mempool transaction, if this is a mempool request.
251    pub fn mempool_transaction(&self) -> Option<UnminedTx> {
252        match self {
253            Request::Block { .. } => None,
254            Request::Mempool { transaction, .. } => Some(transaction.clone()),
255        }
256    }
257
258    /// The unmined transaction ID for the transaction in this request.
259    pub fn tx_id(&self) -> UnminedTxId {
260        match self {
261            // TODO: get the precalculated ID from the block verifier
262            Request::Block { transaction, .. } => transaction.unmined_id(),
263            Request::Mempool { transaction, .. } => transaction.id,
264        }
265    }
266
267    /// The mined transaction ID for the transaction in this request.
268    pub fn tx_mined_id(&self) -> transaction::Hash {
269        match self {
270            Request::Block {
271                transaction_hash, ..
272            } => *transaction_hash,
273            Request::Mempool { transaction, .. } => transaction.id.mined_id(),
274        }
275    }
276
277    /// The set of additional known unspent transaction outputs that's in this request.
278    pub fn known_utxos(&self) -> Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>> {
279        match self {
280            Request::Block { known_utxos, .. } => known_utxos.clone(),
281            Request::Mempool { .. } => HashMap::new().into(),
282        }
283    }
284
285    /// The set of additional known [`transparent::OutPoint`]s of unspent transaction outputs that's in this request.
286    pub fn known_outpoint_hashes(&self) -> Arc<HashSet<transaction::Hash>> {
287        match self {
288            Request::Block {
289                known_outpoint_hashes,
290                ..
291            } => known_outpoint_hashes.clone(),
292            Request::Mempool { .. } => HashSet::new().into(),
293        }
294    }
295
296    /// The height used to select the consensus rules for verifying this transaction.
297    pub fn height(&self) -> block::Height {
298        match self {
299            Request::Block { height, .. } | Request::Mempool { height, .. } => *height,
300        }
301    }
302
303    /// The block time used for lock time consensus rules validation.
304    pub fn block_time(&self) -> Option<DateTime<Utc>> {
305        match self {
306            Request::Block { time, .. } => Some(*time),
307            Request::Mempool { .. } => None,
308        }
309    }
310
311    /// The network upgrade to consider for the verification.
312    ///
313    /// This is based on the block height from the request, and the supplied `network`.
314    pub fn upgrade(&self, network: &Network) -> NetworkUpgrade {
315        NetworkUpgrade::current(network, self.height())
316    }
317
318    /// Returns true if the request is a mempool request.
319    pub fn is_mempool(&self) -> bool {
320        matches!(self, Request::Mempool { .. })
321    }
322}
323
324impl Response {
325    /// The unmined transaction ID for the transaction in this response.
326    pub fn tx_id(&self) -> UnminedTxId {
327        match self {
328            Response::Block { tx_id, .. } => *tx_id,
329            Response::Mempool { transaction, .. } => transaction.transaction.id,
330        }
331    }
332
333    /// The miner fee for the transaction in this response.
334    ///
335    /// Coinbase transactions do not have a miner fee,
336    /// and they don't need UTXOs to calculate their value balance,
337    /// because they don't spend any inputs.
338    pub fn miner_fee(&self) -> Option<Amount<NonNegative>> {
339        match self {
340            Response::Block { miner_fee, .. } => *miner_fee,
341            Response::Mempool { transaction, .. } => Some(transaction.miner_fee),
342        }
343    }
344
345    /// The number of legacy transparent signature operations in this transaction's
346    /// inputs and outputs.
347    pub fn sigops(&self) -> u32 {
348        match self {
349            Response::Block { sigops, .. } => *sigops,
350            Response::Mempool { transaction, .. } => transaction.legacy_sigop_count,
351        }
352    }
353
354    /// Returns true if the request is a mempool request.
355    pub fn is_mempool(&self) -> bool {
356        match self {
357            Response::Block { .. } => false,
358            Response::Mempool { .. } => true,
359        }
360    }
361}
362
363impl<ZS, Mempool> Service<Request> for Verifier<ZS, Mempool>
364where
365    ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
366    ZS::Future: Send + 'static,
367    Mempool: Service<mempool::Request, Response = mempool::Response, Error = BoxError>
368        + Send
369        + Clone
370        + 'static,
371    Mempool::Future: Send + 'static,
372{
373    type Response = Response;
374    type Error = TransactionError;
375    type Future =
376        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
377
378    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
379        // Note: The block verifier expects the transaction verifier to always be ready.
380
381        if self.mempool.is_none() {
382            if let Ok(mempool) = self.mempool_setup_rx.try_recv() {
383                self.mempool = Some(Timeout::new(mempool, MEMPOOL_OUTPUT_LOOKUP_TIMEOUT));
384            }
385        }
386
387        Poll::Ready(Ok(()))
388    }
389
390    fn call(&mut self, req: Request) -> Self::Future {
391        match req {
392            Request::Block { .. } => self.verify_block(req),
393            Request::Mempool { .. } => self.verify_mempool(req),
394        }
395    }
396}
397
398impl<ZS, Mempool> Verifier<ZS, Mempool>
399where
400    ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
401    ZS::Future: Send + 'static,
402    Mempool: Service<mempool::Request, Response = mempool::Response, Error = BoxError>
403        + Send
404        + Clone
405        + 'static,
406    Mempool::Future: Send + 'static,
407{
408    /// Verifies a transaction in block context.
409    fn verify_block(
410        &self,
411        req: Request,
412    ) -> Pin<Box<dyn Future<Output = Result<Response, TransactionError>> + Send + 'static>> {
413        let script_verifier = self.script_verifier;
414        let network = self.network.clone();
415        let state = self.state.clone();
416
417        let tx = req.transaction();
418        let tx_id = req.tx_id();
419        let height = req.height();
420        let time = req.block_time().expect("block requests always have a time");
421        let known_utxos = req.known_utxos();
422        let nu = req.upgrade(&network);
423        let span = tracing::debug_span!("tx", ?tx_id);
424
425        async move {
426            tracing::trace!(?tx_id, ?req, "got tx verify request");
427
428            // Do quick checks first
429            Self::check_structure_and_network_rules(tx.as_ref(), height, &network)?;
430
431            if tx.is_coinbase() {
432                check::coinbase_tx_no_prevout_joinsplit_spend(&tx)?;
433            } else if !tx.is_valid_non_coinbase() {
434                return Err(TransactionError::NonCoinbaseHasCoinbaseInput);
435            }
436
437            // Validate `nExpiryHeight` consensus rules
438            if tx.is_coinbase() {
439                check::coinbase_expiry_height(&height, &tx, &network)?;
440            } else {
441                check::non_coinbase_expiry_height(&height, &tx)?;
442            }
443
444            // Transaction invariants that apply regardless of request type or transaction version.
445            // These are pure consensus rules over the transaction structure and must always hold.
446            Self::check_transaction_invariants(tx.as_ref(), height, &network)?;
447
448            tracing::trace!(?tx_id, "passed quick checks");
449
450            // Block transactions are checked against the block's own time directly.
451            check::lock_time_has_passed(&tx, height, time)?;
452
453            // "The consensus rules applied to valueBalance, vShieldedOutput, and bindingSig
454            // in non-coinbase transactions MUST also be applied to coinbase transactions."
455            //
456            // This rule is implicitly implemented during Sapling and Orchard verification,
457            // because they do not distinguish between coinbase and non-coinbase transactions.
458            //
459            // Note: this rule originally applied to Sapling, but we assume it also applies to Orchard.
460            //
461            // https://zips.z.cash/zip-0213#specification
462
463            // Load spent UTXOs from state.
464            // The UTXOs are required for almost all the async checks.
465            let (spent_utxos, spent_outputs) =
466                Self::block_spent_utxos(tx.clone(), known_utxos, state.clone()).await?;
467
468            let cached_ffi_transaction =
469                Arc::new(CachedFfiTransaction::new(tx.clone(), Arc::new(spent_outputs), nu).map_err(|_| TransactionError::UnsupportedByNetworkUpgrade(tx.version(), nu))?);
470
471            tracing::trace!(?tx_id, "got state UTXOs");
472
473            // Select version-specific async verification pipeline
474            let async_checks = Self::dispatch_version_verification(
475                tx.as_ref(),
476                nu,
477                script_verifier,
478                cached_ffi_transaction.clone()
479            )?;
480
481            tracing::trace!(?tx_id, "awaiting async checks...");
482
483            async_checks.check().await?;
484
485            tracing::trace!(?tx_id, "finished async checks");
486
487            let miner_fee = if tx.is_coinbase() {
488                None
489            } else {
490                Some(Self::miner_fee(tx.as_ref(), &spent_utxos)?)
491            };
492            let sigops = tx.sigops().map_err(zebra_script::Error::from)?;
493
494            Ok(Response::Block {
495                tx_id,
496                miner_fee,
497                // In block validation, the consensus sigop total must include P2SH
498                // redeem-script sigops, matching zcashd's `ConnectBlock` which sums
499                // `GetLegacySigOpCount` and `GetP2SHSigOpCount` per transaction before
500                // comparing against `MAX_BLOCK_SIGOPS`. Coinbase inputs contribute zero P2SH
501                // sigops. See
502                // <https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-jv4h-j224-23cc>.
503                sigops: sigops.saturating_add(cached_ffi_transaction.p2sh_sigops()),
504            })
505        }
506            .inspect(move |result| {
507                // Hide the transaction data to avoid filling the logs
508                tracing::trace!(?tx_id, result = ?result.as_ref().map(|_tx| ()), "got tx verify result");
509            })
510            .instrument(span)
511            .boxed()
512    }
513
514    /// Verifies a transaction in mempool context.
515    fn verify_mempool(
516        &self,
517        req: Request,
518    ) -> Pin<Box<dyn Future<Output = Result<Response, TransactionError>> + Send + 'static>> {
519        let script_verifier = self.script_verifier;
520        let network = self.network.clone();
521        let state = self.state.clone();
522        let mempool = self.mempool.clone();
523
524        let tx = req.transaction();
525        let tx_id = req.tx_id();
526        let height = req.height();
527        let unmined_tx = req
528            .mempool_transaction()
529            .expect("mempool requests always contain an unmined transaction");
530        let nu = req.upgrade(&network);
531        let span = tracing::debug_span!("tx", ?tx_id);
532
533        async move {
534            tracing::trace!(?tx_id, ?req, "got tx verify request");
535
536            // Do quick checks first
537            Self::check_structure_and_network_rules(tx.as_ref(), height, &network)?;
538
539            // Validate the coinbase input consensus rules
540            if tx.is_coinbase() {
541                return Err(TransactionError::CoinbaseInMempool);
542            }
543
544            if !tx.is_valid_non_coinbase() {
545                return Err(TransactionError::NonCoinbaseHasCoinbaseInput);
546            }
547
548            // Validate `nExpiryHeight` consensus rules
549            check::non_coinbase_expiry_height(&height, &tx)?;
550
551            // Transaction invariants that apply regardless of request type or transaction version.
552            // These are pure consensus rules over the transaction structure and must always hold.
553            Self::check_transaction_invariants(tx.as_ref(), height, &network)?;
554
555            tracing::trace!(?tx_id, "passed quick checks");
556
557            // Mempool transactions are checked against the next median-time-past from state.
558            Self::verify_mempool_lock_time(tx.as_ref(), height, state.clone()).await?;
559
560            // "The consensus rules applied to valueBalance, vShieldedOutput, and bindingSig
561            // in non-coinbase transactions MUST also be applied to coinbase transactions."
562            //
563            // This rule is implicitly implemented during Sapling and Orchard verification,
564            // because they do not distinguish between coinbase and non-coinbase transactions.
565            //
566            // Note: this rule originally applied to Sapling, but we assume it also applies to Orchard.
567            //
568            // https://zips.z.cash/zip-0213#specification
569
570            // Load spent UTXOs from state.
571            // The UTXOs are required for almost all the async checks.
572            let (spent_utxos, spent_outputs, spent_mempool_outpoints) =
573                Self::mempool_spent_utxos(tx.clone(), height, state.clone(), mempool.clone()).await?;
574
575            // Mempool transactions have no block context, so there are no outputs from
576            // earlier transactions in the same block to consider.
577            Self::check_maturity_height(tx.clone(), height, &network, &spent_utxos)?;
578
579            // Reject non-standard input scripts (oversized or non-push-only
580            // scriptSigs, and high-sigop P2SH redeem scripts) *before*
581            // doing expensive script verification, to avoid DoS attacks on
582            // the script interpreter.
583            check::mempool_standard_input_scripts(tx.as_ref(), &spent_outputs)?;
584
585            // Apply ZIP-317 policy before expensive cryptographic verification.
586            let miner_fee = Self::miner_fee(tx.as_ref(), &spent_utxos)?;
587            let unpaid_actions = transaction::zip317::unpaid_actions(&unmined_tx, miner_fee);
588            transaction::zip317::mempool_checks(unpaid_actions, miner_fee, unmined_tx.size)?;
589
590            let cached_ffi_transaction =
591                Arc::new(CachedFfiTransaction::new(tx.clone(), Arc::new(spent_outputs), nu).map_err(|_| TransactionError::UnsupportedByNetworkUpgrade(tx.version(), nu))?);
592
593            tracing::trace!(?tx_id, "got state UTXOs");
594
595            // Select version-specific async verification pipeline
596            let mut async_checks = Self::dispatch_version_verification(
597                tx.as_ref(),
598                nu,
599                script_verifier,
600                cached_ffi_transaction.clone()
601            )?;
602
603            let check_anchors_and_revealed_nullifiers_query = state
604                .clone()
605                .oneshot(zs::Request::CheckBestChainTipNullifiersAndAnchors(
606                    unmined_tx.clone(),
607                ))
608                .map(|res| {
609                    assert!(
610                        res? == zs::Response::ValidBestChainTipNullifiersAndAnchors,
611                        "unexpected response to CheckBestChainTipNullifiersAndAnchors request"
612                    );
613                    Ok(())
614                });
615
616            async_checks.push(check_anchors_and_revealed_nullifiers_query);
617
618            tracing::trace!(?tx_id, "awaiting async checks...");
619
620            async_checks.check().await?;
621
622            tracing::trace!(?tx_id, "finished async checks");
623
624            let sigops = tx.sigops().map_err(zebra_script::Error::from)?;
625
626            // TODO: `spent_outputs` may not align with `tx.inputs()` when a transaction
627            // spends both chain and mempool UTXOs (mempool outputs are appended last by
628            // `mempool_spent_utxos()`), causing policy checks to pair the wrong input with
629            // the wrong spent output.
630            // https://github.com/ZcashFoundation/zebra/issues/10346
631            let spent_outputs = cached_ffi_transaction.all_previous_outputs().clone();
632
633            let transaction = VerifiedUnminedTx::new(
634                unmined_tx,
635                miner_fee,
636                sigops,
637                cached_ffi_transaction.p2sh_sigops(),
638                spent_outputs.into(),
639            )?;
640
641            if let Some(mut mempool) = mempool {
642                tokio::spawn(async move {
643                    // Best-effort poll of the mempool to provide a timely response to
644                    // `sendrawtransaction` RPC calls or `AwaitOutput` mempool calls.
645                    tokio::time::sleep(POLL_MEMPOOL_DELAY).await;
646                    let _ = mempool
647                        .ready()
648                        .await
649                        .expect("mempool poll_ready() method should not return an error")
650                        .call(mempool::Request::CheckForVerifiedTransactions)
651                        .await;
652                });
653            }
654
655            Ok(Response::Mempool { transaction, spent_mempool_outpoints })
656        }
657            .inspect(move |result| {
658                // Hide the transaction data to avoid filling the logs
659                tracing::trace!(?tx_id, result = ?result.as_ref().map(|_tx| ()), "got tx verify result");
660            })
661            .instrument(span)
662            .boxed()
663    }
664
665    /// Performs basic structural validation and Orchard-related network upgrade rules.
666    fn check_structure_and_network_rules(
667        tx: &Transaction,
668        height: block::Height,
669        network: &Network,
670    ) -> Result<(), TransactionError> {
671        // The network upgrade active at this height is used by several of the checks below;
672        // `NetworkUpgrade::current` rebuilds the activation-height map on each call, so compute it
673        // once and share it rather than recomputing it per check.
674        let network_upgrade = NetworkUpgrade::current(network, height);
675
676        check::has_inputs_and_outputs(tx)?;
677        check::has_enough_orchard_flags(tx)?;
678        // NU6.3 / Ironwood flag rules (no-ops for pre-v6 transactions).
679        check::has_enough_ironwood_flags(tx)?;
680        check::orchard_cross_address_disabled(tx)?;
681        // [NU6.3 onward] valueBalanceOrchard must be non-negative (Orchard pool frozen against new
682        // inflows; see `orchard_value_balance_non_negative`).
683        check::orchard_value_balance_non_negative(tx, network_upgrade)?;
684        // [NU6.3 onward] Coinbase transactions must have an empty Orchard component (new shielded
685        // coinbase value is routed to the Ironwood pool instead).
686        check::coinbase_orchard_component_empty(tx, network_upgrade)?;
687        check::consensus_branch_id(tx, height, network)?;
688
689        // Soft fork: temporarily require transactions to not contain Orchard actions.
690        //
691        // This soft fork was added while NU 6.1 was the active epoch on the Zcash
692        // chain, but we apply it uniformly even if NU 6.1 is not active in case it is
693        // ported to other chains with a different sequence of NUs.
694        //
695        // This will be treated as "Rules that apply generally before the next NU"
696        // when we add the NU that re-enables Orchard actions.
697        if network.is_orchard_temporarily_disabled(height) && tx.orchard_shielded_data().is_some() {
698            return Err(TransactionError::Other(
699                "transaction has Orchard actions (temporarily disabled)".into(),
700            ));
701        }
702
703        // From the network upgrade that re-enables Orchard actions (NU6.2), require
704        // that any Orchard proof has the canonical length for its number of actions.
705        // A proof that is present but not canonically sized can be padded with
706        // arbitrary trailing data without affecting its validity, allowing excess
707        // bandwidth and storage costs to be imposed while paying only fees sized to a
708        // canonical proof (GHSA-jfw5-j458-pfv6).
709        //
710        // This is a constricting rule, so it is gated on that network upgrade:
711        // Orchard actions mined before it, under earlier rules that did not enforce
712        // the proof size, must remain valid so that nodes can sync and reindex the
713        // chain before the soft fork that temporarily disabled Orchard. Orchard
714        // bundles are deserialized leniently, so the size is checked here, where the
715        // block height is available, rather than during parsing.
716        //
717        // The gate activates at the NU6.2 activation height committed in
718        // MAINNET/TESTNET_ACTIVATION_HEIGHTS. See
719        // `Network::orchard_canonical_proof_size_rule_active`.
720        if network.orchard_canonical_proof_size_rule_active(height) {
721            if let Some(orchard_shielded_data) = tx.orchard_shielded_data() {
722                if !orchard_shielded_data.proof_size_is_canonical() {
723                    return Err(TransactionError::OrchardProofSize);
724                }
725            }
726        }
727
728        // The Ironwood bundle's Halo2 proof must also have a canonical size. Ironwood only exists
729        // from NU6.3 onward (there is no legacy lenient period as there was for Orchard), so this is
730        // enforced unconditionally whenever an Ironwood bundle is present. Like the Orchard bundle,
731        // Ironwood bundles are deserialized leniently, so the size is checked here rather than during
732        // parsing.
733        if let Some(ironwood_shielded_data) = tx.ironwood_shielded_data() {
734            if !ironwood_shielded_data.proof_size_is_canonical() {
735                return Err(TransactionError::IronwoodProofSize);
736            }
737        }
738
739        Ok(())
740    }
741
742    /// Validates transaction invariants.
743    fn check_transaction_invariants(
744        tx: &Transaction,
745        height: block::Height,
746        network: &Network,
747    ) -> Result<(), TransactionError> {
748        // Consensus rule:
749        //
750        // > Either v_{pub}^{old} or v_{pub}^{new} MUST be zero.
751        //
752        // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
753        check::joinsplit_has_vpub_zero(tx)?;
754
755        // [Canopy onward]: `vpub_old` MUST be zero.
756        // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
757        check::disabled_add_to_sprout_pool(tx, height, network)?;
758
759        check::spend_conflicts(tx)?;
760
761        Ok(())
762    }
763
764    /// Validates mempool lock-time consensus rules.
765    ///
766    /// Queries state only for time-based lock times.
767    async fn verify_mempool_lock_time(
768        tx: &Transaction,
769        height: block::Height,
770        state: Timeout<ZS>,
771    ) -> Result<(), TransactionError> {
772        // Skip the state query if we don't need the time for this check.
773        let next_median_time_past = if tx.lock_time_is_time() {
774            // This state query is much faster than loading UTXOs from the database,
775            // so it doesn't need to be executed in parallel
776            Some(
777                Self::mempool_best_chain_next_median_time_past(state)
778                    .await?
779                    .to_chrono(),
780            )
781        } else {
782            None
783        };
784
785        // This consensus check makes sure Zebra produces valid block templates.
786        check::lock_time_has_passed(tx, height, next_median_time_past)?;
787
788        Ok(())
789    }
790
791    /// Fetches the median-time-past of the *next* block after the best state tip.
792    ///
793    /// This is used to verify that the lock times of mempool transactions
794    /// can be included in any valid next block.
795    async fn mempool_best_chain_next_median_time_past(
796        state: Timeout<ZS>,
797    ) -> Result<DateTime32, TransactionError> {
798        let query = state
799            .clone()
800            .oneshot(zs::Request::BestChainNextMedianTimePast);
801
802        if let zebra_state::Response::BestChainNextMedianTimePast(median_time_past) = query
803            .await
804            .map_err(|e| TransactionError::ValidateMempoolLockTimeError(e.to_string()))?
805        {
806            Ok(median_time_past)
807        } else {
808            unreachable!("Request::BestChainNextMedianTimePast always responds with BestChainNextMedianTimePast")
809        }
810    }
811
812    /// Looks up UTXOs spent by `tx` from the best chain state, also checking
813    /// `known_utxos` for UTXOs from earlier transactions in the same block.
814    ///
815    /// Returns an `OutPoint -> Utxo` map and a vec of `Output`s in the same
816    /// order as the matching inputs in `tx`.
817    async fn block_spent_utxos(
818        tx: Arc<Transaction>,
819        known_utxos: Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>>,
820        state: Timeout<ZS>,
821    ) -> Result<
822        (
823            HashMap<transparent::OutPoint, transparent::Utxo>,
824            Vec<transparent::Output>,
825        ),
826        TransactionError,
827    > {
828        let inputs = tx.inputs();
829        let mut spent_utxos = HashMap::new();
830        // Pre-allocate with None so we can fill each slot by input index, preserving input order.
831        let mut spent_outputs: Vec<Option<transparent::Output>> = vec![None; inputs.len()];
832
833        for (input_idx, input) in inputs.iter().enumerate() {
834            if let transparent::Input::PrevOut { outpoint, .. } = input {
835                tracing::trace!("awaiting outpoint lookup");
836
837                let utxo = if let Some(output) = known_utxos.get(outpoint) {
838                    tracing::trace!("UXTO in known_utxos, discarding query");
839                    output.utxo.clone()
840                } else {
841                    let response = state
842                        .clone()
843                        .oneshot(zebra_state::Request::AwaitUtxo(*outpoint))
844                        .await
845                        .map_err(|boxed_error| match boxed_error.downcast::<Elapsed>() {
846                            Ok(_) => TransactionError::TransparentInputNotFound,
847                            Err(boxed_error) => TransactionError::from(boxed_error),
848                        })?;
849
850                    if let zebra_state::Response::Utxo(utxo) = response {
851                        utxo
852                    } else {
853                        unreachable!("AwaitUtxo always responds with Utxo")
854                    }
855                };
856                tracing::trace!(?utxo, "got UTXO");
857                spent_outputs[input_idx] = Some(utxo.output.clone());
858                spent_utxos.insert(*outpoint, utxo);
859            }
860        }
861
862        let spent_outputs: Vec<transparent::Output> = spent_outputs.into_iter().flatten().collect();
863
864        Ok((spent_utxos, spent_outputs))
865    }
866
867    /// Looks up UTXOs spent by a mempool `tx`, first querying the best chain state
868    /// and then the mempool for inputs whose outputs are not present in the best chain.
869    ///
870    /// `height` is the next block height, used to construct `Utxo` values for
871    /// outputs sourced from the mempool.
872    ///
873    /// Returns an `OutPoint -> Utxo` map, a vec of `Output`s in the same order
874    /// as the matching inputs in `tx`, and a vec of `OutPoint`s that were
875    /// sourced from the mempool rather than the best chain.
876    async fn mempool_spent_utxos(
877        tx: Arc<Transaction>,
878        height: block::Height,
879        state: Timeout<ZS>,
880        mempool: Option<Timeout<Mempool>>,
881    ) -> Result<
882        (
883            HashMap<transparent::OutPoint, transparent::Utxo>,
884            Vec<transparent::Output>,
885            Vec<transparent::OutPoint>,
886        ),
887        TransactionError,
888    > {
889        let inputs = tx.inputs();
890        let mut spent_utxos = HashMap::new();
891        // Pre-allocate with None so we can fill each slot by input index, preserving input order
892        // even when chain and mempool UTXOs are fetched in separate passes.
893        let mut spent_outputs: Vec<Option<transparent::Output>> = vec![None; inputs.len()];
894        // Stores (input_idx, outpoint) for UTXOs not found in the best chain (fetched from mempool later).
895        let mut spent_mempool_outpoints: Vec<(usize, transparent::OutPoint)> = Vec::new();
896
897        for (input_idx, input) in inputs.iter().enumerate() {
898            if let transparent::Input::PrevOut { outpoint, .. } = input {
899                tracing::trace!("awaiting outpoint lookup");
900
901                let query = state
902                    .clone()
903                    .oneshot(zs::Request::UnspentBestChainUtxo(*outpoint));
904
905                let zebra_state::Response::UnspentBestChainUtxo(utxo) = query
906                    .await
907                    .map_err(|_| TransactionError::TransparentInputNotFound)?
908                else {
909                    unreachable!("UnspentBestChainUtxo always responds with Option<Utxo>")
910                };
911
912                let Some(utxo) = utxo else {
913                    spent_mempool_outpoints.push((input_idx, *outpoint));
914                    continue;
915                };
916
917                tracing::trace!(?utxo, "got UTXO");
918                spent_outputs[input_idx] = Some(utxo.output.clone());
919                spent_utxos.insert(*outpoint, utxo);
920            }
921        }
922
923        if let Some(mempool) = mempool {
924            for &(input_idx, spent_mempool_outpoint) in &spent_mempool_outpoints {
925                let query = mempool
926                    .clone()
927                    .oneshot(mempool::Request::AwaitOutput(spent_mempool_outpoint));
928
929                let output = match query.await {
930                    Ok(mempool::Response::UnspentOutput(output)) => output,
931                    Ok(_) => unreachable!("UnspentOutput always responds with UnspentOutput"),
932                    Err(err) => {
933                        return match err.downcast::<Elapsed>() {
934                            Ok(_) => Err(TransactionError::TransparentInputNotFound),
935                            Err(err) => Err(err.into()),
936                        };
937                    }
938                };
939
940                spent_outputs[input_idx] = Some(output.clone());
941                spent_utxos.insert(
942                    spent_mempool_outpoint,
943                    // Assume the Utxo height will be next height after the best chain tip height
944                    //
945                    // # Correctness
946                    //
947                    // If the tip height changes while an umined transaction is being verified,
948                    // the transaction must be re-verified before being added to the mempool.
949                    transparent::Utxo::new(output, height, false),
950                );
951            }
952        } else if !spent_mempool_outpoints.is_empty() {
953            return Err(TransactionError::TransparentInputNotFound);
954        }
955
956        // Convert back to return types; slots are in input order.
957        let spent_outputs: Vec<transparent::Output> = spent_outputs.into_iter().flatten().collect();
958        let spent_mempool_outpoints: Vec<transparent::OutPoint> = spent_mempool_outpoints
959            .into_iter()
960            .map(|(_, op)| op)
961            .collect();
962
963        Ok((spent_utxos, spent_outputs, spent_mempool_outpoints))
964    }
965
966    /// Checks that every transparent coinbase output spent by `tx` has matured
967    /// by `height`.
968    ///
969    /// This check applies only to mempool transactions. Block transactions are
970    /// checked during contextual validation in the state (see #2336).
971    ///
972    /// Calls [`check::tx_transparent_coinbase_spends_maturity`] with an empty
973    /// `block_new_outputs` map, since mempool transactions have no block context.
974    ///
975    /// Returns `Ok(())` if every transparent coinbase output spent by the transaction is
976    /// mature and valid for the given height, or a [`TransactionError`] if the transaction
977    /// spends transparent coinbase outputs that are immature and invalid for the given height.
978    fn check_maturity_height(
979        tx: Arc<Transaction>,
980        height: block::Height,
981        network: &Network,
982        spent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
983    ) -> Result<(), TransactionError> {
984        check::tx_transparent_coinbase_spends_maturity(
985            network,
986            tx,
987            height,
988            Arc::new(HashMap::new()),
989            spent_utxos,
990        )
991    }
992
993    /// Dispatches version-specific async verification checks for `tx`.
994    ///
995    /// `nu` is the network upgrade active at the transaction's block height,
996    /// pre-computed by the caller from `req.upgrade(&network)`.
997    ///
998    /// Returns [`TransactionError::WrongVersion`] for V1-V3 transactions, which
999    /// are not supported by any network upgrade Zebra verifies.
1000    fn dispatch_version_verification(
1001        tx: &Transaction,
1002        nu: NetworkUpgrade,
1003        script_verifier: script::Verifier,
1004        cached_ffi_transaction: Arc<CachedFfiTransaction>,
1005    ) -> Result<AsyncChecks, TransactionError> {
1006        match tx {
1007            Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {
1008                tracing::debug!(?tx, "got transaction with wrong version");
1009                Err(TransactionError::WrongVersion)
1010            }
1011            Transaction::V4 { joinsplit_data, .. } => Self::verify_v4_transaction(
1012                tx,
1013                nu,
1014                script_verifier,
1015                cached_ffi_transaction,
1016                joinsplit_data,
1017            ),
1018            Transaction::V5 { .. } => {
1019                Self::verify_v5_transaction(tx, nu, script_verifier, cached_ffi_transaction)
1020            }
1021            Transaction::V6 { .. } => {
1022                Self::verify_v6_transaction(tx, nu, script_verifier, cached_ffi_transaction)
1023            }
1024        }
1025    }
1026
1027    /// Verify a V4 transaction.
1028    ///
1029    /// Returns a set of asynchronous checks that must all succeed for the transaction to be
1030    /// considered valid. These checks include:
1031    ///
1032    /// - transparent transfers
1033    /// - sprout shielded data
1034    /// - sapling shielded data
1035    ///
1036    /// The parameters of this method are:
1037    ///
1038    /// - the `tx` transaction to verify
1039    /// - the `nu` network upgrade active at the transaction's block height
1040    /// - the `script_verifier` to use for verifying the transparent transfers
1041    /// - the prepared `cached_ffi_transaction` used by the script verifier
1042    /// - the Sprout `joinsplit_data` shielded data in the transaction
1043    #[allow(clippy::unwrap_in_result)]
1044    fn verify_v4_transaction(
1045        tx: &Transaction,
1046        nu: NetworkUpgrade,
1047        script_verifier: script::Verifier,
1048        cached_ffi_transaction: Arc<CachedFfiTransaction>,
1049        joinsplit_data: &Option<transaction::JoinSplitData<Groth16Proof>>,
1050    ) -> Result<AsyncChecks, TransactionError> {
1051        Self::verify_v4_transaction_network_upgrade(tx, nu)?;
1052
1053        let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle();
1054
1055        let sighash = cached_ffi_transaction
1056            .sighasher()
1057            .sighash(HashType::ALL, None);
1058
1059        Ok(Self::verify_transparent_inputs_and_outputs(
1060            tx,
1061            script_verifier,
1062            cached_ffi_transaction,
1063        )?
1064        .and(Self::verify_sprout_shielded_data(joinsplit_data, &sighash)?)
1065        .and(Self::verify_sapling_bundle(sapling_bundle, &sighash)))
1066    }
1067
1068    /// Verifies if a V4 `transaction` is supported by `network_upgrade`.
1069    fn verify_v4_transaction_network_upgrade(
1070        transaction: &Transaction,
1071        network_upgrade: NetworkUpgrade,
1072    ) -> Result<(), TransactionError> {
1073        match network_upgrade {
1074            // Supports V4 transactions
1075            //
1076            // # Consensus
1077            //
1078            // > [Sapling to Canopy inclusive, pre-NU5] The transaction version number MUST be 4,
1079            // > and the version group ID MUST be 0x892F2085.
1080            //
1081            // > [NU5 onward] The transaction version number MUST be 4 or 5.
1082            // > If the transaction version number is 4 then the version group ID MUST be 0x892F2085.
1083            // > If the transaction version number is 5 then the version group ID MUST be 0x26A7270A.
1084            //
1085            // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1086            //
1087            // Note: Here we verify the transaction version number of the above two rules, the group
1088            // id is checked in zebra-chain crate, in the transaction serialize.
1089            NetworkUpgrade::Sapling
1090            | NetworkUpgrade::Blossom
1091            | NetworkUpgrade::Heartwood
1092            | NetworkUpgrade::Canopy
1093            | NetworkUpgrade::Nu5
1094            | NetworkUpgrade::Nu6
1095            | NetworkUpgrade::Nu6_1
1096            | NetworkUpgrade::Nu6_2
1097            | NetworkUpgrade::Nu6_3 => Ok(()),
1098
1099            #[cfg(zcash_unstable = "zfuture")]
1100            NetworkUpgrade::ZFuture => Ok(()),
1101
1102            // Does not support V4 transactions
1103            NetworkUpgrade::Genesis
1104            | NetworkUpgrade::BeforeOverwinter
1105            | NetworkUpgrade::Overwinter
1106            | NetworkUpgrade::Nu7 => Err(TransactionError::UnsupportedByNetworkUpgrade(
1107                transaction.version(),
1108                network_upgrade,
1109            )),
1110        }
1111    }
1112
1113    /// Verify a V5 transaction.
1114    ///
1115    /// Returns a set of asynchronous checks that must all succeed for the transaction to be
1116    /// considered valid. These checks include:
1117    ///
1118    /// - transaction support by the considered network upgrade (see [`Request::upgrade`])
1119    /// - transparent transfers
1120    /// - sapling shielded data (TODO)
1121    /// - orchard shielded data (TODO)
1122    ///
1123    /// The parameters of this method are:
1124    ///
1125    /// - the `tx` transaction to verify
1126    /// - the `nu` network upgrade active at the transaction's block height
1127    /// - the `script_verifier` to use for verifying the transparent transfers
1128    /// - the prepared `cached_ffi_transaction` used by the script verifier
1129    #[allow(clippy::unwrap_in_result)]
1130    fn verify_v5_transaction(
1131        tx: &Transaction,
1132        nu: NetworkUpgrade,
1133        script_verifier: script::Verifier,
1134        cached_ffi_transaction: Arc<CachedFfiTransaction>,
1135    ) -> Result<AsyncChecks, TransactionError> {
1136        Self::verify_v5_transaction_network_upgrade(tx, nu)?;
1137
1138        let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle();
1139        let orchard_bundle = cached_ffi_transaction.sighasher().orchard_bundle();
1140
1141        let sighash = cached_ffi_transaction
1142            .sighasher()
1143            .sighash(HashType::ALL, None);
1144
1145        Ok(Self::verify_transparent_inputs_and_outputs(
1146            tx,
1147            script_verifier,
1148            cached_ffi_transaction,
1149        )?
1150        .and(Self::verify_sapling_bundle(sapling_bundle, &sighash))
1151        .and(Self::verify_orchard_bundle(orchard_bundle, &sighash, nu)))
1152    }
1153
1154    /// Verifies if a V5 `transaction` is supported by `network_upgrade`.
1155    fn verify_v5_transaction_network_upgrade(
1156        transaction: &Transaction,
1157        network_upgrade: NetworkUpgrade,
1158    ) -> Result<(), TransactionError> {
1159        match network_upgrade {
1160            // Supports V5 transactions
1161            //
1162            // # Consensus
1163            //
1164            // > [NU5 onward] The transaction version number MUST be 4 or 5.
1165            // > If the transaction version number is 4 then the version group ID MUST be 0x892F2085.
1166            // > If the transaction version number is 5 then the version group ID MUST be 0x26A7270A.
1167            //
1168            // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1169            //
1170            // Note: Here we verify the transaction version number of the above rule, the group
1171            // id is checked in zebra-chain crate, in the transaction serialize.
1172            NetworkUpgrade::Nu5
1173            | NetworkUpgrade::Nu6
1174            | NetworkUpgrade::Nu6_1
1175            | NetworkUpgrade::Nu6_2
1176            | NetworkUpgrade::Nu6_3
1177            | NetworkUpgrade::Nu7 => Ok(()),
1178
1179            #[cfg(zcash_unstable = "zfuture")]
1180            NetworkUpgrade::ZFuture => Ok(()),
1181
1182            // Does not support V5 transactions
1183            NetworkUpgrade::Genesis
1184            | NetworkUpgrade::BeforeOverwinter
1185            | NetworkUpgrade::Overwinter
1186            | NetworkUpgrade::Sapling
1187            | NetworkUpgrade::Blossom
1188            | NetworkUpgrade::Heartwood
1189            | NetworkUpgrade::Canopy => Err(TransactionError::UnsupportedByNetworkUpgrade(
1190                transaction.version(),
1191                network_upgrade,
1192            )),
1193        }
1194    }
1195
1196    /// Verifies a V6 (NU6.3 / Ironwood) transaction's shielded data.
1197    ///
1198    /// Differs from [`Self::verify_v5_transaction`] in the Orchard verifier: a v6 Orchard bundle
1199    /// commits to the NU6.3 cross-address circuit, so it (and the Ironwood bundle) verify under the
1200    /// NU6.3 key, not the v5 fixed key.
1201    fn verify_v6_transaction(
1202        tx: &Transaction,
1203        nu: NetworkUpgrade,
1204        script_verifier: script::Verifier,
1205        cached_ffi_transaction: Arc<CachedFfiTransaction>,
1206    ) -> Result<AsyncChecks, TransactionError> {
1207        Self::verify_v6_transaction_network_upgrade(tx, nu)?;
1208
1209        let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle();
1210        let orchard_bundle = cached_ffi_transaction.sighasher().orchard_bundle();
1211        let ironwood_bundle = cached_ffi_transaction.sighasher().ironwood_bundle();
1212
1213        let sighash = cached_ffi_transaction
1214            .sighasher()
1215            .sighash(HashType::ALL, None);
1216
1217        // The Ironwood bundle reuses the Orchard Action proof system and the NU6.3 circuit key, so
1218        // it is verified the same way as the v6 Orchard bundle (against the NU6.3 key).
1219        Ok(Self::verify_transparent_inputs_and_outputs(
1220            tx,
1221            script_verifier,
1222            cached_ffi_transaction,
1223        )?
1224        .and(Self::verify_sapling_bundle(sapling_bundle, &sighash))
1225        .and(Self::verify_orchard_v6_bundle(orchard_bundle, &sighash))
1226        .and(Self::verify_orchard_v6_bundle(ironwood_bundle, &sighash)))
1227    }
1228
1229    /// Verifies that a V6 `transaction` is supported by `network_upgrade`.
1230    ///
1231    /// V6 transactions are only valid from NU6.3 onward.
1232    fn verify_v6_transaction_network_upgrade(
1233        transaction: &Transaction,
1234        network_upgrade: NetworkUpgrade,
1235    ) -> Result<(), TransactionError> {
1236        match network_upgrade {
1237            NetworkUpgrade::Nu6_3 | NetworkUpgrade::Nu7 => Ok(()),
1238
1239            #[cfg(zcash_unstable = "zfuture")]
1240            NetworkUpgrade::ZFuture => Ok(()),
1241
1242            // V6 transactions are not valid before NU6.3.
1243            NetworkUpgrade::Genesis
1244            | NetworkUpgrade::BeforeOverwinter
1245            | NetworkUpgrade::Overwinter
1246            | NetworkUpgrade::Sapling
1247            | NetworkUpgrade::Blossom
1248            | NetworkUpgrade::Heartwood
1249            | NetworkUpgrade::Canopy
1250            | NetworkUpgrade::Nu5
1251            | NetworkUpgrade::Nu6
1252            | NetworkUpgrade::Nu6_1
1253            | NetworkUpgrade::Nu6_2 => Err(TransactionError::UnsupportedByNetworkUpgrade(
1254                transaction.version(),
1255                network_upgrade,
1256            )),
1257        }
1258    }
1259
1260    /// Verifies if a transaction's transparent inputs are valid using the provided
1261    /// `script_verifier` and `cached_ffi_transaction`.
1262    ///
1263    /// Returns script verification responses via the `utxo_sender`.
1264    fn verify_transparent_inputs_and_outputs(
1265        tx: &Transaction,
1266        script_verifier: script::Verifier,
1267        cached_ffi_transaction: Arc<CachedFfiTransaction>,
1268    ) -> Result<AsyncChecks, TransactionError> {
1269        if tx.is_coinbase() {
1270            // The script verifier only verifies PrevOut inputs and their corresponding UTXOs.
1271            // Coinbase transactions don't have any PrevOut inputs.
1272            Ok(AsyncChecks::new())
1273        } else {
1274            // feed all of the inputs to the script verifier
1275            let inputs = tx.inputs();
1276
1277            let script_checks = (0..inputs.len())
1278                .map(move |input_index| {
1279                    let request = script::Request {
1280                        cached_ffi_transaction: cached_ffi_transaction.clone(),
1281                        input_index,
1282                    };
1283
1284                    script_verifier.oneshot(request)
1285                })
1286                .collect();
1287
1288            Ok(script_checks)
1289        }
1290    }
1291
1292    /// Verifies a transaction's Sprout shielded join split data.
1293    fn verify_sprout_shielded_data(
1294        joinsplit_data: &Option<transaction::JoinSplitData<Groth16Proof>>,
1295        shielded_sighash: &SigHash,
1296    ) -> Result<AsyncChecks, TransactionError> {
1297        let mut checks = AsyncChecks::new();
1298
1299        if let Some(joinsplit_data) = joinsplit_data {
1300            for joinsplit in joinsplit_data.joinsplits() {
1301                // # Consensus
1302                //
1303                // > The proof π_ZKJoinSplit MUST be valid given a
1304                // > primary input formed from the relevant other fields and h_{Sig}
1305                //
1306                // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
1307                //
1308                // Queue the verification of the Groth16 spend proof
1309                // for each JoinSplit description while adding the
1310                // resulting future to our collection of async
1311                // checks that (at a minimum) must pass for the
1312                // transaction to verify.
1313                checks.push(primitives::groth16::JOINSPLIT_VERIFIER.oneshot(
1314                    primitives::groth16::Item::from_joinsplit(joinsplit, &joinsplit_data.pub_key)?,
1315                ));
1316            }
1317
1318            // # Consensus
1319            //
1320            // > If effectiveVersion ≥ 2 and nJoinSplit > 0, then:
1321            // > - joinSplitPubKey MUST be a valid encoding of an Ed25519 validating key
1322            // > - joinSplitSig MUST represent a valid signature under
1323            //     joinSplitPubKey of dataToBeSigned, as defined in § 4.11
1324            //
1325            // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1326            //
1327            // The `if` part is indirectly enforced, since the `joinsplit_data`
1328            // is only parsed if those conditions apply in
1329            // [`Transaction::zcash_deserialize`].
1330            //
1331            // The valid encoding is defined in
1332            //
1333            // > A valid Ed25519 validating key is defined as a sequence of 32
1334            // > bytes encoding a point on the Ed25519 curve
1335            //
1336            // https://zips.z.cash/protocol/protocol.pdf#concreteed25519
1337            //
1338            // which is enforced during signature verification, in both batched
1339            // and single verification, when decompressing the encoded point.
1340            //
1341            // Queue the validation of the JoinSplit signature while
1342            // adding the resulting future to our collection of
1343            // async checks that (at a minimum) must pass for the
1344            // transaction to verify.
1345            //
1346            // https://zips.z.cash/protocol/protocol.pdf#sproutnonmalleability
1347            // https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
1348            let ed25519_verifier = primitives::ed25519::VERIFIER.clone();
1349            let ed25519_item =
1350                (joinsplit_data.pub_key, joinsplit_data.sig, shielded_sighash).into();
1351
1352            checks.push(ed25519_verifier.oneshot(ed25519_item));
1353        }
1354
1355        Ok(checks)
1356    }
1357
1358    /// Verifies a transaction's Sapling shielded data.
1359    fn verify_sapling_bundle(
1360        bundle: Option<sapling_crypto::Bundle<sapling_crypto::bundle::Authorized, ZatBalance>>,
1361        sighash: &SigHash,
1362    ) -> AsyncChecks {
1363        let mut async_checks = AsyncChecks::new();
1364
1365        // The Sapling batch verifier checks the following consensus rules:
1366        //
1367        // # Consensus
1368        //
1369        // > The proof π_ZKSpend MUST be valid given a primary input formed from the other fields
1370        // > except spendAuthSig.
1371        //
1372        // > The spend authorization signature MUST be a valid SpendAuthSig signature over SigHash
1373        // > using rk as the validating key.
1374        //
1375        // > [NU5 onward] As specified in § 5.4.7 ‘RedDSA, RedJubjub, and RedPallas’ on p. 88, the
1376        // > validation of the 𝑅 component of the signature changes to prohibit non-canonical
1377        // > encodings.
1378        //
1379        // https://zips.z.cash/protocol/protocol.pdf#spenddesc
1380        //
1381        // # Consensus
1382        //
1383        // > The proof π_ZKOutput MUST be valid given a primary input formed from the other fields
1384        // > except C^enc and C^out.
1385        //
1386        // https://zips.z.cash/protocol/protocol.pdf#outputdesc
1387        //
1388        // # Consensus
1389        //
1390        // > The Spend transfers and Action transfers of a transaction MUST be consistent with its
1391        // > vbalanceSapling value as specified in § 4.13 ‘Balance and Binding Signature (Sapling)’.
1392        //
1393        // https://zips.z.cash/protocol/protocol.pdf#spendsandoutputs
1394        //
1395        // # Consensus
1396        //
1397        // > [Sapling onward] If effectiveVersion ≥ 4 and nSpendsSapling + nOutputsSapling > 0,
1398        // > then:
1399        // >
1400        // > – let bvk^{Sapling} and SigHash be as defined in § 4.13;
1401        // > – bindingSigSapling MUST represent a valid signature under the transaction binding
1402        // >   validating key bvk Sapling of SigHash — i.e.
1403        // >   BindingSig^{Sapling}.Validate_{bvk^{Sapling}}(SigHash, bindingSigSapling ) = 1.
1404        //
1405        // Note that the `if` part is indirectly enforced, since the `sapling_shielded_data` is only
1406        // parsed if those conditions apply in [`Transaction::zcash_deserialize`].
1407        //
1408        // > [NU5 onward] As specified in § 5.4.7, the validation of the 𝑅 component of the
1409        // > signature changes to prohibit non-canonical encodings.
1410        //
1411        // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1412        if let Some(bundle) = bundle {
1413            async_checks.push(
1414                primitives::sapling::VERIFIER
1415                    .clone()
1416                    .oneshot(primitives::sapling::Item::new(bundle, *sighash)),
1417            );
1418        }
1419
1420        async_checks
1421    }
1422
1423    /// Verifies a **v5** transaction's Orchard bundle.
1424    ///
1425    /// A v5 Orchard bundle commits to the Orchard Action circuit of the block's era, so the
1426    /// verifying key is selected by `network_upgrade` via
1427    /// [`primitives::halo2::orchard_v5_verifier_for`]: the historical insecure key before NU6.2, the
1428    /// fixed key from NU6.2 until NU6.3, and the NU6.3 key from NU6.3 onward. The Orchard-pool
1429    /// cross-address restriction applies to every Orchard Action from NU6.3 onward regardless of
1430    /// transaction version (ZIP 229), so a v5 bundle at NU6.3 uses the NU6.3 circuit — the same key
1431    /// as v6 Orchard and Ironwood bundles — not the fixed one.
1432    fn verify_orchard_bundle(
1433        bundle: Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1434        sighash: &SigHash,
1435        network_upgrade: NetworkUpgrade,
1436    ) -> AsyncChecks {
1437        Self::queue_orchard_bundle(
1438            || primitives::halo2::orchard_v5_verifier_for(network_upgrade),
1439            bundle,
1440            sighash,
1441        )
1442    }
1443
1444    /// Verifies a **v6** transaction's Orchard bundle.
1445    ///
1446    /// A v6 Orchard bundle commits to the NU6.3 cross-address circuit, so it always verifies under
1447    /// the NU6.3 key ([`primitives::halo2::orchard_v6_verifier`]), independent of the block's
1448    /// network upgrade (v6 transactions only exist from NU6.3 onward). The Ironwood bundle reuses
1449    /// the same verifier.
1450    fn verify_orchard_v6_bundle(
1451        bundle: Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1452        sighash: &SigHash,
1453    ) -> AsyncChecks {
1454        Self::queue_orchard_bundle(primitives::halo2::orchard_v6_verifier, bundle, sighash)
1455    }
1456
1457    /// Queues an Orchard-shaped bundle's single aggregated Halo2 proof against a verifier.
1458    ///
1459    /// # Consensus
1460    ///
1461    /// > The proof 𝜋 MUST be valid given a primary input (cv, rt^{Orchard}, nf, rk, cm_x,
1462    /// > enableSpends, enableOutputs)
1463    ///
1464    /// <https://zips.z.cash/protocol/protocol.pdf#actiondesc>
1465    ///
1466    /// Unlike Sapling, Orchard shielded transactions have a single aggregated Halo2 proof per
1467    /// transaction, even with multiple Actions, so it is queued for verification only once instead
1468    /// of once per Action description. The choice of verifying key is the caller's; see
1469    /// [`Self::verify_orchard_bundle`] and [`Self::verify_orchard_v6_bundle`].
1470    ///
1471    /// `select_verifier` is only invoked when a bundle is present, so a bundle-less transaction
1472    /// never forces the (lazily initialized) verifier services.
1473    fn queue_orchard_bundle(
1474        select_verifier: impl FnOnce() -> &'static primitives::halo2::VerifierService,
1475        bundle: Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1476        sighash: &SigHash,
1477    ) -> AsyncChecks {
1478        let mut async_checks = AsyncChecks::new();
1479
1480        if let Some(bundle) = bundle {
1481            async_checks.push(
1482                select_verifier()
1483                    .clone()
1484                    .oneshot(primitives::halo2::Item::new(bundle, *sighash)),
1485            );
1486        }
1487
1488        async_checks
1489    }
1490
1491    /// Calculate the miner fee from the transaction's value balance.
1492    fn miner_fee(
1493        tx: &Transaction,
1494        spent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
1495    ) -> Result<Amount<NonNegative>, TransactionError> {
1496        match tx.value_balance(spent_utxos) {
1497            Ok(value_balance) => value_balance
1498                .remaining_transaction_value()
1499                .map_err(|_| TransactionError::IncorrectFee),
1500            Err(_) => Err(TransactionError::IncorrectFee),
1501        }
1502    }
1503}
1504
1505/// A set of unordered asynchronous checks that should succeed.
1506///
1507/// A wrapper around [`FuturesUnordered`] with some auxiliary methods.
1508struct AsyncChecks(FuturesUnordered<Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>>);
1509
1510impl AsyncChecks {
1511    /// Create an empty set of unordered asynchronous checks.
1512    pub fn new() -> Self {
1513        AsyncChecks(FuturesUnordered::new())
1514    }
1515
1516    /// Push a check into the set.
1517    pub fn push(&mut self, check: impl Future<Output = Result<(), BoxError>> + Send + 'static) {
1518        self.0.push(check.boxed());
1519    }
1520
1521    /// Push a set of checks into the set.
1522    ///
1523    /// This method can be daisy-chained.
1524    pub fn and(mut self, checks: AsyncChecks) -> Self {
1525        self.0.extend(checks.0);
1526        self
1527    }
1528
1529    /// Wait until all checks in the set finish.
1530    ///
1531    /// If any of the checks fail, this method immediately returns the error and cancels all other
1532    /// checks by dropping them.
1533    async fn check(mut self) -> Result<(), BoxError> {
1534        // Wait for all asynchronous checks to complete
1535        // successfully, or fail verification if they error.
1536        while let Some(check) = self.0.next().await {
1537            tracing::trace!(?check, remaining = self.0.len());
1538            check?;
1539        }
1540
1541        Ok(())
1542    }
1543}
1544
1545impl<F> FromIterator<F> for AsyncChecks
1546where
1547    F: Future<Output = Result<(), BoxError>> + Send + 'static,
1548{
1549    fn from_iter<I>(iterator: I) -> Self
1550    where
1551        I: IntoIterator<Item = F>,
1552    {
1553        AsyncChecks(iterator.into_iter().map(FutureExt::boxed).collect())
1554    }
1555}