zebra_consensus/transaction/check.rs
1//! Transaction checks.
2//!
3//! Code in this file can freely assume that no pre-V4 transactions are present.
4
5use std::{
6 borrow::Cow,
7 collections::{HashMap, HashSet},
8 hash::Hash,
9 sync::Arc,
10};
11
12use chrono::{DateTime, Utc};
13
14use zebra_chain::{
15 amount::{Amount, NonNegative},
16 block::Height,
17 orchard::Flags,
18 parameters::{Network, NetworkUpgrade},
19 primitives::zcash_note_encryption,
20 transaction::{LockTime, Transaction},
21 transparent,
22};
23
24use crate::error::TransactionError;
25
26/// Checks if the transaction's lock time allows this transaction to be included in a block.
27///
28/// Arguments:
29/// - `block_height`: the height of the mined block, or the height of the next block for mempool
30/// transactions
31/// - `block_time`: the time in the mined block header, or the median-time-past of the next block
32/// for the mempool. Optional if the lock time is a height.
33///
34/// # Panics
35///
36/// If the lock time is a time, and `block_time` is `None`.
37///
38/// # Consensus
39///
40/// > The transaction must be finalized: either its locktime must be in the past (or less
41/// > than or equal to the current block height), or all of its sequence numbers must be
42/// > 0xffffffff.
43///
44/// [`Transaction::lock_time`] validates the transparent input sequence numbers, returning [`None`]
45/// if they indicate that the transaction is finalized by them.
46/// Otherwise, this function checks that the lock time is in the past.
47///
48/// ## Mempool Consensus for Block Templates
49///
50/// > the nTime field MUST represent a time strictly greater than the median of the
51/// > timestamps of the past PoWMedianBlockSpan blocks.
52///
53/// <https://zips.z.cash/protocol/protocol.pdf#blockheader>
54///
55/// > The transaction can be added to any block whose block time is greater than the locktime.
56///
57/// <https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number>
58///
59/// If the transaction's lock time is less than the median-time-past,
60/// it will always be less than the next block's time,
61/// because the next block's time is strictly greater than the median-time-past.
62/// (That is, `lock-time < median-time-past < block-header-time`.)
63///
64/// Using `median-time-past + 1s` (the next block's mintime) would also satisfy this consensus rule,
65/// but we prefer the rule implemented by `zcashd`'s mempool:
66/// <https://github.com/zcash/zcash/blob/9e1efad2d13dca5ee094a38e6aa25b0f2464da94/src/main.cpp#L776-L784>
67pub fn lock_time_has_passed(
68 tx: &Transaction,
69 block_height: Height,
70 block_time: impl Into<Option<DateTime<Utc>>>,
71) -> Result<(), TransactionError> {
72 match tx.lock_time() {
73 Some(LockTime::Height(unlock_height)) => {
74 // > The transaction can be added to any block which has a greater height.
75 // The Bitcoin documentation is wrong or outdated here,
76 // so this code is based on the `zcashd` implementation at:
77 // https://github.com/zcash/zcash/blob/1a7c2a3b04bcad6549be6d571bfdff8af9a2c814/src/main.cpp#L722
78 if block_height > unlock_height {
79 Ok(())
80 } else {
81 Err(TransactionError::LockedUntilAfterBlockHeight(unlock_height))
82 }
83 }
84 Some(LockTime::Time(unlock_time)) => {
85 // > The transaction can be added to any block whose block time is greater than the locktime.
86 // https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number
87 let block_time = block_time
88 .into()
89 .expect("time must be provided if LockTime is a time");
90
91 if block_time > unlock_time {
92 Ok(())
93 } else {
94 Err(TransactionError::LockedUntilAfterBlockTime(unlock_time))
95 }
96 }
97 None => Ok(()),
98 }
99}
100
101/// Checks that the transaction has inputs and outputs.
102///
103/// # Consensus
104///
105/// For `Transaction::V4`:
106///
107/// > [Sapling onward] If effectiveVersion < 5, then at least one of
108/// > tx_in_count, nSpendsSapling, and nJoinSplit MUST be nonzero.
109///
110/// > [Sapling onward] If effectiveVersion < 5, then at least one of
111/// > tx_out_count, nOutputsSapling, and nJoinSplit MUST be nonzero.
112///
113/// For `Transaction::V5`:
114///
115/// > [NU5 onward] If effectiveVersion >= 5 then this condition MUST hold:
116/// > tx_in_count > 0 or nSpendsSapling > 0 or (nActionsOrchard > 0 and enableSpendsOrchard = 1).
117///
118/// > [NU5 onward] If effectiveVersion >= 5 then this condition MUST hold:
119/// > tx_out_count > 0 or nOutputsSapling > 0 or (nActionsOrchard > 0 and enableOutputsOrchard = 1).
120///
121/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
122///
123/// This check counts both `Coinbase` and `PrevOut` transparent inputs.
124pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> {
125 #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
126 let has_other_circulation_effects = tx.has_zip233_amount();
127
128 #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))]
129 let has_other_circulation_effects = false;
130
131 if !tx.has_transparent_or_shielded_inputs() {
132 Err(TransactionError::NoInputs)
133 } else if !tx.has_transparent_or_shielded_outputs() && !has_other_circulation_effects {
134 Err(TransactionError::NoOutputs)
135 } else {
136 Ok(())
137 }
138}
139
140/// Checks that the transaction has enough orchard flags.
141///
142/// # Consensus
143///
144/// For `Transaction::V5` only:
145///
146/// > [NU5 onward] If effectiveVersion >= 5 and nActionsOrchard > 0, then at least one of enableSpendsOrchard and enableOutputsOrchard MUST be 1.
147///
148/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
149pub fn has_enough_orchard_flags(tx: &Transaction) -> Result<(), TransactionError> {
150 if !tx.has_enough_orchard_flags() {
151 return Err(TransactionError::NotEnoughFlags);
152 }
153 Ok(())
154}
155
156/// Check that a coinbase transaction has no PrevOut inputs, JoinSplits, or spends.
157///
158/// # Consensus
159///
160/// > A coinbase transaction MUST NOT have any JoinSplit descriptions.
161///
162/// > A coinbase transaction MUST NOT have any Spend descriptions.
163///
164/// > [NU5 onward] In a version 5 coinbase transaction, the enableSpendsOrchard flag MUST be 0.
165///
166/// This check only counts `PrevOut` transparent inputs.
167///
168/// > [Pre-Heartwood] A coinbase transaction also MUST NOT have any Output descriptions.
169///
170/// Zebra does not validate this last rule explicitly because we checkpoint until Canopy activation.
171///
172/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
173pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), TransactionError> {
174 if tx.is_coinbase() {
175 if tx.joinsplit_count() > 0 {
176 return Err(TransactionError::CoinbaseHasJoinSplit);
177 } else if tx.sapling_spends_per_anchor().count() > 0 {
178 return Err(TransactionError::CoinbaseHasSpend);
179 }
180
181 if let Some(orchard_shielded_data) = tx.orchard_shielded_data() {
182 if orchard_shielded_data.flags.contains(Flags::ENABLE_SPENDS) {
183 return Err(TransactionError::CoinbaseHasEnableSpendsOrchard);
184 }
185 }
186 }
187
188 Ok(())
189}
190
191/// Check if JoinSplits in the transaction have one of its v_{pub} values equal
192/// to zero.
193///
194/// <https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc>
195pub fn joinsplit_has_vpub_zero(tx: &Transaction) -> Result<(), TransactionError> {
196 let zero = Amount::<NonNegative>::zero();
197
198 let vpub_pairs = tx
199 .output_values_to_sprout()
200 .zip(tx.input_values_from_sprout());
201 for (vpub_old, vpub_new) in vpub_pairs {
202 // # Consensus
203 //
204 // > Either v_{pub}^{old} or v_{pub}^{new} MUST be zero.
205 //
206 // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
207 if *vpub_old != zero && *vpub_new != zero {
208 return Err(TransactionError::BothVPubsNonZero);
209 }
210 }
211
212 Ok(())
213}
214
215/// Check if a transaction is adding to the sprout pool after Canopy
216/// network upgrade given a block height and a network.
217///
218/// <https://zips.z.cash/zip-0211>
219/// <https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc>
220pub fn disabled_add_to_sprout_pool(
221 tx: &Transaction,
222 height: Height,
223 network: &Network,
224) -> Result<(), TransactionError> {
225 let canopy_activation_height = NetworkUpgrade::Canopy
226 .activation_height(network)
227 .expect("Canopy activation height must be present for both networks");
228
229 // # Consensus
230 //
231 // > [Canopy onward]: `vpub_old` MUST be zero.
232 //
233 // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
234 if height >= canopy_activation_height {
235 let zero = Amount::<NonNegative>::zero();
236
237 let tx_sprout_pool = tx.output_values_to_sprout();
238 for vpub_old in tx_sprout_pool {
239 if *vpub_old != zero {
240 return Err(TransactionError::DisabledAddToSproutPool);
241 }
242 }
243 }
244
245 Ok(())
246}
247
248/// Check if a transaction has any internal spend conflicts.
249///
250/// An internal spend conflict happens if the transaction spends a UTXO more than once or if it
251/// reveals a nullifier more than once.
252///
253/// Consensus rules:
254///
255/// "each output of a particular transaction
256/// can only be used as an input once in the block chain.
257/// Any subsequent reference is a forbidden double spend-
258/// an attempt to spend the same satoshis twice."
259///
260/// <https://developer.bitcoin.org/devguide/block_chain.html#introduction>
261///
262/// A _nullifier_ *MUST NOT* repeat either within a _transaction_, or across _transactions_ in a
263/// _valid blockchain_ . *Sprout* and *Sapling* and *Orchard* _nulliers_ are considered disjoint,
264/// even if they have the same bit pattern.
265///
266/// <https://zips.z.cash/protocol/protocol.pdf#nullifierset>
267pub fn spend_conflicts(transaction: &Transaction) -> Result<(), TransactionError> {
268 use crate::error::TransactionError::*;
269
270 let transparent_outpoints = transaction.spent_outpoints().map(Cow::Owned);
271 let sprout_nullifiers = transaction.sprout_nullifiers().map(Cow::Borrowed);
272 let sapling_nullifiers = transaction.sapling_nullifiers().map(Cow::Borrowed);
273 let orchard_nullifiers = transaction.orchard_nullifiers().map(Cow::Borrowed);
274
275 check_for_duplicates(transparent_outpoints, DuplicateTransparentSpend)?;
276 check_for_duplicates(sprout_nullifiers, DuplicateSproutNullifier)?;
277 check_for_duplicates(sapling_nullifiers, DuplicateSaplingNullifier)?;
278 check_for_duplicates(orchard_nullifiers, DuplicateOrchardNullifier)?;
279
280 Ok(())
281}
282
283/// Check for duplicate items in a collection.
284///
285/// Each item should be wrapped by a [`Cow`] instance so that this helper function can properly
286/// handle borrowed items and owned items.
287///
288/// If a duplicate is found, an error created by the `error_wrapper` is returned.
289fn check_for_duplicates<'t, T>(
290 items: impl IntoIterator<Item = Cow<'t, T>>,
291 error_wrapper: impl FnOnce(T) -> TransactionError,
292) -> Result<(), TransactionError>
293where
294 T: Clone + Eq + Hash + 't,
295{
296 let mut hash_set = HashSet::new();
297
298 for item in items {
299 if let Some(duplicate) = hash_set.replace(item) {
300 return Err(error_wrapper(duplicate.into_owned()));
301 }
302 }
303
304 Ok(())
305}
306
307/// Checks compatibility with [ZIP-212] shielded Sapling and Orchard coinbase output decryption
308///
309/// Pre-Heartwood: returns `Ok`.
310/// Heartwood-onward: returns `Ok` if all Sapling or Orchard outputs, if any, decrypt successfully with
311/// an all-zeroes outgoing viewing key. Returns `Err` otherwise.
312///
313/// This is used to validate coinbase transactions:
314///
315/// # Consensus
316///
317/// > [Heartwood onward] All Sapling and Orchard outputs in coinbase transactions MUST decrypt to a note
318/// > plaintext, i.e. the procedure in § 4.20.3 ‘Decryption using a Full Viewing Key (Sapling and Orchard)’
319/// > does not return ⊥, using a sequence of 32 zero bytes as the outgoing viewing key. (This implies that before
320/// > Canopy activation, Sapling outputs of a coinbase transaction MUST have note plaintext lead byte equal to
321/// > 0x01.)
322///
323/// > [Canopy onward] Any Sapling or Orchard output of a coinbase transaction decrypted to a note plaintext
324/// > according to the preceding rule MUST have note plaintext lead byte equal to 0x02. (This applies even during
325/// > the "grace period" specified in [ZIP-212].)
326///
327/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
328///
329/// [ZIP-212]: https://zips.z.cash/zip-0212#consensus-rule-change-for-coinbase-transactions
330///
331/// TODO: Currently, a 0x01 lead byte is allowed in the "grace period" mentioned since we're
332/// using `librustzcash` to implement this and it doesn't currently allow changing that behavior.
333/// <https://github.com/ZcashFoundation/zebra/issues/3027>
334pub fn coinbase_outputs_are_decryptable(
335 transaction: &Transaction,
336 network: &Network,
337 height: Height,
338) -> Result<(), TransactionError> {
339 // Do quick checks first so we can avoid an expensive tx conversion
340 // in `zcash_note_encryption::decrypts_successfully`.
341
342 // The consensus rule only applies to coinbase txs with shielded outputs.
343 if !transaction.has_shielded_outputs() {
344 return Ok(());
345 }
346
347 // The consensus rule only applies to Heartwood onward.
348 if height
349 < NetworkUpgrade::Heartwood
350 .activation_height(network)
351 .expect("Heartwood height is known")
352 {
353 return Ok(());
354 }
355
356 // The passed tx should have been be a coinbase tx.
357 if !transaction.is_coinbase() {
358 return Err(TransactionError::NotCoinbase);
359 }
360
361 if !zcash_note_encryption::decrypts_successfully(transaction, network, height) {
362 return Err(TransactionError::CoinbaseOutputsNotDecryptable);
363 }
364
365 Ok(())
366}
367
368/// Returns `Ok(())` if the expiry height for the coinbase transaction is valid
369/// according to specifications [7.1] and [ZIP-203].
370///
371/// [7.1]: https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
372/// [ZIP-203]: https://zips.z.cash/zip-0203
373pub fn coinbase_expiry_height(
374 block_height: &Height,
375 coinbase: &Transaction,
376 network: &Network,
377) -> Result<(), TransactionError> {
378 let expiry_height = coinbase.expiry_height();
379
380 if let Some(nu5_activation_height) = NetworkUpgrade::Nu5.activation_height(network) {
381 // # Consensus
382 //
383 // > [NU5 onward] The nExpiryHeight field of a coinbase transaction
384 // > MUST be equal to its block height.
385 //
386 // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
387 if *block_height >= nu5_activation_height {
388 if expiry_height != Some(*block_height) {
389 return Err(TransactionError::CoinbaseExpiryBlockHeight {
390 expiry_height,
391 block_height: *block_height,
392 transaction_hash: coinbase.hash(),
393 });
394 } else {
395 return Ok(());
396 }
397 }
398 }
399
400 // # Consensus
401 //
402 // > [Overwinter to Canopy inclusive, pre-NU5] nExpiryHeight MUST be less than
403 // > or equal to 499999999.
404 //
405 // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
406 validate_expiry_height_max(expiry_height, true, block_height, coinbase)
407}
408
409/// Returns `Ok(())` if the expiry height for a non coinbase transaction is
410/// valid according to specifications [7.1] and [ZIP-203].
411///
412/// [7.1]: https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
413/// [ZIP-203]: https://zips.z.cash/zip-0203
414pub fn non_coinbase_expiry_height(
415 block_height: &Height,
416 transaction: &Transaction,
417) -> Result<(), TransactionError> {
418 if transaction.is_overwintered() {
419 let expiry_height = transaction.expiry_height();
420
421 // # Consensus
422 //
423 // > [Overwinter to Canopy inclusive, pre-NU5] nExpiryHeight MUST be
424 // > less than or equal to 499999999.
425 //
426 // > [NU5 onward] nExpiryHeight MUST be less than or equal to 499999999
427 // > for non-coinbase transactions.
428 //
429 // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
430 validate_expiry_height_max(expiry_height, false, block_height, transaction)?;
431
432 // # Consensus
433 //
434 // > [Overwinter onward] If a transaction is not a coinbase transaction and its
435 // > nExpiryHeight field is nonzero, then it MUST NOT be mined at a block
436 // > height greater than its nExpiryHeight.
437 //
438 // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
439 validate_expiry_height_mined(expiry_height, block_height, transaction)?;
440 }
441 Ok(())
442}
443
444/// Checks that the expiry height of a transaction does not exceed the maximal
445/// value.
446///
447/// Only the `expiry_height` parameter is used for the check. The
448/// remaining parameters are used to give details about the error when the check
449/// fails.
450fn validate_expiry_height_max(
451 expiry_height: Option<Height>,
452 is_coinbase: bool,
453 block_height: &Height,
454 transaction: &Transaction,
455) -> Result<(), TransactionError> {
456 if let Some(expiry_height) = expiry_height {
457 if expiry_height > Height::MAX_EXPIRY_HEIGHT {
458 Err(TransactionError::MaximumExpiryHeight {
459 expiry_height,
460 is_coinbase,
461 block_height: *block_height,
462 transaction_hash: transaction.hash(),
463 })?;
464 }
465 }
466
467 Ok(())
468}
469
470/// Checks that a transaction does not exceed its expiry height.
471///
472/// The `transaction` parameter is only used to give details about the error
473/// when the check fails.
474fn validate_expiry_height_mined(
475 expiry_height: Option<Height>,
476 block_height: &Height,
477 transaction: &Transaction,
478) -> Result<(), TransactionError> {
479 if let Some(expiry_height) = expiry_height {
480 if *block_height > expiry_height {
481 Err(TransactionError::ExpiredTransaction {
482 expiry_height,
483 block_height: *block_height,
484 transaction_hash: transaction.hash(),
485 })?;
486 }
487 }
488
489 Ok(())
490}
491
492/// Accepts a transaction, block height, block UTXOs, and
493/// the transaction's spent UTXOs from the chain.
494///
495/// Returns `Ok(())` if spent transparent coinbase outputs are
496/// valid for the block height, or a [`Err(TransactionError)`](TransactionError)
497pub fn tx_transparent_coinbase_spends_maturity(
498 network: &Network,
499 tx: Arc<Transaction>,
500 height: Height,
501 block_new_outputs: Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>>,
502 spent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
503) -> Result<(), TransactionError> {
504 for spend in tx.spent_outpoints() {
505 let utxo = block_new_outputs
506 .get(&spend)
507 .map(|ordered_utxo| ordered_utxo.utxo.clone())
508 .or_else(|| spent_utxos.get(&spend).cloned())
509 .expect("load_spent_utxos_fut.await should return an error if a utxo is missing");
510
511 let spend_restriction = tx.coinbase_spend_restriction(network, height);
512
513 zebra_state::check::transparent_coinbase_spend(spend, spend_restriction, &utxo)?;
514 }
515
516 Ok(())
517}
518
519/// Checks the `nConsensusBranchId` field.
520///
521/// # Consensus
522///
523/// ## [7.1.2 Transaction Consensus Rules]
524///
525/// > [**NU5** onward] If `effectiveVersion` ≥ 5, the `nConsensusBranchId` field **MUST** match the
526/// > consensus branch ID used for SIGHASH transaction hashes, as specified in [ZIP-244].
527///
528/// ### Notes
529///
530/// - When deserializing transactions, Zebra converts the `nConsensusBranchId` into
531/// [`NetworkUpgrade`].
532///
533/// - The values returned by [`Transaction::version`] match `effectiveVersion` so we use them in
534/// place of `effectiveVersion`. More details in [`Transaction::version`].
535///
536/// [ZIP-244]: <https://zips.z.cash/zip-0244>
537/// [7.1.2 Transaction Consensus Rules]: <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
538pub fn consensus_branch_id(
539 tx: &Transaction,
540 height: Height,
541 network: &Network,
542) -> Result<(), TransactionError> {
543 let current_nu = NetworkUpgrade::current(network, height);
544
545 if current_nu < NetworkUpgrade::Nu5 || tx.version() < 5 {
546 return Ok(());
547 }
548
549 let Some(tx_nu) = tx.network_upgrade() else {
550 return Err(TransactionError::MissingConsensusBranchId);
551 };
552
553 if tx_nu != current_nu {
554 return Err(TransactionError::WrongConsensusBranchId);
555 }
556
557 Ok(())
558}