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}