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 if !tx.has_transparent_or_shielded_inputs() {
126 Err(TransactionError::NoInputs)
127 } else if !tx.has_transparent_or_shielded_outputs() {
128 Err(TransactionError::NoOutputs)
129 } else {
130 Ok(())
131 }
132}
133
134/// Checks that the transaction has enough orchard flags.
135///
136/// # Consensus
137///
138/// For `Transaction::V5` only:
139///
140/// > [NU5 onward] If effectiveVersion >= 5 and nActionsOrchard > 0, then at least one of enableSpendsOrchard and enableOutputsOrchard MUST be 1.
141///
142/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
143pub fn has_enough_orchard_flags(tx: &Transaction) -> Result<(), TransactionError> {
144 if !tx.has_enough_orchard_flags() {
145 return Err(TransactionError::NotEnoughFlags);
146 }
147 Ok(())
148}
149
150/// Check that a coinbase transaction has no PrevOut inputs, JoinSplits, or spends.
151///
152/// # Consensus
153///
154/// > A coinbase transaction MUST NOT have any JoinSplit descriptions.
155///
156/// > A coinbase transaction MUST NOT have any Spend descriptions.
157///
158/// > [NU5 onward] In a version 5 coinbase transaction, the enableSpendsOrchard flag MUST be 0.
159///
160/// This check only counts `PrevOut` transparent inputs.
161///
162/// > [Pre-Heartwood] A coinbase transaction also MUST NOT have any Output descriptions.
163///
164/// Zebra does not validate this last rule explicitly because we checkpoint until Canopy activation.
165///
166/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
167pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), TransactionError> {
168 if tx.is_coinbase() {
169 if tx.joinsplit_count() > 0 {
170 return Err(TransactionError::CoinbaseHasJoinSplit);
171 } else if tx.sapling_spends_per_anchor().count() > 0 {
172 return Err(TransactionError::CoinbaseHasSpend);
173 }
174
175 if let Some(orchard_shielded_data) = tx.orchard_shielded_data() {
176 if orchard_shielded_data.flags.contains(Flags::ENABLE_SPENDS) {
177 return Err(TransactionError::CoinbaseHasEnableSpendsOrchard);
178 }
179 }
180 }
181
182 Ok(())
183}
184
185/// Check if JoinSplits in the transaction have one of its v_{pub} values equal
186/// to zero.
187///
188/// <https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc>
189pub fn joinsplit_has_vpub_zero(tx: &Transaction) -> Result<(), TransactionError> {
190 let zero = Amount::<NonNegative>::try_from(0).expect("an amount of 0 is always valid");
191
192 let vpub_pairs = tx
193 .output_values_to_sprout()
194 .zip(tx.input_values_from_sprout());
195 for (vpub_old, vpub_new) in vpub_pairs {
196 // # Consensus
197 //
198 // > Either v_{pub}^{old} or v_{pub}^{new} MUST be zero.
199 //
200 // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
201 if *vpub_old != zero && *vpub_new != zero {
202 return Err(TransactionError::BothVPubsNonZero);
203 }
204 }
205
206 Ok(())
207}
208
209/// Check if a transaction is adding to the sprout pool after Canopy
210/// network upgrade given a block height and a network.
211///
212/// <https://zips.z.cash/zip-0211>
213/// <https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc>
214pub fn disabled_add_to_sprout_pool(
215 tx: &Transaction,
216 height: Height,
217 network: &Network,
218) -> Result<(), TransactionError> {
219 let canopy_activation_height = NetworkUpgrade::Canopy
220 .activation_height(network)
221 .expect("Canopy activation height must be present for both networks");
222
223 // # Consensus
224 //
225 // > [Canopy onward]: `vpub_old` MUST be zero.
226 //
227 // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
228 if height >= canopy_activation_height {
229 let zero = Amount::<NonNegative>::try_from(0).expect("an amount of 0 is always valid");
230
231 let tx_sprout_pool = tx.output_values_to_sprout();
232 for vpub_old in tx_sprout_pool {
233 if *vpub_old != zero {
234 return Err(TransactionError::DisabledAddToSproutPool);
235 }
236 }
237 }
238
239 Ok(())
240}
241
242/// Check if a transaction has any internal spend conflicts.
243///
244/// An internal spend conflict happens if the transaction spends a UTXO more than once or if it
245/// reveals a nullifier more than once.
246///
247/// Consensus rules:
248///
249/// "each output of a particular transaction
250/// can only be used as an input once in the block chain.
251/// Any subsequent reference is a forbidden double spend-
252/// an attempt to spend the same satoshis twice."
253///
254/// <https://developer.bitcoin.org/devguide/block_chain.html#introduction>
255///
256/// A _nullifier_ *MUST NOT* repeat either within a _transaction_, or across _transactions_ in a
257/// _valid blockchain_ . *Sprout* and *Sapling* and *Orchard* _nulliers_ are considered disjoint,
258/// even if they have the same bit pattern.
259///
260/// <https://zips.z.cash/protocol/protocol.pdf#nullifierset>
261pub fn spend_conflicts(transaction: &Transaction) -> Result<(), TransactionError> {
262 use crate::error::TransactionError::*;
263
264 let transparent_outpoints = transaction.spent_outpoints().map(Cow::Owned);
265 let sprout_nullifiers = transaction.sprout_nullifiers().map(Cow::Borrowed);
266 let sapling_nullifiers = transaction.sapling_nullifiers().map(Cow::Borrowed);
267 let orchard_nullifiers = transaction.orchard_nullifiers().map(Cow::Borrowed);
268
269 check_for_duplicates(transparent_outpoints, DuplicateTransparentSpend)?;
270 check_for_duplicates(sprout_nullifiers, DuplicateSproutNullifier)?;
271 check_for_duplicates(sapling_nullifiers, DuplicateSaplingNullifier)?;
272 check_for_duplicates(orchard_nullifiers, DuplicateOrchardNullifier)?;
273
274 Ok(())
275}
276
277/// Check for duplicate items in a collection.
278///
279/// Each item should be wrapped by a [`Cow`] instance so that this helper function can properly
280/// handle borrowed items and owned items.
281///
282/// If a duplicate is found, an error created by the `error_wrapper` is returned.
283fn check_for_duplicates<'t, T>(
284 items: impl IntoIterator<Item = Cow<'t, T>>,
285 error_wrapper: impl FnOnce(T) -> TransactionError,
286) -> Result<(), TransactionError>
287where
288 T: Clone + Eq + Hash + 't,
289{
290 let mut hash_set = HashSet::new();
291
292 for item in items {
293 if let Some(duplicate) = hash_set.replace(item) {
294 return Err(error_wrapper(duplicate.into_owned()));
295 }
296 }
297
298 Ok(())
299}
300
301/// Checks compatibility with [ZIP-212] shielded Sapling and Orchard coinbase output decryption
302///
303/// Pre-Heartwood: returns `Ok`.
304/// Heartwood-onward: returns `Ok` if all Sapling or Orchard outputs, if any, decrypt successfully with
305/// an all-zeroes outgoing viewing key. Returns `Err` otherwise.
306///
307/// This is used to validate coinbase transactions:
308///
309/// # Consensus
310///
311/// > [Heartwood onward] All Sapling and Orchard outputs in coinbase transactions MUST decrypt to a note
312/// > plaintext, i.e. the procedure in § 4.20.3 ‘Decryption using a Full Viewing Key (Sapling and Orchard)’
313/// > does not return ⊥, using a sequence of 32 zero bytes as the outgoing viewing key. (This implies that before
314/// > Canopy activation, Sapling outputs of a coinbase transaction MUST have note plaintext lead byte equal to
315/// > 0x01.)
316///
317/// > [Canopy onward] Any Sapling or Orchard output of a coinbase transaction decrypted to a note plaintext
318/// > according to the preceding rule MUST have note plaintext lead byte equal to 0x02. (This applies even during
319/// > the "grace period" specified in [ZIP-212].)
320///
321/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
322///
323/// [ZIP-212]: https://zips.z.cash/zip-0212#consensus-rule-change-for-coinbase-transactions
324///
325/// TODO: Currently, a 0x01 lead byte is allowed in the "grace period" mentioned since we're
326/// using `librustzcash` to implement this and it doesn't currently allow changing that behavior.
327/// <https://github.com/ZcashFoundation/zebra/issues/3027>
328pub fn coinbase_outputs_are_decryptable(
329 transaction: &Transaction,
330 network: &Network,
331 height: Height,
332) -> Result<(), TransactionError> {
333 // Do quick checks first so we can avoid an expensive tx conversion
334 // in `zcash_note_encryption::decrypts_successfully`.
335
336 // The consensus rule only applies to coinbase txs with shielded outputs.
337 if !transaction.has_shielded_outputs() {
338 return Ok(());
339 }
340
341 // The consensus rule only applies to Heartwood onward.
342 if height
343 < NetworkUpgrade::Heartwood
344 .activation_height(network)
345 .expect("Heartwood height is known")
346 {
347 return Ok(());
348 }
349
350 // The passed tx should have been be a coinbase tx.
351 if !transaction.is_coinbase() {
352 return Err(TransactionError::NotCoinbase);
353 }
354
355 if !zcash_note_encryption::decrypts_successfully(transaction, network, height) {
356 return Err(TransactionError::CoinbaseOutputsNotDecryptable);
357 }
358
359 Ok(())
360}
361
362/// Returns `Ok(())` if the expiry height for the coinbase transaction is valid
363/// according to specifications [7.1] and [ZIP-203].
364///
365/// [7.1]: https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
366/// [ZIP-203]: https://zips.z.cash/zip-0203
367pub fn coinbase_expiry_height(
368 block_height: &Height,
369 coinbase: &Transaction,
370 network: &Network,
371) -> Result<(), TransactionError> {
372 let expiry_height = coinbase.expiry_height();
373
374 if let Some(nu5_activation_height) = NetworkUpgrade::Nu5.activation_height(network) {
375 // # Consensus
376 //
377 // > [NU5 onward] The nExpiryHeight field of a coinbase transaction
378 // > MUST be equal to its block height.
379 //
380 // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
381 if *block_height >= nu5_activation_height {
382 if expiry_height != Some(*block_height) {
383 return Err(TransactionError::CoinbaseExpiryBlockHeight {
384 expiry_height,
385 block_height: *block_height,
386 transaction_hash: coinbase.hash(),
387 });
388 } else {
389 return Ok(());
390 }
391 }
392 }
393
394 // # Consensus
395 //
396 // > [Overwinter to Canopy inclusive, pre-NU5] nExpiryHeight MUST be less than
397 // > or equal to 499999999.
398 //
399 // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
400 validate_expiry_height_max(expiry_height, true, block_height, coinbase)
401}
402
403/// Returns `Ok(())` if the expiry height for a non coinbase transaction is
404/// valid according to specifications [7.1] and [ZIP-203].
405///
406/// [7.1]: https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
407/// [ZIP-203]: https://zips.z.cash/zip-0203
408pub fn non_coinbase_expiry_height(
409 block_height: &Height,
410 transaction: &Transaction,
411) -> Result<(), TransactionError> {
412 if transaction.is_overwintered() {
413 let expiry_height = transaction.expiry_height();
414
415 // # Consensus
416 //
417 // > [Overwinter to Canopy inclusive, pre-NU5] nExpiryHeight MUST be
418 // > less than or equal to 499999999.
419 //
420 // > [NU5 onward] nExpiryHeight MUST be less than or equal to 499999999
421 // > for non-coinbase transactions.
422 //
423 // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
424 validate_expiry_height_max(expiry_height, false, block_height, transaction)?;
425
426 // # Consensus
427 //
428 // > [Overwinter onward] If a transaction is not a coinbase transaction and its
429 // > nExpiryHeight field is nonzero, then it MUST NOT be mined at a block
430 // > height greater than its nExpiryHeight.
431 //
432 // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
433 validate_expiry_height_mined(expiry_height, block_height, transaction)?;
434 }
435 Ok(())
436}
437
438/// Checks that the expiry height of a transaction does not exceed the maximal
439/// value.
440///
441/// Only the `expiry_height` parameter is used for the check. The
442/// remaining parameters are used to give details about the error when the check
443/// fails.
444fn validate_expiry_height_max(
445 expiry_height: Option<Height>,
446 is_coinbase: bool,
447 block_height: &Height,
448 transaction: &Transaction,
449) -> Result<(), TransactionError> {
450 if let Some(expiry_height) = expiry_height {
451 if expiry_height > Height::MAX_EXPIRY_HEIGHT {
452 Err(TransactionError::MaximumExpiryHeight {
453 expiry_height,
454 is_coinbase,
455 block_height: *block_height,
456 transaction_hash: transaction.hash(),
457 })?;
458 }
459 }
460
461 Ok(())
462}
463
464/// Checks that a transaction does not exceed its expiry height.
465///
466/// The `transaction` parameter is only used to give details about the error
467/// when the check fails.
468fn validate_expiry_height_mined(
469 expiry_height: Option<Height>,
470 block_height: &Height,
471 transaction: &Transaction,
472) -> Result<(), TransactionError> {
473 if let Some(expiry_height) = expiry_height {
474 if *block_height > expiry_height {
475 Err(TransactionError::ExpiredTransaction {
476 expiry_height,
477 block_height: *block_height,
478 transaction_hash: transaction.hash(),
479 })?;
480 }
481 }
482
483 Ok(())
484}
485
486/// Accepts a transaction, block height, block UTXOs, and
487/// the transaction's spent UTXOs from the chain.
488///
489/// Returns `Ok(())` if spent transparent coinbase outputs are
490/// valid for the block height, or a [`Err(TransactionError)`](TransactionError)
491pub fn tx_transparent_coinbase_spends_maturity(
492 network: &Network,
493 tx: Arc<Transaction>,
494 height: Height,
495 block_new_outputs: Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>>,
496 spent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
497) -> Result<(), TransactionError> {
498 for spend in tx.spent_outpoints() {
499 let utxo = block_new_outputs
500 .get(&spend)
501 .map(|ordered_utxo| ordered_utxo.utxo.clone())
502 .or_else(|| spent_utxos.get(&spend).cloned())
503 .expect("load_spent_utxos_fut.await should return an error if a utxo is missing");
504
505 let spend_restriction = tx.coinbase_spend_restriction(network, height);
506
507 zebra_state::check::transparent_coinbase_spend(spend, spend_restriction, &utxo)?;
508 }
509
510 Ok(())
511}
512
513/// Checks the `nConsensusBranchId` field.
514///
515/// # Consensus
516///
517/// ## [7.1.2 Transaction Consensus Rules]
518///
519/// > [**NU5** onward] If `effectiveVersion` ≥ 5, the `nConsensusBranchId` field **MUST** match the
520/// > consensus branch ID used for SIGHASH transaction hashes, as specified in [ZIP-244].
521///
522/// ### Notes
523///
524/// - When deserializing transactions, Zebra converts the `nConsensusBranchId` into
525/// [`NetworkUpgrade`].
526///
527/// - The values returned by [`Transaction::version`] match `effectiveVersion` so we use them in
528/// place of `effectiveVersion`. More details in [`Transaction::version`].
529///
530/// [ZIP-244]: <https://zips.z.cash/zip-0244>
531/// [7.1.2 Transaction Consensus Rules]: <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
532pub fn consensus_branch_id(
533 tx: &Transaction,
534 height: Height,
535 network: &Network,
536) -> Result<(), TransactionError> {
537 let current_nu = NetworkUpgrade::current(network, height);
538
539 if current_nu < NetworkUpgrade::Nu5 || tx.version() < 5 {
540 return Ok(());
541 }
542
543 let Some(tx_nu) = tx.network_upgrade() else {
544 return Err(TransactionError::MissingConsensusBranchId);
545 };
546
547 if tx_nu != current_nu {
548 return Err(TransactionError::WrongConsensusBranchId);
549 }
550
551 Ok(())
552}