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, 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 fn call(&mut self, req: Request) -> Self::Future {
391 match req {
392 Request::Block { .. } => self.verify_block(req),
393 Request::Mempool { .. } => self.verify_mempool(req),
394 }
395 }
396}
397
398impl<ZS, Mempool> Verifier<ZS, Mempool>
399where
400 ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
401 ZS::Future: Send + 'static,
402 Mempool: Service<mempool::Request, Response = mempool::Response, Error = BoxError>
403 + Send
404 + Clone
405 + 'static,
406 Mempool::Future: Send + 'static,
407{
408 /// Verifies a transaction in block context.
409 fn verify_block(
410 &self,
411 req: Request,
412 ) -> Pin<Box<dyn Future<Output = Result<Response, TransactionError>> + Send + 'static>> {
413 let script_verifier = self.script_verifier;
414 let network = self.network.clone();
415 let state = self.state.clone();
416
417 let tx = req.transaction();
418 let tx_id = req.tx_id();
419 let height = req.height();
420 let time = req.block_time().expect("block requests always have a time");
421 let known_utxos = req.known_utxos();
422 let nu = req.upgrade(&network);
423 let span = tracing::debug_span!("tx", ?tx_id);
424
425 async move {
426 tracing::trace!(?tx_id, ?req, "got tx verify request");
427
428 // Do quick checks first
429 Self::check_structure_and_network_rules(tx.as_ref(), height, &network)?;
430
431 if tx.is_coinbase() {
432 check::coinbase_tx_no_prevout_joinsplit_spend(&tx)?;
433 } else if !tx.is_valid_non_coinbase() {
434 return Err(TransactionError::NonCoinbaseHasCoinbaseInput);
435 }
436
437 // Validate `nExpiryHeight` consensus rules
438 if tx.is_coinbase() {
439 check::coinbase_expiry_height(&height, &tx, &network)?;
440 } else {
441 check::non_coinbase_expiry_height(&height, &tx)?;
442 }
443
444 // Transaction invariants that apply regardless of request type or transaction version.
445 // These are pure consensus rules over the transaction structure and must always hold.
446 Self::check_transaction_invariants(tx.as_ref(), height, &network)?;
447
448 tracing::trace!(?tx_id, "passed quick checks");
449
450 // Block transactions are checked against the block's own time directly.
451 check::lock_time_has_passed(&tx, height, time)?;
452
453 // "The consensus rules applied to valueBalance, vShieldedOutput, and bindingSig
454 // in non-coinbase transactions MUST also be applied to coinbase transactions."
455 //
456 // This rule is implicitly implemented during Sapling and Orchard verification,
457 // because they do not distinguish between coinbase and non-coinbase transactions.
458 //
459 // Note: this rule originally applied to Sapling, but we assume it also applies to Orchard.
460 //
461 // https://zips.z.cash/zip-0213#specification
462
463 // Load spent UTXOs from state.
464 // The UTXOs are required for almost all the async checks.
465 let (spent_utxos, spent_outputs) =
466 Self::block_spent_utxos(tx.clone(), known_utxos, state.clone()).await?;
467
468 let cached_ffi_transaction =
469 Arc::new(CachedFfiTransaction::new(tx.clone(), Arc::new(spent_outputs), nu).map_err(|_| TransactionError::UnsupportedByNetworkUpgrade(tx.version(), nu))?);
470
471 tracing::trace!(?tx_id, "got state UTXOs");
472
473 // Select version-specific async verification pipeline
474 let async_checks = Self::dispatch_version_verification(
475 tx.as_ref(),
476 nu,
477 script_verifier,
478 cached_ffi_transaction.clone()
479 )?;
480
481 tracing::trace!(?tx_id, "awaiting async checks...");
482
483 async_checks.check().await?;
484
485 tracing::trace!(?tx_id, "finished async checks");
486
487 let miner_fee = if tx.is_coinbase() {
488 None
489 } else {
490 Some(Self::miner_fee(tx.as_ref(), &spent_utxos)?)
491 };
492 let sigops = tx.sigops().map_err(zebra_script::Error::from)?;
493
494 Ok(Response::Block {
495 tx_id,
496 miner_fee,
497 // In block validation, the consensus sigop total must include P2SH
498 // redeem-script sigops, matching zcashd's `ConnectBlock` which sums
499 // `GetLegacySigOpCount` and `GetP2SHSigOpCount` per transaction before
500 // comparing against `MAX_BLOCK_SIGOPS`. Coinbase inputs contribute zero P2SH
501 // sigops. See
502 // <https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-jv4h-j224-23cc>.
503 sigops: sigops.saturating_add(cached_ffi_transaction.p2sh_sigops()),
504 })
505 }
506 .inspect(move |result| {
507 // Hide the transaction data to avoid filling the logs
508 tracing::trace!(?tx_id, result = ?result.as_ref().map(|_tx| ()), "got tx verify result");
509 })
510 .instrument(span)
511 .boxed()
512 }
513
514 /// Verifies a transaction in mempool context.
515 fn verify_mempool(
516 &self,
517 req: Request,
518 ) -> Pin<Box<dyn Future<Output = Result<Response, TransactionError>> + Send + 'static>> {
519 let script_verifier = self.script_verifier;
520 let network = self.network.clone();
521 let state = self.state.clone();
522 let mempool = self.mempool.clone();
523
524 let tx = req.transaction();
525 let tx_id = req.tx_id();
526 let height = req.height();
527 let unmined_tx = req
528 .mempool_transaction()
529 .expect("mempool requests always contain an unmined transaction");
530 let nu = req.upgrade(&network);
531 let span = tracing::debug_span!("tx", ?tx_id);
532
533 async move {
534 tracing::trace!(?tx_id, ?req, "got tx verify request");
535
536 // Do quick checks first
537 Self::check_structure_and_network_rules(tx.as_ref(), height, &network)?;
538
539 // Validate the coinbase input consensus rules
540 if tx.is_coinbase() {
541 return Err(TransactionError::CoinbaseInMempool);
542 }
543
544 if !tx.is_valid_non_coinbase() {
545 return Err(TransactionError::NonCoinbaseHasCoinbaseInput);
546 }
547
548 // Validate `nExpiryHeight` consensus rules
549 check::non_coinbase_expiry_height(&height, &tx)?;
550
551 // Transaction invariants that apply regardless of request type or transaction version.
552 // These are pure consensus rules over the transaction structure and must always hold.
553 Self::check_transaction_invariants(tx.as_ref(), height, &network)?;
554
555 tracing::trace!(?tx_id, "passed quick checks");
556
557 // Mempool transactions are checked against the next median-time-past from state.
558 Self::verify_mempool_lock_time(tx.as_ref(), height, state.clone()).await?;
559
560 // "The consensus rules applied to valueBalance, vShieldedOutput, and bindingSig
561 // in non-coinbase transactions MUST also be applied to coinbase transactions."
562 //
563 // This rule is implicitly implemented during Sapling and Orchard verification,
564 // because they do not distinguish between coinbase and non-coinbase transactions.
565 //
566 // Note: this rule originally applied to Sapling, but we assume it also applies to Orchard.
567 //
568 // https://zips.z.cash/zip-0213#specification
569
570 // Load spent UTXOs from state.
571 // The UTXOs are required for almost all the async checks.
572 let (spent_utxos, spent_outputs, spent_mempool_outpoints) =
573 Self::mempool_spent_utxos(tx.clone(), height, state.clone(), mempool.clone()).await?;
574
575 // Mempool transactions have no block context, so there are no outputs from
576 // earlier transactions in the same block to consider.
577 Self::check_maturity_height(tx.clone(), height, &network, &spent_utxos)?;
578
579 // Reject non-standard input scripts (oversized or non-push-only
580 // scriptSigs, and high-sigop P2SH redeem scripts) *before*
581 // doing expensive script verification, to avoid DoS attacks on
582 // the script interpreter.
583 check::mempool_standard_input_scripts(tx.as_ref(), &spent_outputs)?;
584
585 // Apply ZIP-317 policy before expensive cryptographic verification.
586 let miner_fee = Self::miner_fee(tx.as_ref(), &spent_utxos)?;
587 let unpaid_actions = transaction::zip317::unpaid_actions(&unmined_tx, miner_fee);
588 transaction::zip317::mempool_checks(unpaid_actions, miner_fee, unmined_tx.size)?;
589
590 let cached_ffi_transaction =
591 Arc::new(CachedFfiTransaction::new(tx.clone(), Arc::new(spent_outputs), nu).map_err(|_| TransactionError::UnsupportedByNetworkUpgrade(tx.version(), nu))?);
592
593 tracing::trace!(?tx_id, "got state UTXOs");
594
595 // Select version-specific async verification pipeline
596 let mut async_checks = Self::dispatch_version_verification(
597 tx.as_ref(),
598 nu,
599 script_verifier,
600 cached_ffi_transaction.clone()
601 )?;
602
603 let check_anchors_and_revealed_nullifiers_query = state
604 .clone()
605 .oneshot(zs::Request::CheckBestChainTipNullifiersAndAnchors(
606 unmined_tx.clone(),
607 ))
608 .map(|res| {
609 assert!(
610 res? == zs::Response::ValidBestChainTipNullifiersAndAnchors,
611 "unexpected response to CheckBestChainTipNullifiersAndAnchors request"
612 );
613 Ok(())
614 });
615
616 async_checks.push(check_anchors_and_revealed_nullifiers_query);
617
618 tracing::trace!(?tx_id, "awaiting async checks...");
619
620 async_checks.check().await?;
621
622 tracing::trace!(?tx_id, "finished async checks");
623
624 let sigops = tx.sigops().map_err(zebra_script::Error::from)?;
625
626 // TODO: `spent_outputs` may not align with `tx.inputs()` when a transaction
627 // spends both chain and mempool UTXOs (mempool outputs are appended last by
628 // `mempool_spent_utxos()`), causing policy checks to pair the wrong input with
629 // the wrong spent output.
630 // https://github.com/ZcashFoundation/zebra/issues/10346
631 let spent_outputs = cached_ffi_transaction.all_previous_outputs().clone();
632
633 let transaction = VerifiedUnminedTx::new(
634 unmined_tx,
635 miner_fee,
636 sigops,
637 cached_ffi_transaction.p2sh_sigops(),
638 spent_outputs.into(),
639 )?;
640
641 if let Some(mut mempool) = mempool {
642 tokio::spawn(async move {
643 // Best-effort poll of the mempool to provide a timely response to
644 // `sendrawtransaction` RPC calls or `AwaitOutput` mempool calls.
645 tokio::time::sleep(POLL_MEMPOOL_DELAY).await;
646 let _ = mempool
647 .ready()
648 .await
649 .expect("mempool poll_ready() method should not return an error")
650 .call(mempool::Request::CheckForVerifiedTransactions)
651 .await;
652 });
653 }
654
655 Ok(Response::Mempool { transaction, spent_mempool_outpoints })
656 }
657 .inspect(move |result| {
658 // Hide the transaction data to avoid filling the logs
659 tracing::trace!(?tx_id, result = ?result.as_ref().map(|_tx| ()), "got tx verify result");
660 })
661 .instrument(span)
662 .boxed()
663 }
664
665 /// Performs basic structural validation and Orchard-related network upgrade rules.
666 fn check_structure_and_network_rules(
667 tx: &Transaction,
668 height: block::Height,
669 network: &Network,
670 ) -> Result<(), TransactionError> {
671 // The network upgrade active at this height is used by several of the checks below;
672 // `NetworkUpgrade::current` rebuilds the activation-height map on each call, so compute it
673 // once and share it rather than recomputing it per check.
674 let network_upgrade = NetworkUpgrade::current(network, height);
675
676 check::has_inputs_and_outputs(tx)?;
677 check::has_enough_orchard_flags(tx)?;
678 // NU6.3 / Ironwood flag rules (no-ops for pre-v6 transactions).
679 check::has_enough_ironwood_flags(tx)?;
680 check::orchard_cross_address_disabled(tx)?;
681 // [NU6.3 onward] valueBalanceOrchard must be non-negative (Orchard pool frozen against new
682 // inflows; see `orchard_value_balance_non_negative`).
683 check::orchard_value_balance_non_negative(tx, network_upgrade)?;
684 // [NU6.3 onward] Coinbase transactions must have an empty Orchard component (new shielded
685 // coinbase value is routed to the Ironwood pool instead).
686 check::coinbase_orchard_component_empty(tx, network_upgrade)?;
687 check::consensus_branch_id(tx, height, network)?;
688
689 // Soft fork: temporarily require transactions to not contain Orchard actions.
690 //
691 // This soft fork was added while NU 6.1 was the active epoch on the Zcash
692 // chain, but we apply it uniformly even if NU 6.1 is not active in case it is
693 // ported to other chains with a different sequence of NUs.
694 //
695 // This will be treated as "Rules that apply generally before the next NU"
696 // when we add the NU that re-enables Orchard actions.
697 if network.is_orchard_temporarily_disabled(height) && tx.orchard_shielded_data().is_some() {
698 return Err(TransactionError::Other(
699 "transaction has Orchard actions (temporarily disabled)".into(),
700 ));
701 }
702
703 // From the network upgrade that re-enables Orchard actions (NU6.2), require
704 // that any Orchard proof has the canonical length for its number of actions.
705 // A proof that is present but not canonically sized can be padded with
706 // arbitrary trailing data without affecting its validity, allowing excess
707 // bandwidth and storage costs to be imposed while paying only fees sized to a
708 // canonical proof (GHSA-jfw5-j458-pfv6).
709 //
710 // This is a constricting rule, so it is gated on that network upgrade:
711 // Orchard actions mined before it, under earlier rules that did not enforce
712 // the proof size, must remain valid so that nodes can sync and reindex the
713 // chain before the soft fork that temporarily disabled Orchard. Orchard
714 // bundles are deserialized leniently, so the size is checked here, where the
715 // block height is available, rather than during parsing.
716 //
717 // The gate activates at the NU6.2 activation height committed in
718 // MAINNET/TESTNET_ACTIVATION_HEIGHTS. See
719 // `Network::orchard_canonical_proof_size_rule_active`.
720 if network.orchard_canonical_proof_size_rule_active(height) {
721 if let Some(orchard_shielded_data) = tx.orchard_shielded_data() {
722 if !orchard_shielded_data.proof_size_is_canonical() {
723 return Err(TransactionError::OrchardProofSize);
724 }
725 }
726 }
727
728 // The Ironwood bundle's Halo2 proof must also have a canonical size. Ironwood only exists
729 // from NU6.3 onward (there is no legacy lenient period as there was for Orchard), so this is
730 // enforced unconditionally whenever an Ironwood bundle is present. Like the Orchard bundle,
731 // Ironwood bundles are deserialized leniently, so the size is checked here rather than during
732 // parsing.
733 if let Some(ironwood_shielded_data) = tx.ironwood_shielded_data() {
734 if !ironwood_shielded_data.proof_size_is_canonical() {
735 return Err(TransactionError::IronwoodProofSize);
736 }
737 }
738
739 Ok(())
740 }
741
742 /// Validates transaction invariants.
743 fn check_transaction_invariants(
744 tx: &Transaction,
745 height: block::Height,
746 network: &Network,
747 ) -> Result<(), TransactionError> {
748 // Consensus rule:
749 //
750 // > Either v_{pub}^{old} or v_{pub}^{new} MUST be zero.
751 //
752 // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
753 check::joinsplit_has_vpub_zero(tx)?;
754
755 // [Canopy onward]: `vpub_old` MUST be zero.
756 // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
757 check::disabled_add_to_sprout_pool(tx, height, network)?;
758
759 check::spend_conflicts(tx)?;
760
761 Ok(())
762 }
763
764 /// Validates mempool lock-time consensus rules.
765 ///
766 /// Queries state only for time-based lock times.
767 async fn verify_mempool_lock_time(
768 tx: &Transaction,
769 height: block::Height,
770 state: Timeout<ZS>,
771 ) -> Result<(), TransactionError> {
772 // Skip the state query if we don't need the time for this check.
773 let next_median_time_past = if tx.lock_time_is_time() {
774 // This state query is much faster than loading UTXOs from the database,
775 // so it doesn't need to be executed in parallel
776 Some(
777 Self::mempool_best_chain_next_median_time_past(state)
778 .await?
779 .to_chrono(),
780 )
781 } else {
782 None
783 };
784
785 // This consensus check makes sure Zebra produces valid block templates.
786 check::lock_time_has_passed(tx, height, next_median_time_past)?;
787
788 Ok(())
789 }
790
791 /// Fetches the median-time-past of the *next* block after the best state tip.
792 ///
793 /// This is used to verify that the lock times of mempool transactions
794 /// can be included in any valid next block.
795 async fn mempool_best_chain_next_median_time_past(
796 state: Timeout<ZS>,
797 ) -> Result<DateTime32, TransactionError> {
798 let query = state
799 .clone()
800 .oneshot(zs::Request::BestChainNextMedianTimePast);
801
802 if let zebra_state::Response::BestChainNextMedianTimePast(median_time_past) = query
803 .await
804 .map_err(|e| TransactionError::ValidateMempoolLockTimeError(e.to_string()))?
805 {
806 Ok(median_time_past)
807 } else {
808 unreachable!("Request::BestChainNextMedianTimePast always responds with BestChainNextMedianTimePast")
809 }
810 }
811
812 /// Looks up UTXOs spent by `tx` from the best chain state, also checking
813 /// `known_utxos` for UTXOs from earlier transactions in the same block.
814 ///
815 /// Returns an `OutPoint -> Utxo` map and a vec of `Output`s in the same
816 /// order as the matching inputs in `tx`.
817 async fn block_spent_utxos(
818 tx: Arc<Transaction>,
819 known_utxos: Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>>,
820 state: Timeout<ZS>,
821 ) -> Result<
822 (
823 HashMap<transparent::OutPoint, transparent::Utxo>,
824 Vec<transparent::Output>,
825 ),
826 TransactionError,
827 > {
828 let inputs = tx.inputs();
829 let mut spent_utxos = HashMap::new();
830 // Pre-allocate with None so we can fill each slot by input index, preserving input order.
831 let mut spent_outputs: Vec<Option<transparent::Output>> = vec![None; inputs.len()];
832
833 for (input_idx, input) in inputs.iter().enumerate() {
834 if let transparent::Input::PrevOut { outpoint, .. } = input {
835 tracing::trace!("awaiting outpoint lookup");
836
837 let utxo = if let Some(output) = known_utxos.get(outpoint) {
838 tracing::trace!("UXTO in known_utxos, discarding query");
839 output.utxo.clone()
840 } else {
841 let response = state
842 .clone()
843 .oneshot(zebra_state::Request::AwaitUtxo(*outpoint))
844 .await
845 .map_err(|boxed_error| match boxed_error.downcast::<Elapsed>() {
846 Ok(_) => TransactionError::TransparentInputNotFound,
847 Err(boxed_error) => TransactionError::from(boxed_error),
848 })?;
849
850 if let zebra_state::Response::Utxo(utxo) = response {
851 utxo
852 } else {
853 unreachable!("AwaitUtxo always responds with Utxo")
854 }
855 };
856 tracing::trace!(?utxo, "got UTXO");
857 spent_outputs[input_idx] = Some(utxo.output.clone());
858 spent_utxos.insert(*outpoint, utxo);
859 }
860 }
861
862 let spent_outputs: Vec<transparent::Output> = spent_outputs.into_iter().flatten().collect();
863
864 Ok((spent_utxos, spent_outputs))
865 }
866
867 /// Looks up UTXOs spent by a mempool `tx`, first querying the best chain state
868 /// and then the mempool for inputs whose outputs are not present in the best chain.
869 ///
870 /// `height` is the next block height, used to construct `Utxo` values for
871 /// outputs sourced from the mempool.
872 ///
873 /// Returns an `OutPoint -> Utxo` map, a vec of `Output`s in the same order
874 /// as the matching inputs in `tx`, and a vec of `OutPoint`s that were
875 /// sourced from the mempool rather than the best chain.
876 async fn mempool_spent_utxos(
877 tx: Arc<Transaction>,
878 height: block::Height,
879 state: Timeout<ZS>,
880 mempool: Option<Timeout<Mempool>>,
881 ) -> Result<
882 (
883 HashMap<transparent::OutPoint, transparent::Utxo>,
884 Vec<transparent::Output>,
885 Vec<transparent::OutPoint>,
886 ),
887 TransactionError,
888 > {
889 let inputs = tx.inputs();
890 let mut spent_utxos = HashMap::new();
891 // Pre-allocate with None so we can fill each slot by input index, preserving input order
892 // even when chain and mempool UTXOs are fetched in separate passes.
893 let mut spent_outputs: Vec<Option<transparent::Output>> = vec![None; inputs.len()];
894 // Stores (input_idx, outpoint) for UTXOs not found in the best chain (fetched from mempool later).
895 let mut spent_mempool_outpoints: Vec<(usize, transparent::OutPoint)> = Vec::new();
896
897 for (input_idx, input) in inputs.iter().enumerate() {
898 if let transparent::Input::PrevOut { outpoint, .. } = input {
899 tracing::trace!("awaiting outpoint lookup");
900
901 let query = state
902 .clone()
903 .oneshot(zs::Request::UnspentBestChainUtxo(*outpoint));
904
905 let zebra_state::Response::UnspentBestChainUtxo(utxo) = query
906 .await
907 .map_err(|_| TransactionError::TransparentInputNotFound)?
908 else {
909 unreachable!("UnspentBestChainUtxo always responds with Option<Utxo>")
910 };
911
912 let Some(utxo) = utxo else {
913 spent_mempool_outpoints.push((input_idx, *outpoint));
914 continue;
915 };
916
917 tracing::trace!(?utxo, "got UTXO");
918 spent_outputs[input_idx] = Some(utxo.output.clone());
919 spent_utxos.insert(*outpoint, utxo);
920 }
921 }
922
923 if let Some(mempool) = mempool {
924 for &(input_idx, spent_mempool_outpoint) in &spent_mempool_outpoints {
925 let query = mempool
926 .clone()
927 .oneshot(mempool::Request::AwaitOutput(spent_mempool_outpoint));
928
929 let output = match query.await {
930 Ok(mempool::Response::UnspentOutput(output)) => output,
931 Ok(_) => unreachable!("UnspentOutput always responds with UnspentOutput"),
932 Err(err) => {
933 return match err.downcast::<Elapsed>() {
934 Ok(_) => Err(TransactionError::TransparentInputNotFound),
935 Err(err) => Err(err.into()),
936 };
937 }
938 };
939
940 spent_outputs[input_idx] = Some(output.clone());
941 spent_utxos.insert(
942 spent_mempool_outpoint,
943 // Assume the Utxo height will be next height after the best chain tip height
944 //
945 // # Correctness
946 //
947 // If the tip height changes while an umined transaction is being verified,
948 // the transaction must be re-verified before being added to the mempool.
949 transparent::Utxo::new(output, height, false),
950 );
951 }
952 } else if !spent_mempool_outpoints.is_empty() {
953 return Err(TransactionError::TransparentInputNotFound);
954 }
955
956 // Convert back to return types; slots are in input order.
957 let spent_outputs: Vec<transparent::Output> = spent_outputs.into_iter().flatten().collect();
958 let spent_mempool_outpoints: Vec<transparent::OutPoint> = spent_mempool_outpoints
959 .into_iter()
960 .map(|(_, op)| op)
961 .collect();
962
963 Ok((spent_utxos, spent_outputs, spent_mempool_outpoints))
964 }
965
966 /// Checks that every transparent coinbase output spent by `tx` has matured
967 /// by `height`.
968 ///
969 /// This check applies only to mempool transactions. Block transactions are
970 /// checked during contextual validation in the state (see #2336).
971 ///
972 /// Calls [`check::tx_transparent_coinbase_spends_maturity`] with an empty
973 /// `block_new_outputs` map, since mempool transactions have no block context.
974 ///
975 /// Returns `Ok(())` if every transparent coinbase output spent by the transaction is
976 /// mature and valid for the given height, or a [`TransactionError`] if the transaction
977 /// spends transparent coinbase outputs that are immature and invalid for the given height.
978 fn check_maturity_height(
979 tx: Arc<Transaction>,
980 height: block::Height,
981 network: &Network,
982 spent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
983 ) -> Result<(), TransactionError> {
984 check::tx_transparent_coinbase_spends_maturity(
985 network,
986 tx,
987 height,
988 Arc::new(HashMap::new()),
989 spent_utxos,
990 )
991 }
992
993 /// Dispatches version-specific async verification checks for `tx`.
994 ///
995 /// `nu` is the network upgrade active at the transaction's block height,
996 /// pre-computed by the caller from `req.upgrade(&network)`.
997 ///
998 /// Returns [`TransactionError::WrongVersion`] for V1-V3 transactions, which
999 /// are not supported by any network upgrade Zebra verifies.
1000 fn dispatch_version_verification(
1001 tx: &Transaction,
1002 nu: NetworkUpgrade,
1003 script_verifier: script::Verifier,
1004 cached_ffi_transaction: Arc<CachedFfiTransaction>,
1005 ) -> Result<AsyncChecks, TransactionError> {
1006 match tx {
1007 Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {
1008 tracing::debug!(?tx, "got transaction with wrong version");
1009 Err(TransactionError::WrongVersion)
1010 }
1011 Transaction::V4 { joinsplit_data, .. } => Self::verify_v4_transaction(
1012 tx,
1013 nu,
1014 script_verifier,
1015 cached_ffi_transaction,
1016 joinsplit_data,
1017 ),
1018 Transaction::V5 { .. } => {
1019 Self::verify_v5_transaction(tx, nu, script_verifier, cached_ffi_transaction)
1020 }
1021 Transaction::V6 { .. } => {
1022 Self::verify_v6_transaction(tx, nu, script_verifier, cached_ffi_transaction)
1023 }
1024 }
1025 }
1026
1027 /// Verify a V4 transaction.
1028 ///
1029 /// Returns a set of asynchronous checks that must all succeed for the transaction to be
1030 /// considered valid. These checks include:
1031 ///
1032 /// - transparent transfers
1033 /// - sprout shielded data
1034 /// - sapling shielded data
1035 ///
1036 /// The parameters of this method are:
1037 ///
1038 /// - the `tx` transaction to verify
1039 /// - the `nu` network upgrade active at the transaction's block height
1040 /// - the `script_verifier` to use for verifying the transparent transfers
1041 /// - the prepared `cached_ffi_transaction` used by the script verifier
1042 /// - the Sprout `joinsplit_data` shielded data in the transaction
1043 #[allow(clippy::unwrap_in_result)]
1044 fn verify_v4_transaction(
1045 tx: &Transaction,
1046 nu: NetworkUpgrade,
1047 script_verifier: script::Verifier,
1048 cached_ffi_transaction: Arc<CachedFfiTransaction>,
1049 joinsplit_data: &Option<transaction::JoinSplitData<Groth16Proof>>,
1050 ) -> Result<AsyncChecks, TransactionError> {
1051 Self::verify_v4_transaction_network_upgrade(tx, nu)?;
1052
1053 let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle();
1054
1055 let sighash = cached_ffi_transaction
1056 .sighasher()
1057 .sighash(HashType::ALL, None);
1058
1059 Ok(Self::verify_transparent_inputs_and_outputs(
1060 tx,
1061 script_verifier,
1062 cached_ffi_transaction,
1063 )?
1064 .and(Self::verify_sprout_shielded_data(joinsplit_data, &sighash)?)
1065 .and(Self::verify_sapling_bundle(sapling_bundle, &sighash)))
1066 }
1067
1068 /// Verifies if a V4 `transaction` is supported by `network_upgrade`.
1069 fn verify_v4_transaction_network_upgrade(
1070 transaction: &Transaction,
1071 network_upgrade: NetworkUpgrade,
1072 ) -> Result<(), TransactionError> {
1073 match network_upgrade {
1074 // Supports V4 transactions
1075 //
1076 // # Consensus
1077 //
1078 // > [Sapling to Canopy inclusive, pre-NU5] The transaction version number MUST be 4,
1079 // > and the version group ID MUST be 0x892F2085.
1080 //
1081 // > [NU5 onward] The transaction version number MUST be 4 or 5.
1082 // > If the transaction version number is 4 then the version group ID MUST be 0x892F2085.
1083 // > If the transaction version number is 5 then the version group ID MUST be 0x26A7270A.
1084 //
1085 // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1086 //
1087 // Note: Here we verify the transaction version number of the above two rules, the group
1088 // id is checked in zebra-chain crate, in the transaction serialize.
1089 NetworkUpgrade::Sapling
1090 | NetworkUpgrade::Blossom
1091 | NetworkUpgrade::Heartwood
1092 | NetworkUpgrade::Canopy
1093 | NetworkUpgrade::Nu5
1094 | NetworkUpgrade::Nu6
1095 | NetworkUpgrade::Nu6_1
1096 | NetworkUpgrade::Nu6_2
1097 | NetworkUpgrade::Nu6_3 => Ok(()),
1098
1099 #[cfg(zcash_unstable = "zfuture")]
1100 NetworkUpgrade::ZFuture => Ok(()),
1101
1102 // Does not support V4 transactions
1103 NetworkUpgrade::Genesis
1104 | NetworkUpgrade::BeforeOverwinter
1105 | NetworkUpgrade::Overwinter
1106 | NetworkUpgrade::Nu7 => Err(TransactionError::UnsupportedByNetworkUpgrade(
1107 transaction.version(),
1108 network_upgrade,
1109 )),
1110 }
1111 }
1112
1113 /// Verify a V5 transaction.
1114 ///
1115 /// Returns a set of asynchronous checks that must all succeed for the transaction to be
1116 /// considered valid. These checks include:
1117 ///
1118 /// - transaction support by the considered network upgrade (see [`Request::upgrade`])
1119 /// - transparent transfers
1120 /// - sapling shielded data (TODO)
1121 /// - orchard shielded data (TODO)
1122 ///
1123 /// The parameters of this method are:
1124 ///
1125 /// - the `tx` transaction to verify
1126 /// - the `nu` network upgrade active at the transaction's block height
1127 /// - the `script_verifier` to use for verifying the transparent transfers
1128 /// - the prepared `cached_ffi_transaction` used by the script verifier
1129 #[allow(clippy::unwrap_in_result)]
1130 fn verify_v5_transaction(
1131 tx: &Transaction,
1132 nu: NetworkUpgrade,
1133 script_verifier: script::Verifier,
1134 cached_ffi_transaction: Arc<CachedFfiTransaction>,
1135 ) -> Result<AsyncChecks, TransactionError> {
1136 Self::verify_v5_transaction_network_upgrade(tx, nu)?;
1137
1138 let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle();
1139 let orchard_bundle = cached_ffi_transaction.sighasher().orchard_bundle();
1140
1141 let sighash = cached_ffi_transaction
1142 .sighasher()
1143 .sighash(HashType::ALL, None);
1144
1145 Ok(Self::verify_transparent_inputs_and_outputs(
1146 tx,
1147 script_verifier,
1148 cached_ffi_transaction,
1149 )?
1150 .and(Self::verify_sapling_bundle(sapling_bundle, &sighash))
1151 .and(Self::verify_orchard_bundle(orchard_bundle, &sighash, nu)))
1152 }
1153
1154 /// Verifies if a V5 `transaction` is supported by `network_upgrade`.
1155 fn verify_v5_transaction_network_upgrade(
1156 transaction: &Transaction,
1157 network_upgrade: NetworkUpgrade,
1158 ) -> Result<(), TransactionError> {
1159 match network_upgrade {
1160 // Supports V5 transactions
1161 //
1162 // # Consensus
1163 //
1164 // > [NU5 onward] The transaction version number MUST be 4 or 5.
1165 // > If the transaction version number is 4 then the version group ID MUST be 0x892F2085.
1166 // > If the transaction version number is 5 then the version group ID MUST be 0x26A7270A.
1167 //
1168 // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1169 //
1170 // Note: Here we verify the transaction version number of the above rule, the group
1171 // id is checked in zebra-chain crate, in the transaction serialize.
1172 NetworkUpgrade::Nu5
1173 | NetworkUpgrade::Nu6
1174 | NetworkUpgrade::Nu6_1
1175 | NetworkUpgrade::Nu6_2
1176 | NetworkUpgrade::Nu6_3
1177 | NetworkUpgrade::Nu7 => Ok(()),
1178
1179 #[cfg(zcash_unstable = "zfuture")]
1180 NetworkUpgrade::ZFuture => Ok(()),
1181
1182 // Does not support V5 transactions
1183 NetworkUpgrade::Genesis
1184 | NetworkUpgrade::BeforeOverwinter
1185 | NetworkUpgrade::Overwinter
1186 | NetworkUpgrade::Sapling
1187 | NetworkUpgrade::Blossom
1188 | NetworkUpgrade::Heartwood
1189 | NetworkUpgrade::Canopy => Err(TransactionError::UnsupportedByNetworkUpgrade(
1190 transaction.version(),
1191 network_upgrade,
1192 )),
1193 }
1194 }
1195
1196 /// Verifies a V6 (NU6.3 / Ironwood) transaction's shielded data.
1197 ///
1198 /// Differs from [`Self::verify_v5_transaction`] in the Orchard verifier: a v6 Orchard bundle
1199 /// commits to the NU6.3 cross-address circuit, so it (and the Ironwood bundle) verify under the
1200 /// NU6.3 key, not the v5 fixed key.
1201 fn verify_v6_transaction(
1202 tx: &Transaction,
1203 nu: NetworkUpgrade,
1204 script_verifier: script::Verifier,
1205 cached_ffi_transaction: Arc<CachedFfiTransaction>,
1206 ) -> Result<AsyncChecks, TransactionError> {
1207 Self::verify_v6_transaction_network_upgrade(tx, nu)?;
1208
1209 let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle();
1210 let orchard_bundle = cached_ffi_transaction.sighasher().orchard_bundle();
1211 let ironwood_bundle = cached_ffi_transaction.sighasher().ironwood_bundle();
1212
1213 let sighash = cached_ffi_transaction
1214 .sighasher()
1215 .sighash(HashType::ALL, None);
1216
1217 // The Ironwood bundle reuses the Orchard Action proof system and the NU6.3 circuit key, so
1218 // it is verified the same way as the v6 Orchard bundle (against the NU6.3 key).
1219 Ok(Self::verify_transparent_inputs_and_outputs(
1220 tx,
1221 script_verifier,
1222 cached_ffi_transaction,
1223 )?
1224 .and(Self::verify_sapling_bundle(sapling_bundle, &sighash))
1225 .and(Self::verify_orchard_v6_bundle(orchard_bundle, &sighash))
1226 .and(Self::verify_orchard_v6_bundle(ironwood_bundle, &sighash)))
1227 }
1228
1229 /// Verifies that a V6 `transaction` is supported by `network_upgrade`.
1230 ///
1231 /// V6 transactions are only valid from NU6.3 onward.
1232 fn verify_v6_transaction_network_upgrade(
1233 transaction: &Transaction,
1234 network_upgrade: NetworkUpgrade,
1235 ) -> Result<(), TransactionError> {
1236 match network_upgrade {
1237 NetworkUpgrade::Nu6_3 | NetworkUpgrade::Nu7 => Ok(()),
1238
1239 #[cfg(zcash_unstable = "zfuture")]
1240 NetworkUpgrade::ZFuture => Ok(()),
1241
1242 // V6 transactions are not valid before NU6.3.
1243 NetworkUpgrade::Genesis
1244 | NetworkUpgrade::BeforeOverwinter
1245 | NetworkUpgrade::Overwinter
1246 | NetworkUpgrade::Sapling
1247 | NetworkUpgrade::Blossom
1248 | NetworkUpgrade::Heartwood
1249 | NetworkUpgrade::Canopy
1250 | NetworkUpgrade::Nu5
1251 | NetworkUpgrade::Nu6
1252 | NetworkUpgrade::Nu6_1
1253 | NetworkUpgrade::Nu6_2 => Err(TransactionError::UnsupportedByNetworkUpgrade(
1254 transaction.version(),
1255 network_upgrade,
1256 )),
1257 }
1258 }
1259
1260 /// Verifies if a transaction's transparent inputs are valid using the provided
1261 /// `script_verifier` and `cached_ffi_transaction`.
1262 ///
1263 /// Returns script verification responses via the `utxo_sender`.
1264 fn verify_transparent_inputs_and_outputs(
1265 tx: &Transaction,
1266 script_verifier: script::Verifier,
1267 cached_ffi_transaction: Arc<CachedFfiTransaction>,
1268 ) -> Result<AsyncChecks, TransactionError> {
1269 if tx.is_coinbase() {
1270 // The script verifier only verifies PrevOut inputs and their corresponding UTXOs.
1271 // Coinbase transactions don't have any PrevOut inputs.
1272 Ok(AsyncChecks::new())
1273 } else {
1274 // feed all of the inputs to the script verifier
1275 let inputs = tx.inputs();
1276
1277 let script_checks = (0..inputs.len())
1278 .map(move |input_index| {
1279 let request = script::Request {
1280 cached_ffi_transaction: cached_ffi_transaction.clone(),
1281 input_index,
1282 };
1283
1284 script_verifier.oneshot(request)
1285 })
1286 .collect();
1287
1288 Ok(script_checks)
1289 }
1290 }
1291
1292 /// Verifies a transaction's Sprout shielded join split data.
1293 fn verify_sprout_shielded_data(
1294 joinsplit_data: &Option<transaction::JoinSplitData<Groth16Proof>>,
1295 shielded_sighash: &SigHash,
1296 ) -> Result<AsyncChecks, TransactionError> {
1297 let mut checks = AsyncChecks::new();
1298
1299 if let Some(joinsplit_data) = joinsplit_data {
1300 for joinsplit in joinsplit_data.joinsplits() {
1301 // # Consensus
1302 //
1303 // > The proof π_ZKJoinSplit MUST be valid given a
1304 // > primary input formed from the relevant other fields and h_{Sig}
1305 //
1306 // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
1307 //
1308 // Queue the verification of the Groth16 spend proof
1309 // for each JoinSplit description while adding the
1310 // resulting future to our collection of async
1311 // checks that (at a minimum) must pass for the
1312 // transaction to verify.
1313 checks.push(primitives::groth16::JOINSPLIT_VERIFIER.oneshot(
1314 primitives::groth16::Item::from_joinsplit(joinsplit, &joinsplit_data.pub_key)?,
1315 ));
1316 }
1317
1318 // # Consensus
1319 //
1320 // > If effectiveVersion ≥ 2 and nJoinSplit > 0, then:
1321 // > - joinSplitPubKey MUST be a valid encoding of an Ed25519 validating key
1322 // > - joinSplitSig MUST represent a valid signature under
1323 // joinSplitPubKey of dataToBeSigned, as defined in § 4.11
1324 //
1325 // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1326 //
1327 // The `if` part is indirectly enforced, since the `joinsplit_data`
1328 // is only parsed if those conditions apply in
1329 // [`Transaction::zcash_deserialize`].
1330 //
1331 // The valid encoding is defined in
1332 //
1333 // > A valid Ed25519 validating key is defined as a sequence of 32
1334 // > bytes encoding a point on the Ed25519 curve
1335 //
1336 // https://zips.z.cash/protocol/protocol.pdf#concreteed25519
1337 //
1338 // which is enforced during signature verification, in both batched
1339 // and single verification, when decompressing the encoded point.
1340 //
1341 // Queue the validation of the JoinSplit signature while
1342 // adding the resulting future to our collection of
1343 // async checks that (at a minimum) must pass for the
1344 // transaction to verify.
1345 //
1346 // https://zips.z.cash/protocol/protocol.pdf#sproutnonmalleability
1347 // https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
1348 let ed25519_verifier = primitives::ed25519::VERIFIER.clone();
1349 let ed25519_item =
1350 (joinsplit_data.pub_key, joinsplit_data.sig, shielded_sighash).into();
1351
1352 checks.push(ed25519_verifier.oneshot(ed25519_item));
1353 }
1354
1355 Ok(checks)
1356 }
1357
1358 /// Verifies a transaction's Sapling shielded data.
1359 fn verify_sapling_bundle(
1360 bundle: Option<sapling_crypto::Bundle<sapling_crypto::bundle::Authorized, ZatBalance>>,
1361 sighash: &SigHash,
1362 ) -> AsyncChecks {
1363 let mut async_checks = AsyncChecks::new();
1364
1365 // The Sapling batch verifier checks the following consensus rules:
1366 //
1367 // # Consensus
1368 //
1369 // > The proof π_ZKSpend MUST be valid given a primary input formed from the other fields
1370 // > except spendAuthSig.
1371 //
1372 // > The spend authorization signature MUST be a valid SpendAuthSig signature over SigHash
1373 // > using rk as the validating key.
1374 //
1375 // > [NU5 onward] As specified in § 5.4.7 ‘RedDSA, RedJubjub, and RedPallas’ on p. 88, the
1376 // > validation of the 𝑅 component of the signature changes to prohibit non-canonical
1377 // > encodings.
1378 //
1379 // https://zips.z.cash/protocol/protocol.pdf#spenddesc
1380 //
1381 // # Consensus
1382 //
1383 // > The proof π_ZKOutput MUST be valid given a primary input formed from the other fields
1384 // > except C^enc and C^out.
1385 //
1386 // https://zips.z.cash/protocol/protocol.pdf#outputdesc
1387 //
1388 // # Consensus
1389 //
1390 // > The Spend transfers and Action transfers of a transaction MUST be consistent with its
1391 // > vbalanceSapling value as specified in § 4.13 ‘Balance and Binding Signature (Sapling)’.
1392 //
1393 // https://zips.z.cash/protocol/protocol.pdf#spendsandoutputs
1394 //
1395 // # Consensus
1396 //
1397 // > [Sapling onward] If effectiveVersion ≥ 4 and nSpendsSapling + nOutputsSapling > 0,
1398 // > then:
1399 // >
1400 // > – let bvk^{Sapling} and SigHash be as defined in § 4.13;
1401 // > – bindingSigSapling MUST represent a valid signature under the transaction binding
1402 // > validating key bvk Sapling of SigHash — i.e.
1403 // > BindingSig^{Sapling}.Validate_{bvk^{Sapling}}(SigHash, bindingSigSapling ) = 1.
1404 //
1405 // Note that the `if` part is indirectly enforced, since the `sapling_shielded_data` is only
1406 // parsed if those conditions apply in [`Transaction::zcash_deserialize`].
1407 //
1408 // > [NU5 onward] As specified in § 5.4.7, the validation of the 𝑅 component of the
1409 // > signature changes to prohibit non-canonical encodings.
1410 //
1411 // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
1412 if let Some(bundle) = bundle {
1413 async_checks.push(
1414 primitives::sapling::VERIFIER
1415 .clone()
1416 .oneshot(primitives::sapling::Item::new(bundle, *sighash)),
1417 );
1418 }
1419
1420 async_checks
1421 }
1422
1423 /// Verifies a **v5** transaction's Orchard bundle.
1424 ///
1425 /// A v5 Orchard bundle commits to the Orchard Action circuit of the block's era, so the
1426 /// verifying key is selected by `network_upgrade` via
1427 /// [`primitives::halo2::orchard_v5_verifier_for`]: the historical insecure key before NU6.2, the
1428 /// fixed key from NU6.2 until NU6.3, and the NU6.3 key from NU6.3 onward. The Orchard-pool
1429 /// cross-address restriction applies to every Orchard Action from NU6.3 onward regardless of
1430 /// transaction version (ZIP 229), so a v5 bundle at NU6.3 uses the NU6.3 circuit — the same key
1431 /// as v6 Orchard and Ironwood bundles — not the fixed one.
1432 fn verify_orchard_bundle(
1433 bundle: Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1434 sighash: &SigHash,
1435 network_upgrade: NetworkUpgrade,
1436 ) -> AsyncChecks {
1437 Self::queue_orchard_bundle(
1438 || primitives::halo2::orchard_v5_verifier_for(network_upgrade),
1439 bundle,
1440 sighash,
1441 )
1442 }
1443
1444 /// Verifies a **v6** transaction's Orchard bundle.
1445 ///
1446 /// A v6 Orchard bundle commits to the NU6.3 cross-address circuit, so it always verifies under
1447 /// the NU6.3 key ([`primitives::halo2::orchard_v6_verifier`]), independent of the block's
1448 /// network upgrade (v6 transactions only exist from NU6.3 onward). The Ironwood bundle reuses
1449 /// the same verifier.
1450 fn verify_orchard_v6_bundle(
1451 bundle: Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1452 sighash: &SigHash,
1453 ) -> AsyncChecks {
1454 Self::queue_orchard_bundle(primitives::halo2::orchard_v6_verifier, bundle, sighash)
1455 }
1456
1457 /// Queues an Orchard-shaped bundle's single aggregated Halo2 proof against a verifier.
1458 ///
1459 /// # Consensus
1460 ///
1461 /// > The proof 𝜋 MUST be valid given a primary input (cv, rt^{Orchard}, nf, rk, cm_x,
1462 /// > enableSpends, enableOutputs)
1463 ///
1464 /// <https://zips.z.cash/protocol/protocol.pdf#actiondesc>
1465 ///
1466 /// Unlike Sapling, Orchard shielded transactions have a single aggregated Halo2 proof per
1467 /// transaction, even with multiple Actions, so it is queued for verification only once instead
1468 /// of once per Action description. The choice of verifying key is the caller's; see
1469 /// [`Self::verify_orchard_bundle`] and [`Self::verify_orchard_v6_bundle`].
1470 ///
1471 /// `select_verifier` is only invoked when a bundle is present, so a bundle-less transaction
1472 /// never forces the (lazily initialized) verifier services.
1473 fn queue_orchard_bundle(
1474 select_verifier: impl FnOnce() -> &'static primitives::halo2::VerifierService,
1475 bundle: Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1476 sighash: &SigHash,
1477 ) -> AsyncChecks {
1478 let mut async_checks = AsyncChecks::new();
1479
1480 if let Some(bundle) = bundle {
1481 async_checks.push(
1482 select_verifier()
1483 .clone()
1484 .oneshot(primitives::halo2::Item::new(bundle, *sighash)),
1485 );
1486 }
1487
1488 async_checks
1489 }
1490
1491 /// Calculate the miner fee from the transaction's value balance.
1492 fn miner_fee(
1493 tx: &Transaction,
1494 spent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
1495 ) -> Result<Amount<NonNegative>, TransactionError> {
1496 match tx.value_balance(spent_utxos) {
1497 Ok(value_balance) => value_balance
1498 .remaining_transaction_value()
1499 .map_err(|_| TransactionError::IncorrectFee),
1500 Err(_) => Err(TransactionError::IncorrectFee),
1501 }
1502 }
1503}
1504
1505/// A set of unordered asynchronous checks that should succeed.
1506///
1507/// A wrapper around [`FuturesUnordered`] with some auxiliary methods.
1508struct AsyncChecks(FuturesUnordered<Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>>);
1509
1510impl AsyncChecks {
1511 /// Create an empty set of unordered asynchronous checks.
1512 pub fn new() -> Self {
1513 AsyncChecks(FuturesUnordered::new())
1514 }
1515
1516 /// Push a check into the set.
1517 pub fn push(&mut self, check: impl Future<Output = Result<(), BoxError>> + Send + 'static) {
1518 self.0.push(check.boxed());
1519 }
1520
1521 /// Push a set of checks into the set.
1522 ///
1523 /// This method can be daisy-chained.
1524 pub fn and(mut self, checks: AsyncChecks) -> Self {
1525 self.0.extend(checks.0);
1526 self
1527 }
1528
1529 /// Wait until all checks in the set finish.
1530 ///
1531 /// If any of the checks fail, this method immediately returns the error and cancels all other
1532 /// checks by dropping them.
1533 async fn check(mut self) -> Result<(), BoxError> {
1534 // Wait for all asynchronous checks to complete
1535 // successfully, or fail verification if they error.
1536 while let Some(check) = self.0.next().await {
1537 tracing::trace!(?check, remaining = self.0.len());
1538 check?;
1539 }
1540
1541 Ok(())
1542 }
1543}
1544
1545impl<F> FromIterator<F> for AsyncChecks
1546where
1547 F: Future<Output = Result<(), BoxError>> + Send + 'static,
1548{
1549 fn from_iter<I>(iterator: I) -> Self
1550 where
1551 I: IntoIterator<Item = F>,
1552 {
1553 AsyncChecks(iterator.into_iter().map(FutureExt::boxed).collect())
1554 }
1555}