Skip to main content

zebra_consensus/
transaction.rs

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