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}