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