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