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