Skip to main content

zebra_consensus/
transaction.rs

1//! Asynchronous verification of transactions.
2
3use std::{
4    collections::{HashMap, HashSet},
5    future::Future,
6    pin::Pin,
7    sync::Arc,
8    task::{Context, Poll},
9    time::Duration,
10};
11
12use chrono::{DateTime, Utc};
13use futures::{
14    stream::{FuturesUnordered, StreamExt},
15    FutureExt,
16};
17use tokio::sync::oneshot;
18use tower::{
19    buffer::Buffer,
20    timeout::{error::Elapsed, Timeout},
21    util::BoxService,
22    Service, ServiceExt,
23};
24use tracing::Instrument;
25
26use zcash_protocol::value::ZatBalance;
27
28use zebra_chain::{
29    amount::{Amount, NonNegative},
30    block,
31    parameters::{Network, NetworkUpgrade},
32    primitives::Groth16Proof,
33    serialization::DateTime32,
34    transaction::{
35        self, HashType, SigHash, Transaction, UnminedTx, UnminedTxId, VerifiedUnminedTx,
36    },
37    transparent,
38};
39
40use zebra_node_services::mempool;
41use zebra_script::{CachedFfiTransaction, Sigops};
42use zebra_state as zs;
43
44use crate::{error::TransactionError, groth16::DescriptionWrapper, primitives, script, BoxError};
45
46pub mod check;
47#[cfg(test)]
48mod tests;
49
50/// A timeout applied to UTXO lookup requests.
51///
52/// The exact value is non-essential, but this should be long enough to allow
53/// out-of-order verification of blocks (UTXOs are not required to be ready
54/// immediately) while being short enough to:
55///   * prune blocks that are too far in the future to be worth keeping in the
56///     queue,
57///   * fail blocks that reference invalid UTXOs, and
58///   * fail blocks that reference UTXOs from blocks that have temporarily failed
59///     to download, because a peer sent Zebra a bad list of block hashes. (The
60///     UTXO verification failure will restart the sync, and re-download the
61///     chain in the correct order.)
62const UTXO_LOOKUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(6 * 60);
63
64/// A timeout applied to output lookup requests sent to the mempool. This is shorter than the
65/// timeout for the state UTXO lookups because a block is likely to be mined every 75 seconds
66/// after Blossom is active, changing the best chain tip and requiring re-verification of transactions
67/// in the mempool.
68///
69/// This is how long Zebra will wait for an output to be added to the mempool before verification
70/// of the transaction that spends it will fail.
71const MEMPOOL_OUTPUT_LOOKUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
72
73/// How long to wait after responding to a mempool request with a transaction that creates new
74/// transparent outputs before polling the mempool service so that it will try adding the verified
75/// transaction and responding to any potential `AwaitOutput` requests.
76///
77/// This should be long enough for the mempool service's `Downloads` to finish processing the
78/// response from the transaction verifier.
79const POLL_MEMPOOL_DELAY: std::time::Duration = Duration::from_millis(50);
80
81/// Asynchronous transaction verification.
82///
83/// # Correctness
84///
85/// Transaction verification requests should be wrapped in a timeout, so that
86/// out-of-order and invalid requests do not hang indefinitely. See the [`router`](`crate::router`)
87/// module documentation for details.
88pub struct Verifier<ZS, Mempool> {
89    network: Network,
90    state: Timeout<ZS>,
91    // TODO: Use an enum so that this can either be Pending(oneshot::Receiver) or Initialized(MempoolService)
92    mempool: Option<Timeout<Mempool>>,
93    script_verifier: script::Verifier,
94    mempool_setup_rx: oneshot::Receiver<Mempool>,
95}
96
97impl<ZS, Mempool> Verifier<ZS, Mempool>
98where
99    ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
100    ZS::Future: Send + 'static,
101    Mempool: Service<mempool::Request, Response = mempool::Response, Error = BoxError>
102        + Send
103        + Clone
104        + 'static,
105    Mempool::Future: Send + 'static,
106{
107    /// Create a new transaction verifier.
108    pub fn new(network: &Network, state: ZS, mempool_setup_rx: oneshot::Receiver<Mempool>) -> Self {
109        Self {
110            network: network.clone(),
111            state: Timeout::new(state, UTXO_LOOKUP_TIMEOUT),
112            mempool: None,
113            script_verifier: script::Verifier,
114            mempool_setup_rx,
115        }
116    }
117}
118
119impl<ZS>
120    Verifier<
121        ZS,
122        Buffer<BoxService<mempool::Request, mempool::Response, BoxError>, mempool::Request>,
123    >
124where
125    ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
126    ZS::Future: Send + 'static,
127{
128    /// Create a new transaction verifier with a closed channel receiver for mempool setup for tests.
129    #[cfg(test)]
130    pub fn new_for_tests(network: &Network, state: ZS) -> Self {
131        Self {
132            network: network.clone(),
133            state: Timeout::new(state, UTXO_LOOKUP_TIMEOUT),
134            mempool: None,
135            script_verifier: script::Verifier,
136            mempool_setup_rx: oneshot::channel().1,
137        }
138    }
139}
140
141/// Specifies whether a transaction should be verified as part of a block or as
142/// part of the mempool.
143///
144/// Transaction verification has slightly different consensus rules, depending on
145/// whether the transaction is to be included in a block on in the mempool.
146#[derive(Clone, Debug, Eq, PartialEq)]
147pub enum Request {
148    /// Verify the supplied transaction as part of a block.
149    Block {
150        /// The transaction hash.
151        transaction_hash: transaction::Hash,
152        /// The transaction itself.
153        transaction: Arc<Transaction>,
154        /// Set of transaction hashes that create new transparent outputs.
155        known_outpoint_hashes: Arc<HashSet<transaction::Hash>>,
156        /// Additional UTXOs which are known at the time of verification.
157        known_utxos: Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>>,
158        /// The height of the block containing this transaction.
159        height: block::Height,
160        /// The time that the block was mined.
161        time: DateTime<Utc>,
162    },
163    /// Verify the supplied transaction as part of the mempool.
164    ///
165    /// Mempool transactions do not have any additional UTXOs.
166    ///
167    /// Note: coinbase transactions are invalid in the mempool
168    Mempool {
169        /// The transaction itself.
170        transaction: UnminedTx,
171        /// The height of the next block.
172        ///
173        /// The next block is the first block that could possibly contain a
174        /// mempool transaction.
175        height: block::Height,
176    },
177}
178
179/// The response type for the transaction verifier service.
180/// Responses identify the transaction that was verified.
181#[derive(Clone, Debug, PartialEq)]
182pub enum Response {
183    /// A response to a block transaction verification request.
184    Block {
185        /// The witnessed transaction ID for this transaction.
186        ///
187        /// [`Response::Block`] responses can be uniquely identified by
188        /// [`UnminedTxId::mined_id`], because the block's authorizing data root
189        /// will be checked during contextual validation.
190        tx_id: UnminedTxId,
191
192        /// The miner fee for this transaction.
193        ///
194        /// `None` for coinbase transactions.
195        ///
196        /// # Consensus
197        ///
198        /// > The remaining value in the transparent transaction value pool
199        /// > of a coinbase transaction is destroyed.
200        ///
201        /// <https://zips.z.cash/protocol/protocol.pdf#transactions>
202        miner_fee: Option<Amount<NonNegative>>,
203
204        /// The number of legacy signature operations in this transaction's
205        /// transparent inputs and outputs.
206        sigops: u32,
207    },
208
209    /// A response to a mempool transaction verification request.
210    Mempool {
211        /// The full content of the verified mempool transaction.
212        /// Also contains the transaction fee and other associated fields.
213        ///
214        /// Mempool transactions always have a transaction fee,
215        /// because coinbase transactions are rejected from the mempool.
216        ///
217        /// [`Response::Mempool`] responses are uniquely identified by the
218        /// [`UnminedTxId`] variant for their transaction version.
219        transaction: VerifiedUnminedTx,
220
221        /// A list of spent [`transparent::OutPoint`]s that were found in
222        /// the mempool's list of `created_outputs`.
223        ///
224        /// Used by the mempool to determine dependencies between transactions
225        /// in the mempool and to avoid adding transactions with missing spends
226        /// to its verified set.
227        spent_mempool_outpoints: Vec<transparent::OutPoint>,
228    },
229}
230
231#[cfg(any(test, feature = "proptest-impl"))]
232impl From<VerifiedUnminedTx> for Response {
233    fn from(transaction: VerifiedUnminedTx) -> Self {
234        Response::Mempool {
235            transaction,
236            spent_mempool_outpoints: Vec::new(),
237        }
238    }
239}
240
241impl Request {
242    /// The transaction to verify that's in this request.
243    pub fn transaction(&self) -> Arc<Transaction> {
244        match self {
245            Request::Block { transaction, .. } => transaction.clone(),
246            Request::Mempool { transaction, .. } => transaction.transaction.clone(),
247        }
248    }
249
250    /// The unverified mempool transaction, if this is a mempool request.
251    pub fn mempool_transaction(&self) -> Option<UnminedTx> {
252        match self {
253            Request::Block { .. } => None,
254            Request::Mempool { transaction, .. } => Some(transaction.clone()),
255        }
256    }
257
258    /// The unmined transaction ID for the transaction in this request.
259    pub fn tx_id(&self) -> UnminedTxId {
260        match self {
261            // TODO: get the precalculated ID from the block verifier
262            Request::Block { transaction, .. } => transaction.unmined_id(),
263            Request::Mempool { transaction, .. } => transaction.id,
264        }
265    }
266
267    /// The mined transaction ID for the transaction in this request.
268    pub fn tx_mined_id(&self) -> transaction::Hash {
269        match self {
270            Request::Block {
271                transaction_hash, ..
272            } => *transaction_hash,
273            Request::Mempool { transaction, .. } => transaction.id.mined_id(),
274        }
275    }
276
277    /// The set of additional known unspent transaction outputs that's in this request.
278    pub fn known_utxos(&self) -> Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>> {
279        match self {
280            Request::Block { known_utxos, .. } => known_utxos.clone(),
281            Request::Mempool { .. } => HashMap::new().into(),
282        }
283    }
284
285    /// The set of additional known [`transparent::OutPoint`]s of unspent transaction outputs that's in this request.
286    pub fn known_outpoint_hashes(&self) -> Arc<HashSet<transaction::Hash>> {
287        match self {
288            Request::Block {
289                known_outpoint_hashes,
290                ..
291            } => known_outpoint_hashes.clone(),
292            Request::Mempool { .. } => HashSet::new().into(),
293        }
294    }
295
296    /// The height used to select the consensus rules for verifying this transaction.
297    pub fn height(&self) -> block::Height {
298        match self {
299            Request::Block { height, .. } | Request::Mempool { height, .. } => *height,
300        }
301    }
302
303    /// The block time used for lock time consensus rules validation.
304    pub fn block_time(&self) -> Option<DateTime<Utc>> {
305        match self {
306            Request::Block { time, .. } => Some(*time),
307            Request::Mempool { .. } => None,
308        }
309    }
310
311    /// The network upgrade to consider for the verification.
312    ///
313    /// This is based on the block height from the request, and the supplied `network`.
314    pub fn upgrade(&self, network: &Network) -> NetworkUpgrade {
315        NetworkUpgrade::current(network, self.height())
316    }
317
318    /// Returns true if the request is a mempool request.
319    pub fn is_mempool(&self) -> bool {
320        matches!(self, Request::Mempool { .. })
321    }
322}
323
324impl Response {
325    /// The unmined transaction ID for the transaction in this response.
326    pub fn tx_id(&self) -> UnminedTxId {
327        match self {
328            Response::Block { tx_id, .. } => *tx_id,
329            Response::Mempool { transaction, .. } => transaction.transaction.id,
330        }
331    }
332
333    /// The miner fee for the transaction in this response.
334    ///
335    /// Coinbase transactions do not have a miner fee,
336    /// and they don't need UTXOs to calculate their value balance,
337    /// because they don't spend any inputs.
338    pub fn miner_fee(&self) -> Option<Amount<NonNegative>> {
339        match self {
340            Response::Block { miner_fee, .. } => *miner_fee,
341            Response::Mempool { transaction, .. } => Some(transaction.miner_fee),
342        }
343    }
344
345    /// The number of legacy transparent signature operations in this transaction's
346    /// inputs and outputs.
347    pub fn sigops(&self) -> u32 {
348        match self {
349            Response::Block { sigops, .. } => *sigops,
350            Response::Mempool { transaction, .. } => transaction.legacy_sigop_count,
351        }
352    }
353
354    /// Returns true if the request is a mempool request.
355    pub fn is_mempool(&self) -> bool {
356        match self {
357            Response::Block { .. } => false,
358            Response::Mempool { .. } => true,
359        }
360    }
361}
362
363impl<ZS, Mempool> Service<Request> for Verifier<ZS, Mempool>
364where
365    ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
366    ZS::Future: Send + 'static,
367    Mempool: Service<mempool::Request, Response = mempool::Response, Error = BoxError>
368        + Send
369        + Clone
370        + 'static,
371    Mempool::Future: Send + 'static,
372{
373    type Response = Response;
374    type Error = TransactionError;
375    type Future =
376        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
377
378    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
379        // Note: The block verifier expects the transaction verifier to always be ready.
380
381        if self.mempool.is_none() {
382            if let Ok(mempool) = self.mempool_setup_rx.try_recv() {
383                self.mempool = Some(Timeout::new(mempool, MEMPOOL_OUTPUT_LOOKUP_TIMEOUT));
384            }
385        }
386
387        Poll::Ready(Ok(()))
388    }
389
390    // TODO: break up each chunk into its own method
391    fn call(&mut self, req: Request) -> Self::Future {
392        let script_verifier = self.script_verifier;
393        let network = self.network.clone();
394        let state = self.state.clone();
395        let mempool = self.mempool.clone();
396
397        let tx = req.transaction();
398        let tx_id = req.tx_id();
399        let span = tracing::debug_span!("tx", ?tx_id);
400
401        async move {
402            tracing::trace!(?tx_id, ?req, "got tx verify request");
403
404            if let Some(result) = Self::find_verified_unmined_tx(&req, mempool.clone(), state.clone()).await {
405                let verified_tx = result?;
406
407                return Ok(Response::Block {
408                    tx_id,
409                    miner_fee: Some(verified_tx.miner_fee),
410                    sigops: verified_tx.legacy_sigop_count
411                });
412            }
413
414            // Do quick checks first
415            check::has_inputs_and_outputs(&tx)?;
416            check::has_enough_orchard_flags(&tx)?;
417            check::consensus_branch_id(&tx, req.height(), &network)?;
418
419            // Validate the coinbase input consensus rules
420            if req.is_mempool() && tx.is_coinbase() {
421                return Err(TransactionError::CoinbaseInMempool);
422            }
423
424            if tx.is_coinbase() {
425                check::coinbase_tx_no_prevout_joinsplit_spend(&tx)?;
426            } else if !tx.is_valid_non_coinbase() {
427                return Err(TransactionError::NonCoinbaseHasCoinbaseInput);
428            }
429
430            // Validate `nExpiryHeight` consensus rules
431            if tx.is_coinbase() {
432                check::coinbase_expiry_height(&req.height(), &tx, &network)?;
433            } else {
434                check::non_coinbase_expiry_height(&req.height(), &tx)?;
435            }
436
437            // Consensus rule:
438            //
439            // > Either v_{pub}^{old} or v_{pub}^{new} MUST be zero.
440            //
441            // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
442            check::joinsplit_has_vpub_zero(&tx)?;
443
444            // [Canopy onward]: `vpub_old` MUST be zero.
445            // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
446            check::disabled_add_to_sprout_pool(&tx, req.height(), &network)?;
447
448            check::spend_conflicts(&tx)?;
449
450            tracing::trace!(?tx_id, "passed quick checks");
451
452            if let Some(block_time) = req.block_time() {
453                check::lock_time_has_passed(&tx, req.height(), block_time)?;
454            } else {
455                // Skip the state query if we don't need the time for this check.
456                let next_median_time_past = if tx.lock_time_is_time() {
457                    // This state query is much faster than loading UTXOs from the database,
458                    // so it doesn't need to be executed in parallel
459                    let state = state.clone();
460                    Some(Self::mempool_best_chain_next_median_time_past(state).await?.to_chrono())
461                } else {
462                    None
463                };
464
465                // This consensus check makes sure Zebra produces valid block templates.
466                check::lock_time_has_passed(&tx, req.height(), next_median_time_past)?;
467            }
468
469            // "The consensus rules applied to valueBalance, vShieldedOutput, and bindingSig
470            // in non-coinbase transactions MUST also be applied to coinbase transactions."
471            //
472            // This rule is implicitly implemented during Sapling and Orchard verification,
473            // because they do not distinguish between coinbase and non-coinbase transactions.
474            //
475            // Note: this rule originally applied to Sapling, but we assume it also applies to Orchard.
476            //
477            // https://zips.z.cash/zip-0213#specification
478
479            // Load spent UTXOs from state.
480            // The UTXOs are required for almost all the async checks.
481            let load_spent_utxos_fut =
482                Self::spent_utxos(tx.clone(), req.clone(), state.clone(), mempool.clone(),);
483            let (spent_utxos, spent_outputs, spent_mempool_outpoints) = load_spent_utxos_fut.await?;
484
485            // WONTFIX: Return an error for Request::Block as well to replace this check in
486            //       the state once #2336 has been implemented?
487            if req.is_mempool() {
488                Self::check_maturity_height(&network, &req, &spent_utxos)?;
489            }
490
491            let nu = req.upgrade(&network);
492            let cached_ffi_transaction =
493                Arc::new(CachedFfiTransaction::new(tx.clone(), Arc::new(spent_outputs), nu).map_err(|_| TransactionError::UnsupportedByNetworkUpgrade(tx.version(), nu))?);
494
495            tracing::trace!(?tx_id, "got state UTXOs");
496
497            let mut async_checks = match tx.as_ref() {
498                Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {
499                    tracing::debug!(?tx, "got transaction with wrong version");
500                    return Err(TransactionError::WrongVersion);
501                }
502                Transaction::V4 {
503                    joinsplit_data,
504                    ..
505                } => Self::verify_v4_transaction(
506                    &req,
507                    &network,
508                    script_verifier,
509                    cached_ffi_transaction.clone(),
510                    joinsplit_data,
511                )?,
512                Transaction::V5 {
513                    ..
514                } => Self::verify_v5_transaction(
515                    &req,
516                    &network,
517                    script_verifier,
518                    cached_ffi_transaction.clone(),
519                )?,
520                #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
521                Transaction::V6 {
522                    ..
523                } => Self::verify_v6_transaction(
524                    &req,
525                    &network,
526                    script_verifier,
527                    cached_ffi_transaction.clone(),
528                )?,
529            };
530
531            if let Some(unmined_tx) = req.mempool_transaction() {
532                let check_anchors_and_revealed_nullifiers_query = state
533                    .clone()
534                    .oneshot(zs::Request::CheckBestChainTipNullifiersAndAnchors(
535                        unmined_tx,
536                    ))
537                    .map(|res| {
538                        assert!(
539                            res? == zs::Response::ValidBestChainTipNullifiersAndAnchors,
540                            "unexpected response to CheckBestChainTipNullifiersAndAnchors request"
541                        );
542                        Ok(())
543                    }
544                );
545
546                async_checks.push(check_anchors_and_revealed_nullifiers_query);
547            }
548
549            tracing::trace!(?tx_id, "awaiting async checks...");
550
551            async_checks.check().await?;
552
553            tracing::trace!(?tx_id, "finished async checks");
554
555            // Get the `value_balance` to calculate the transaction fee.
556            let value_balance = tx.value_balance(&spent_utxos);
557
558            let zip233_amount = match *tx {
559            	#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
560                Transaction::V6{ .. } => tx.zip233_amount(),
561                _ => Amount::zero()
562            };
563
564            // Calculate the fee only for non-coinbase transactions.
565            let mut miner_fee = None;
566            if !tx.is_coinbase() {
567                // TODO: deduplicate this code with remaining_transaction_value()?
568                miner_fee = match value_balance {
569                    Ok(vb) => match vb.remaining_transaction_value() {
570                        Ok(tx_rtv) => match tx_rtv - zip233_amount {
571                            Ok(fee) => Some(fee),
572                            Err(_) => return Err(TransactionError::IncorrectFee),
573                        }
574                        Err(_) => return Err(TransactionError::IncorrectFee),
575                    },
576                    Err(_) => return Err(TransactionError::IncorrectFee),
577                };
578            }
579
580            let sigops = tx.sigops().map_err(zebra_script::Error::from)?;
581
582            let rsp = match req {
583                Request::Block { .. } => Response::Block {
584                    tx_id,
585                    miner_fee,
586                    sigops,
587                },
588                Request::Mempool { transaction: tx, .. } => {
589                    // TODO: `spent_outputs` may not align with `tx.inputs()` when a transaction
590                    // spends both chain and mempool UTXOs (mempool outputs are appended last by
591                    // `spent_utxos()`), causing policy checks to pair the wrong input with
592                    // the wrong spent output.
593                    // https://github.com/ZcashFoundation/zebra/issues/10346
594                    let spent_outputs = cached_ffi_transaction.all_previous_outputs().clone();
595                    let transaction = VerifiedUnminedTx::new(
596                        tx,
597                        miner_fee.expect("fee should have been checked earlier"),
598                        sigops,
599                        spent_outputs.into(),
600                    )?;
601
602                    if let Some(mut mempool) = mempool {
603                        tokio::spawn(async move {
604                            // Best-effort poll of the mempool to provide a timely response to
605                            // `sendrawtransaction` RPC calls or `AwaitOutput` mempool calls.
606                            tokio::time::sleep(POLL_MEMPOOL_DELAY).await;
607                            let _ = mempool
608                                .ready()
609                                .await
610                                .expect("mempool poll_ready() method should not return an error")
611                                .call(mempool::Request::CheckForVerifiedTransactions)
612                                .await;
613                        });
614                    }
615
616                    Response::Mempool { transaction, spent_mempool_outpoints }
617                },
618            };
619
620            Ok(rsp)
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> Verifier<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    /// Fetches the median-time-past of the *next* block after the best state tip.
642    ///
643    /// This is used to verify that the lock times of mempool transactions
644    /// can be included in any valid next block.
645    async fn mempool_best_chain_next_median_time_past(
646        state: Timeout<ZS>,
647    ) -> Result<DateTime32, TransactionError> {
648        let query = state
649            .clone()
650            .oneshot(zs::Request::BestChainNextMedianTimePast);
651
652        if let zebra_state::Response::BestChainNextMedianTimePast(median_time_past) = query
653            .await
654            .map_err(|e| TransactionError::ValidateMempoolLockTimeError(e.to_string()))?
655        {
656            Ok(median_time_past)
657        } else {
658            unreachable!("Request::BestChainNextMedianTimePast always responds with BestChainNextMedianTimePast")
659        }
660    }
661
662    /// Attempts to find a transaction in the mempool by its transaction hash and checks
663    /// that all of its dependencies are available in the block or in the state.  Waits
664    /// for UTXOs being spent by the given transaction to arrive in the state if they're
665    /// not found elsewhere.
666    ///
667    /// Returns [`Some(Ok(VerifiedUnminedTx))`](VerifiedUnminedTx) if successful,
668    /// None if the transaction id was not found in the mempool,
669    /// or `Some(Err(TransparentInputNotFound))` if the transaction was found, but some of its
670    /// dependencies were not found in the block or state after a timeout.
671    async fn find_verified_unmined_tx(
672        req: &Request,
673        mempool: Option<Timeout<Mempool>>,
674        state: Timeout<ZS>,
675    ) -> Option<Result<VerifiedUnminedTx, TransactionError>> {
676        let tx = req.transaction();
677
678        if req.is_mempool() || tx.is_coinbase() {
679            return None;
680        }
681
682        let mempool = mempool?;
683        let known_outpoint_hashes = req.known_outpoint_hashes();
684        let tx_id = req.tx_mined_id();
685
686        let mempool::Response::TransactionWithDeps {
687            transaction: verified_tx,
688            dependencies,
689        } = mempool
690            .oneshot(mempool::Request::TransactionWithDepsByMinedId(tx_id))
691            .await
692            .ok()?
693        else {
694            panic!("unexpected response to TransactionWithDepsByMinedId request");
695        };
696
697        // Note: This does not verify that the spends are in order, the spend order
698        //       should be verified during contextual validation in zebra-state.
699        let missing_deps: HashSet<_> = dependencies
700            .into_iter()
701            .filter(|dependency_id| !known_outpoint_hashes.contains(dependency_id))
702            .collect();
703
704        if missing_deps.is_empty() {
705            return Some(Ok(verified_tx));
706        }
707
708        let missing_outpoints = tx.inputs().iter().filter_map(|input| {
709            if let transparent::Input::PrevOut { outpoint, .. } = input {
710                missing_deps.contains(&outpoint.hash).then_some(outpoint)
711            } else {
712                None
713            }
714        });
715
716        for missing_outpoint in missing_outpoints {
717            let query = state
718                .clone()
719                .oneshot(zebra_state::Request::AwaitUtxo(*missing_outpoint));
720            match query.await {
721                Ok(zebra_state::Response::Utxo(_)) => {}
722                Err(_) => return Some(Err(TransactionError::TransparentInputNotFound)),
723                _ => unreachable!("AwaitUtxo always responds with Utxo"),
724            };
725        }
726
727        Some(Ok(verified_tx))
728    }
729
730    /// Wait for the UTXOs that are being spent by the given transaction.
731    ///
732    /// Looks up UTXOs that are being spent by the given transaction in the state or waits
733    /// for them to be added to the mempool for [`Mempool`](Request::Mempool) requests.
734    ///
735    /// Returns a triple containing:
736    /// - `OutPoint` -> `Utxo` map,
737    /// - vec of `Output`s in the same order as the matching inputs in the `tx`,
738    /// - vec of `Outpoint`s spent by a mempool `tx` that were not found in the best chain's utxo set.
739    async fn spent_utxos(
740        tx: Arc<Transaction>,
741        req: Request,
742        state: Timeout<ZS>,
743        mempool: Option<Timeout<Mempool>>,
744    ) -> Result<
745        (
746            HashMap<transparent::OutPoint, transparent::Utxo>,
747            Vec<transparent::Output>,
748            Vec<transparent::OutPoint>,
749        ),
750        TransactionError,
751    > {
752        let is_mempool = req.is_mempool();
753        // Additional UTXOs known at the time of validation,
754        // i.e., from previous transactions in the block.
755        let known_utxos = req.known_utxos();
756
757        let inputs = tx.inputs();
758        let mut spent_utxos = HashMap::new();
759        // Pre-allocate with None so we can fill each slot by input index, preserving input order
760        // even when chain and mempool UTXOs are fetched in separate passes.
761        let mut spent_outputs: Vec<Option<transparent::Output>> = vec![None; inputs.len()];
762        // Stores (input_idx, outpoint) for UTXOs not found in the best chain (fetched from mempool later).
763        let mut spent_mempool_outpoints: Vec<(usize, transparent::OutPoint)> = Vec::new();
764
765        for (input_idx, input) in inputs.iter().enumerate() {
766            if let transparent::Input::PrevOut { outpoint, .. } = input {
767                tracing::trace!("awaiting outpoint lookup");
768                let utxo = if let Some(output) = known_utxos.get(outpoint) {
769                    tracing::trace!("UXTO in known_utxos, discarding query");
770                    output.utxo.clone()
771                } else if is_mempool {
772                    let query = state
773                        .clone()
774                        .oneshot(zs::Request::UnspentBestChainUtxo(*outpoint));
775
776                    let zebra_state::Response::UnspentBestChainUtxo(utxo) = query
777                        .await
778                        .map_err(|_| TransactionError::TransparentInputNotFound)?
779                    else {
780                        unreachable!("UnspentBestChainUtxo always responds with Option<Utxo>")
781                    };
782
783                    let Some(utxo) = utxo else {
784                        spent_mempool_outpoints.push((input_idx, *outpoint));
785                        continue;
786                    };
787
788                    utxo
789                } else {
790                    let query = state
791                        .clone()
792                        .oneshot(zebra_state::Request::AwaitUtxo(*outpoint));
793                    if let zebra_state::Response::Utxo(utxo) = query.await? {
794                        utxo
795                    } else {
796                        unreachable!("AwaitUtxo always responds with Utxo")
797                    }
798                };
799                tracing::trace!(?utxo, "got UTXO");
800                spent_outputs[input_idx] = Some(utxo.output.clone());
801                spent_utxos.insert(*outpoint, utxo);
802            } else {
803                continue;
804            }
805        }
806
807        if let Some(mempool) = mempool {
808            for &(input_idx, spent_mempool_outpoint) in &spent_mempool_outpoints {
809                let query = mempool
810                    .clone()
811                    .oneshot(mempool::Request::AwaitOutput(spent_mempool_outpoint));
812
813                let output = match query.await {
814                    Ok(mempool::Response::UnspentOutput(output)) => output,
815                    Ok(_) => unreachable!("UnspentOutput always responds with UnspentOutput"),
816                    Err(err) => {
817                        return match err.downcast::<Elapsed>() {
818                            Ok(_) => Err(TransactionError::TransparentInputNotFound),
819                            Err(err) => Err(err.into()),
820                        };
821                    }
822                };
823
824                spent_outputs[input_idx] = Some(output.clone());
825                spent_utxos.insert(
826                    spent_mempool_outpoint,
827                    // Assume the Utxo height will be next height after the best chain tip height
828                    //
829                    // # Correctness
830                    //
831                    // If the tip height changes while an umined transaction is being verified,
832                    // the transaction must be re-verified before being added to the mempool.
833                    transparent::Utxo::new(output, req.height(), false),
834                );
835            }
836        } else if !spent_mempool_outpoints.is_empty() {
837            return Err(TransactionError::TransparentInputNotFound);
838        }
839
840        // Convert back to return types; slots are in input order.
841        let spent_outputs: Vec<transparent::Output> = spent_outputs.into_iter().flatten().collect();
842        let spent_mempool_outpoints: Vec<transparent::OutPoint> = spent_mempool_outpoints
843            .into_iter()
844            .map(|(_, op)| op)
845            .collect();
846
847        Ok((spent_utxos, spent_outputs, spent_mempool_outpoints))
848    }
849
850    /// Accepts `request`, a transaction verifier [`&Request`](Request),
851    /// and `spent_utxos`, a HashMap of UTXOs in the chain that are spent by this transaction.
852    ///
853    /// Gets the `transaction`, `height`, and `known_utxos` for the request and checks calls
854    /// [`check::tx_transparent_coinbase_spends_maturity`] to verify that every transparent
855    /// coinbase output spent by the transaction will have matured by `height`.
856    ///
857    /// Returns `Ok(())` if every transparent coinbase output spent by the transaction is
858    /// mature and valid for the request height, or a [`TransactionError`] if the transaction
859    /// spends transparent coinbase outputs that are immature and invalid for the request height.
860    pub fn check_maturity_height(
861        network: &Network,
862        request: &Request,
863        spent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
864    ) -> Result<(), TransactionError> {
865        check::tx_transparent_coinbase_spends_maturity(
866            network,
867            request.transaction(),
868            request.height(),
869            request.known_utxos(),
870            spent_utxos,
871        )
872    }
873
874    /// Verify a V4 transaction.
875    ///
876    /// Returns a set of asynchronous checks that must all succeed for the transaction to be
877    /// considered valid. These checks include:
878    ///
879    /// - transparent transfers
880    /// - sprout shielded data
881    /// - sapling shielded data
882    ///
883    /// The parameters of this method are:
884    ///
885    /// - the `request` to verify (that contains the transaction and other metadata, see [`Request`]
886    ///   for more information)
887    /// - the `network` to consider when verifying
888    /// - the `script_verifier` to use for verifying the transparent transfers
889    /// - the prepared `cached_ffi_transaction` used by the script verifier
890    /// - the Sprout `joinsplit_data` shielded data in the transaction
891    /// - the `sapling_shielded_data` in the transaction
892    #[allow(clippy::unwrap_in_result)]
893    fn verify_v4_transaction(
894        request: &Request,
895        network: &Network,
896        script_verifier: script::Verifier,
897        cached_ffi_transaction: Arc<CachedFfiTransaction>,
898        joinsplit_data: &Option<transaction::JoinSplitData<Groth16Proof>>,
899    ) -> Result<AsyncChecks, TransactionError> {
900        let tx = request.transaction();
901        let nu = request.upgrade(network);
902
903        Self::verify_v4_transaction_network_upgrade(&tx, nu)?;
904
905        let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle();
906
907        let sighash = cached_ffi_transaction
908            .sighasher()
909            .sighash(HashType::ALL, None);
910
911        Ok(Self::verify_transparent_inputs_and_outputs(
912            request,
913            script_verifier,
914            cached_ffi_transaction,
915        )?
916        .and(Self::verify_sprout_shielded_data(joinsplit_data, &sighash)?)
917        .and(Self::verify_sapling_bundle(sapling_bundle, &sighash)))
918    }
919
920    /// Verifies if a V4 `transaction` is supported by `network_upgrade`.
921    fn verify_v4_transaction_network_upgrade(
922        transaction: &Transaction,
923        network_upgrade: NetworkUpgrade,
924    ) -> Result<(), TransactionError> {
925        match network_upgrade {
926            // Supports V4 transactions
927            //
928            // # Consensus
929            //
930            // > [Sapling to Canopy inclusive, pre-NU5] The transaction version number MUST be 4,
931            // > and the version group ID MUST be 0x892F2085.
932            //
933            // > [NU5 onward] The transaction version number MUST be 4 or 5.
934            // > If the transaction version number is 4 then the version group ID MUST be 0x892F2085.
935            // > If the transaction version number is 5 then the version group ID MUST be 0x26A7270A.
936            //
937            // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
938            //
939            // Note: Here we verify the transaction version number of the above two rules, the group
940            // id is checked in zebra-chain crate, in the transaction serialize.
941            NetworkUpgrade::Sapling
942            | NetworkUpgrade::Blossom
943            | NetworkUpgrade::Heartwood
944            | NetworkUpgrade::Canopy
945            | NetworkUpgrade::Nu5
946            | NetworkUpgrade::Nu6
947            | NetworkUpgrade::Nu6_1 => Ok(()),
948
949            #[cfg(zcash_unstable = "zfuture")]
950            NetworkUpgrade::ZFuture => Ok(()),
951
952            // Does not support V4 transactions
953            NetworkUpgrade::Genesis
954            | NetworkUpgrade::BeforeOverwinter
955            | NetworkUpgrade::Overwinter
956            | NetworkUpgrade::Nu7 => Err(TransactionError::UnsupportedByNetworkUpgrade(
957                transaction.version(),
958                network_upgrade,
959            )),
960        }
961    }
962
963    /// Verify a V5 transaction.
964    ///
965    /// Returns a set of asynchronous checks that must all succeed for the transaction to be
966    /// considered valid. These checks include:
967    ///
968    /// - transaction support by the considered network upgrade (see [`Request::upgrade`])
969    /// - transparent transfers
970    /// - sapling shielded data (TODO)
971    /// - orchard shielded data (TODO)
972    ///
973    /// The parameters of this method are:
974    ///
975    /// - the `request` to verify (that contains the transaction and other metadata, see [`Request`]
976    ///   for more information)
977    /// - the `network` to consider when verifying
978    /// - the `script_verifier` to use for verifying the transparent transfers
979    /// - the prepared `cached_ffi_transaction` used by the script verifier
980    /// - the sapling shielded data of the transaction, if any
981    /// - the orchard shielded data of the transaction, if any
982    #[allow(clippy::unwrap_in_result)]
983    fn verify_v5_transaction(
984        request: &Request,
985        network: &Network,
986        script_verifier: script::Verifier,
987        cached_ffi_transaction: Arc<CachedFfiTransaction>,
988    ) -> Result<AsyncChecks, TransactionError> {
989        let transaction = request.transaction();
990        let nu = request.upgrade(network);
991
992        Self::verify_v5_transaction_network_upgrade(&transaction, nu)?;
993
994        let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle();
995        let orchard_bundle = cached_ffi_transaction.sighasher().orchard_bundle();
996
997        let sighash = cached_ffi_transaction
998            .sighasher()
999            .sighash(HashType::ALL, None);
1000
1001        Ok(Self::verify_transparent_inputs_and_outputs(
1002            request,
1003            script_verifier,
1004            cached_ffi_transaction,
1005        )?
1006        .and(Self::verify_sapling_bundle(sapling_bundle, &sighash))
1007        .and(Self::verify_orchard_bundle(orchard_bundle, &sighash)))
1008    }
1009
1010    /// Verifies if a V5 `transaction` is supported by `network_upgrade`.
1011    fn verify_v5_transaction_network_upgrade(
1012        transaction: &Transaction,
1013        network_upgrade: NetworkUpgrade,
1014    ) -> Result<(), TransactionError> {
1015        match network_upgrade {
1016            // Supports V5 transactions
1017            //
1018            // # Consensus
1019            //
1020            // > [NU5 onward] The transaction version number MUST be 4 or 5.
1021            // > If the transaction version number is 4 then the version group ID MUST be 0x892F2085.
1022            // > If the transaction version number is 5 then the version group ID MUST be 0x26A7270A.
1023            //
1024            // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1025            //
1026            // Note: Here we verify the transaction version number of the above rule, the group
1027            // id is checked in zebra-chain crate, in the transaction serialize.
1028            NetworkUpgrade::Nu5
1029            | NetworkUpgrade::Nu6
1030            | NetworkUpgrade::Nu6_1
1031            | NetworkUpgrade::Nu7 => Ok(()),
1032
1033            #[cfg(zcash_unstable = "zfuture")]
1034            NetworkUpgrade::ZFuture => Ok(()),
1035
1036            // Does not support V5 transactions
1037            NetworkUpgrade::Genesis
1038            | NetworkUpgrade::BeforeOverwinter
1039            | NetworkUpgrade::Overwinter
1040            | NetworkUpgrade::Sapling
1041            | NetworkUpgrade::Blossom
1042            | NetworkUpgrade::Heartwood
1043            | NetworkUpgrade::Canopy => Err(TransactionError::UnsupportedByNetworkUpgrade(
1044                transaction.version(),
1045                network_upgrade,
1046            )),
1047        }
1048    }
1049
1050    /// Passthrough to verify_v5_transaction, but for V6 transactions.
1051    #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
1052    fn verify_v6_transaction(
1053        request: &Request,
1054        network: &Network,
1055        script_verifier: script::Verifier,
1056        cached_ffi_transaction: Arc<CachedFfiTransaction>,
1057    ) -> Result<AsyncChecks, TransactionError> {
1058        Self::verify_v5_transaction(request, network, script_verifier, cached_ffi_transaction)
1059    }
1060
1061    /// Verifies if a transaction's transparent inputs are valid using the provided
1062    /// `script_verifier` and `cached_ffi_transaction`.
1063    ///
1064    /// Returns script verification responses via the `utxo_sender`.
1065    fn verify_transparent_inputs_and_outputs(
1066        request: &Request,
1067        script_verifier: script::Verifier,
1068        cached_ffi_transaction: Arc<CachedFfiTransaction>,
1069    ) -> Result<AsyncChecks, TransactionError> {
1070        let transaction = request.transaction();
1071
1072        if transaction.is_coinbase() {
1073            // The script verifier only verifies PrevOut inputs and their corresponding UTXOs.
1074            // Coinbase transactions don't have any PrevOut inputs.
1075            Ok(AsyncChecks::new())
1076        } else {
1077            // feed all of the inputs to the script verifier
1078            let inputs = transaction.inputs();
1079
1080            let script_checks = (0..inputs.len())
1081                .map(move |input_index| {
1082                    let request = script::Request {
1083                        cached_ffi_transaction: cached_ffi_transaction.clone(),
1084                        input_index,
1085                    };
1086
1087                    script_verifier.oneshot(request)
1088                })
1089                .collect();
1090
1091            Ok(script_checks)
1092        }
1093    }
1094
1095    /// Verifies a transaction's Sprout shielded join split data.
1096    fn verify_sprout_shielded_data(
1097        joinsplit_data: &Option<transaction::JoinSplitData<Groth16Proof>>,
1098        shielded_sighash: &SigHash,
1099    ) -> Result<AsyncChecks, TransactionError> {
1100        let mut checks = AsyncChecks::new();
1101
1102        if let Some(joinsplit_data) = joinsplit_data {
1103            for joinsplit in joinsplit_data.joinsplits() {
1104                // # Consensus
1105                //
1106                // > The proof π_ZKJoinSplit MUST be valid given a
1107                // > primary input formed from the relevant other fields and h_{Sig}
1108                //
1109                // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
1110                //
1111                // Queue the verification of the Groth16 spend proof
1112                // for each JoinSplit description while adding the
1113                // resulting future to our collection of async
1114                // checks that (at a minimum) must pass for the
1115                // transaction to verify.
1116                checks.push(primitives::groth16::JOINSPLIT_VERIFIER.oneshot(
1117                    DescriptionWrapper(&(joinsplit, &joinsplit_data.pub_key)).try_into()?,
1118                ));
1119            }
1120
1121            // # Consensus
1122            //
1123            // > If effectiveVersion ≥ 2 and nJoinSplit > 0, then:
1124            // > - joinSplitPubKey MUST be a valid encoding of an Ed25519 validating key
1125            // > - joinSplitSig MUST represent a valid signature under
1126            //     joinSplitPubKey of dataToBeSigned, as defined in § 4.11
1127            //
1128            // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1129            //
1130            // The `if` part is indirectly enforced, since the `joinsplit_data`
1131            // is only parsed if those conditions apply in
1132            // [`Transaction::zcash_deserialize`].
1133            //
1134            // The valid encoding is defined in
1135            //
1136            // > A valid Ed25519 validating key is defined as a sequence of 32
1137            // > bytes encoding a point on the Ed25519 curve
1138            //
1139            // https://zips.z.cash/protocol/protocol.pdf#concreteed25519
1140            //
1141            // which is enforced during signature verification, in both batched
1142            // and single verification, when decompressing the encoded point.
1143            //
1144            // Queue the validation of the JoinSplit signature while
1145            // adding the resulting future to our collection of
1146            // async checks that (at a minimum) must pass for the
1147            // transaction to verify.
1148            //
1149            // https://zips.z.cash/protocol/protocol.pdf#sproutnonmalleability
1150            // https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
1151            let ed25519_verifier = primitives::ed25519::VERIFIER.clone();
1152            let ed25519_item =
1153                (joinsplit_data.pub_key, joinsplit_data.sig, shielded_sighash).into();
1154
1155            checks.push(ed25519_verifier.oneshot(ed25519_item));
1156        }
1157
1158        Ok(checks)
1159    }
1160
1161    /// Verifies a transaction's Sapling shielded data.
1162    fn verify_sapling_bundle(
1163        bundle: Option<sapling_crypto::Bundle<sapling_crypto::bundle::Authorized, ZatBalance>>,
1164        sighash: &SigHash,
1165    ) -> AsyncChecks {
1166        let mut async_checks = AsyncChecks::new();
1167
1168        // The Sapling batch verifier checks the following consensus rules:
1169        //
1170        // # Consensus
1171        //
1172        // > The proof π_ZKSpend MUST be valid given a primary input formed from the other fields
1173        // > except spendAuthSig.
1174        //
1175        // > The spend authorization signature MUST be a valid SpendAuthSig signature over SigHash
1176        // > using rk as the validating key.
1177        //
1178        // > [NU5 onward] As specified in § 5.4.7 ‘RedDSA, RedJubjub, and RedPallas’ on p. 88, the
1179        // > validation of the 𝑅 component of the signature changes to prohibit non-canonical
1180        // > encodings.
1181        //
1182        // https://zips.z.cash/protocol/protocol.pdf#spenddesc
1183        //
1184        // # Consensus
1185        //
1186        // > The proof π_ZKOutput MUST be valid given a primary input formed from the other fields
1187        // > except C^enc and C^out.
1188        //
1189        // https://zips.z.cash/protocol/protocol.pdf#outputdesc
1190        //
1191        // # Consensus
1192        //
1193        // > The Spend transfers and Action transfers of a transaction MUST be consistent with its
1194        // > vbalanceSapling value as specified in § 4.13 ‘Balance and Binding Signature (Sapling)’.
1195        //
1196        // https://zips.z.cash/protocol/protocol.pdf#spendsandoutputs
1197        //
1198        // # Consensus
1199        //
1200        // > [Sapling onward] If effectiveVersion ≥ 4 and nSpendsSapling + nOutputsSapling > 0,
1201        // > then:
1202        // >
1203        // > – let bvk^{Sapling} and SigHash be as defined in § 4.13;
1204        // > – bindingSigSapling MUST represent a valid signature under the transaction binding
1205        // >   validating key bvk Sapling of SigHash — i.e.
1206        // >   BindingSig^{Sapling}.Validate_{bvk^{Sapling}}(SigHash, bindingSigSapling ) = 1.
1207        //
1208        // Note that the `if` part is indirectly enforced, since the `sapling_shielded_data` is only
1209        // parsed if those conditions apply in [`Transaction::zcash_deserialize`].
1210        //
1211        // > [NU5 onward] As specified in § 5.4.7, the validation of the 𝑅 component of the
1212        // > signature changes to prohibit non-canonical encodings.
1213        //
1214        // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1215        if let Some(bundle) = bundle {
1216            async_checks.push(
1217                primitives::sapling::VERIFIER
1218                    .clone()
1219                    .oneshot(primitives::sapling::Item::new(bundle, *sighash)),
1220            );
1221        }
1222
1223        async_checks
1224    }
1225
1226    /// Verifies a transaction's Orchard shielded data.
1227    fn verify_orchard_bundle(
1228        bundle: Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1229        sighash: &SigHash,
1230    ) -> AsyncChecks {
1231        let mut async_checks = AsyncChecks::new();
1232
1233        if let Some(bundle) = bundle {
1234            // # Consensus
1235            //
1236            // > The proof 𝜋 MUST be valid given a primary input (cv, rt^{Orchard},
1237            // > nf, rk, cm_x, enableSpends, enableOutputs)
1238            //
1239            // https://zips.z.cash/protocol/protocol.pdf#actiondesc
1240            //
1241            // Unlike Sapling, Orchard shielded transactions have a single
1242            // aggregated Halo2 proof per transaction, even with multiple
1243            // Actions in one transaction. So we queue it for verification
1244            // only once instead of queuing it up for every Action description.
1245            async_checks.push(
1246                primitives::halo2::VERIFIER
1247                    .clone()
1248                    .oneshot(primitives::halo2::Item::new(bundle, *sighash)),
1249            );
1250        }
1251
1252        async_checks
1253    }
1254}
1255
1256/// A set of unordered asynchronous checks that should succeed.
1257///
1258/// A wrapper around [`FuturesUnordered`] with some auxiliary methods.
1259struct AsyncChecks(FuturesUnordered<Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>>);
1260
1261impl AsyncChecks {
1262    /// Create an empty set of unordered asynchronous checks.
1263    pub fn new() -> Self {
1264        AsyncChecks(FuturesUnordered::new())
1265    }
1266
1267    /// Push a check into the set.
1268    pub fn push(&mut self, check: impl Future<Output = Result<(), BoxError>> + Send + 'static) {
1269        self.0.push(check.boxed());
1270    }
1271
1272    /// Push a set of checks into the set.
1273    ///
1274    /// This method can be daisy-chained.
1275    pub fn and(mut self, checks: AsyncChecks) -> Self {
1276        self.0.extend(checks.0);
1277        self
1278    }
1279
1280    /// Wait until all checks in the set finish.
1281    ///
1282    /// If any of the checks fail, this method immediately returns the error and cancels all other
1283    /// checks by dropping them.
1284    async fn check(mut self) -> Result<(), BoxError> {
1285        // Wait for all asynchronous checks to complete
1286        // successfully, or fail verification if they error.
1287        while let Some(check) = self.0.next().await {
1288            tracing::trace!(?check, remaining = self.0.len());
1289            check?;
1290        }
1291
1292        Ok(())
1293    }
1294}
1295
1296impl<F> FromIterator<F> for AsyncChecks
1297where
1298    F: Future<Output = Result<(), BoxError>> + Send + 'static,
1299{
1300    fn from_iter<I>(iterator: I) -> Self
1301    where
1302        I: IntoIterator<Item = F>,
1303    {
1304        AsyncChecks(iterator.into_iter().map(FutureExt::boxed).collect())
1305    }
1306}