Skip to main content

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 zcash_script::{
15    opcode::PossiblyBad,
16    script::{self, Evaluable as _},
17    solver, Opcode,
18};
19use zebra_chain::{
20    amount::{Amount, NegativeAllowed},
21    block::Height,
22    parameters::{Network, NetworkUpgrade},
23    primitives::zcash_note_encryption,
24    transaction::{LockTime, Transaction},
25    transparent,
26};
27
28use crate::error::TransactionError;
29
30/// Checks if the transaction's lock time allows this transaction to be included in a block.
31///
32/// Arguments:
33/// - `block_height`: the height of the mined block, or the height of the next block for mempool
34///   transactions
35/// - `block_time`: the time in the mined block header, or the median-time-past of the next block
36///   for the mempool. Optional if the lock time is a height.
37///
38/// # Panics
39///
40/// If the lock time is a time, and `block_time` is `None`.
41///
42/// # Consensus
43///
44/// > The transaction must be finalized: either its locktime must be in the past (or less
45/// > than or equal to the current block height), or all of its sequence numbers must be
46/// > 0xffffffff.
47///
48/// [`Transaction::lock_time`] validates the transparent input sequence numbers, returning [`None`]
49/// if they indicate that the transaction is finalized by them.
50/// Otherwise, this function checks that the lock time is in the past.
51///
52/// ## Mempool Consensus for Block Templates
53///
54/// > the nTime field MUST represent a time strictly greater than the median of the
55/// > timestamps of the past PoWMedianBlockSpan blocks.
56///
57/// <https://zips.z.cash/protocol/protocol.pdf#blockheader>
58///
59/// > The transaction can be added to any block whose block time is greater than the locktime.
60///
61/// <https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number>
62///
63/// If the transaction's lock time is less than the median-time-past,
64/// it will always be less than the next block's time,
65/// because the next block's time is strictly greater than the median-time-past.
66/// (That is, `lock-time < median-time-past < block-header-time`.)
67///
68/// Using `median-time-past + 1s` (the next block's mintime) would also satisfy this consensus rule,
69/// but we prefer the rule implemented by `zcashd`'s mempool:
70/// <https://github.com/zcash/zcash/blob/9e1efad2d13dca5ee094a38e6aa25b0f2464da94/src/main.cpp#L776-L784>
71pub fn lock_time_has_passed(
72    tx: &Transaction,
73    block_height: Height,
74    block_time: impl Into<Option<DateTime<Utc>>>,
75) -> Result<(), TransactionError> {
76    match tx.lock_time() {
77        Some(LockTime::Height(unlock_height)) => {
78            // > The transaction can be added to any block which has a greater height.
79            // The Bitcoin documentation is wrong or outdated here,
80            // so this code is based on the `zcashd` implementation at:
81            // https://github.com/zcash/zcash/blob/1a7c2a3b04bcad6549be6d571bfdff8af9a2c814/src/main.cpp#L722
82            if block_height > unlock_height {
83                Ok(())
84            } else {
85                Err(TransactionError::LockedUntilAfterBlockHeight(unlock_height))
86            }
87        }
88        Some(LockTime::Time(unlock_time)) => {
89            // > The transaction can be added to any block whose block time is greater than the locktime.
90            // https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number
91            let block_time = block_time
92                .into()
93                .expect("time must be provided if LockTime is a time");
94
95            if block_time > unlock_time {
96                Ok(())
97            } else {
98                Err(TransactionError::LockedUntilAfterBlockTime(unlock_time))
99            }
100        }
101        None => Ok(()),
102    }
103}
104
105/// Checks that the transaction has inputs and outputs.
106///
107/// # Consensus
108///
109/// > [Sapling onward] If effectiveVersion < 5, then at least one of
110/// > tx_in_count, nSpendsSapling, and nJoinSplit MUST be nonzero.
111///
112/// > [Sapling onward] If effectiveVersion < 5, then at least one of
113/// > tx_out_count, nOutputsSapling, and nJoinSplit MUST be nonzero.
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/// > [NU6.3 onward] If effectiveVersion >= 6 then this condition MUST hold:
122/// > tx_in_count > 0 or nSpendsSapling > 0 or (nActionsOrchard > 0 and enableSpendsOrchard = 1) or (nActionsIronwood > 0 and enableSpendsIronwood = 1).
123///
124/// > [NU6.3 onward] If effectiveVersion >= 6 then this condition MUST hold:
125/// > tx_out_count > 0 or nOutputsSapling > 0 or (nActionsOrchard > 0 and enableOutputsOrchard = 1) or (nActionsIronwood > 0 and enableOutputsIronwood = 1).
126///
127/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
128///
129/// This check counts both `Coinbase` and `PrevOut` transparent inputs.
130pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> {
131    if !tx.has_transparent_or_shielded_inputs() {
132        Err(TransactionError::NoInputs)
133    } else if !tx.has_transparent_or_shielded_outputs() {
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::NotEnoughOrchardFlags);
152    }
153    Ok(())
154}
155
156/// Checks that a transaction with Ironwood actions has at least one Ironwood flag set.
157///
158/// # Consensus
159///
160/// > [NU6.3 onward] If effectiveVersion ≥ 6 and nActionsIronwood > 0, then at least one of
161/// > enableSpendsIronwood and enableOutputsIronwood MUST be 1.
162///
163/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
164///
165/// (No-op for transactions without Ironwood actions, i.e. all pre-v6 transactions.)
166pub fn has_enough_ironwood_flags(tx: &Transaction) -> Result<(), TransactionError> {
167    if !tx.has_enough_ironwood_flags() {
168        return Err(TransactionError::NotEnoughIronwoodFlags);
169    }
170    Ok(())
171}
172
173/// Checks that the Orchard pool does not enable cross-address transfers (NU6.3 onward).
174///
175/// # Consensus
176///
177/// > [NU6.3 onward] The `enableCrossAddress` flag of `flagsOrchard` MUST be 0.
178///
179/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
180///
181/// An Orchard bundle can never carry this flag off the wire: bit 2 is rejected at deserialization
182/// for the Orchard pool in every tx version (only the Ironwood pool permits it). So this is a
183/// defense-in-depth check that also covers an in-memory-constructed bundle.
184pub fn orchard_cross_address_disabled(tx: &Transaction) -> Result<(), TransactionError> {
185    // Only the NU6.3-onward Orchard pool (`orchard_v3`) carries `enableCrossAddress` as a wire
186    // flag that must be 0. Earlier Orchard revisions have no such bit — cross-address transfers
187    // are unconditionally permitted and `Flags::cross_address_enabled` reports `true` for every
188    // pre-NU6.3 bundle — so restricting this to `orchard_v3` is what keeps the rule from
189    // rejecting every Orchard transaction already on chain.
190    if tx.orchard_bundle().is_some_and(|bundle| {
191        bundle.bundle_version() == ::orchard::bundle::BundleVersion::orchard_v3()
192            && bundle.flags().cross_address_enabled()
193    }) {
194        return Err(TransactionError::OrchardHasEnableCrossAddress);
195    }
196
197    Ok(())
198}
199
200/// Checks that no net new value is shielded into the Orchard pool from NU6.3 onward.
201///
202/// # Consensus
203///
204/// > [NU6.3 onward] `valueBalanceOrchard` MUST be nonnegative.
205///
206/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
207///
208/// From NU6.3, newly shielded value is routed to the Ironwood pool, so the Orchard pool is frozen
209/// against new inflows. An Orchard bundle may still spend existing notes — Orchard-to-Orchard note
210/// management nets to a zero balance and Orchard-to-transparent unshielding to a positive one — but
211/// a net-negative `valueBalanceOrchard` (which would move new value into the pool) is rejected.
212///
213/// This applies to both v5 and v6 Orchard bundles, since v5 Orchard bundles remain valid after
214/// NU6.3 (so that non-upgraded hardware wallets can keep authorizing Orchard spends).
215///
216/// (No-op for transactions without an Orchard bundle, and before NU6.3.)
217pub fn orchard_value_balance_non_negative(
218    tx: &Transaction,
219    network_upgrade: NetworkUpgrade,
220) -> Result<(), TransactionError> {
221    if network_upgrade >= NetworkUpgrade::Nu6_3
222        && tx.orchard_bundle().is_some()
223        && tx.orchard_value_balance().orchard_amount() < Amount::<NegativeAllowed>::zero()
224    {
225        return Err(TransactionError::NegativeOrchardValueBalance);
226    }
227
228    Ok(())
229}
230
231/// Checks that a coinbase transaction has an empty Orchard component from NU6.3 onward.
232///
233/// # Consensus
234///
235/// > [NU6.3 onward] Coinbase transactions MUST have an empty Orchard component.
236///
237/// <https://zips.z.cash/zip-0229>
238///
239/// From NU6.3, newly shielded coinbase value is routed to the Ironwood pool instead, so coinbase
240/// transactions can no longer create Orchard notes. This is stronger than the pre-NU6.3 rule (which
241/// only forbids `enableSpendsOrchard`) and applies regardless of transaction version: a v5 coinbase
242/// mined at NU6.3 is constrained too, so the rule cannot be bypassed by using an older format.
243///
244/// (No-op for non-coinbase transactions, transactions without an Orchard component, and before
245/// NU6.3.)
246pub fn coinbase_orchard_component_empty(
247    tx: &Transaction,
248    network_upgrade: NetworkUpgrade,
249) -> Result<(), TransactionError> {
250    if network_upgrade >= NetworkUpgrade::Nu6_3
251        && tx.is_coinbase()
252        && tx.has_orchard_shielded_data()
253    {
254        return Err(TransactionError::CoinbaseHasOrchardActions);
255    }
256
257    Ok(())
258}
259
260/// Check that a coinbase transaction has no PrevOut inputs, JoinSplits, or spends.
261///
262/// # Consensus
263///
264/// > A coinbase transaction MUST NOT have any JoinSplit descriptions.
265///
266/// > A coinbase transaction MUST NOT have any Spend descriptions.
267///
268/// > [NU5 onward] In a version 5 coinbase transaction, the enableSpendsOrchard flag MUST be 0.
269///
270/// This check only counts `PrevOut` transparent inputs.
271///
272/// > [Pre-Heartwood] A coinbase transaction also MUST NOT have any Output descriptions.
273///
274/// Zebra does not validate this last rule explicitly because we checkpoint until Canopy activation.
275///
276/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
277pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), TransactionError> {
278    if tx.is_coinbase() {
279        if tx.joinsplit_count() > 0 {
280            return Err(TransactionError::CoinbaseHasJoinSplit);
281        } else if tx.sapling_spends_count() > 0 {
282            return Err(TransactionError::CoinbaseHasSpend);
283        }
284
285        if let Some(flags) = tx.orchard_flags() {
286            if flags.spends_enabled() {
287                return Err(TransactionError::CoinbaseHasEnableSpendsOrchard);
288            }
289        }
290
291        // The stronger NU6.3 rule that a coinbase transaction must have an *empty* Orchard component
292        // is height-gated and applies to every transaction version, so it lives in
293        // `coinbase_orchard_component_empty` (called from `check_structure_and_network_rules`).
294
295        // > [NU6.3 onward] In a version 6 coinbase transaction, the enableSpendsIronwood flag MUST
296        // > be 0.
297        //
298        // (`ironwood_shielded_data` is only ever present in v6 transactions, so this is a no-op for
299        // earlier versions.)
300        if tx
301            .ironwood_flags()
302            .is_some_and(|flags| flags.spends_enabled())
303        {
304            return Err(TransactionError::CoinbaseHasEnableSpendsIronwood);
305        }
306    }
307
308    Ok(())
309}
310
311/// Check if JoinSplits in the transaction have one of its v_{pub} values equal
312/// to zero.
313///
314/// <https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc>
315pub fn joinsplit_has_vpub_zero(tx: &Transaction) -> Result<(), TransactionError> {
316    let vpub_old_values = tx.output_values_to_sprout();
317    let vpub_new_values = tx.input_values_from_sprout();
318
319    for (vpub_old, vpub_new) in vpub_old_values.iter().zip(vpub_new_values.iter()) {
320        // # Consensus
321        //
322        // > Either v_{pub}^{old} or v_{pub}^{new} MUST be zero.
323        //
324        // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
325        if *vpub_old != 0 && *vpub_new != 0 {
326            return Err(TransactionError::BothVPubsNonZero);
327        }
328    }
329
330    Ok(())
331}
332
333/// Check if a transaction is adding to the sprout pool after Canopy
334/// network upgrade given a block height and a network.
335///
336/// <https://zips.z.cash/zip-0211>
337/// <https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc>
338pub fn disabled_add_to_sprout_pool(
339    tx: &Transaction,
340    height: Height,
341    network: &Network,
342) -> Result<(), TransactionError> {
343    let canopy_activation_height = NetworkUpgrade::Canopy
344        .activation_height(network)
345        .expect("Canopy activation height must be present for both networks");
346
347    // # Consensus
348    //
349    // > [Canopy onward]: `vpub_old` MUST be zero.
350    //
351    // https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
352    if height >= canopy_activation_height {
353        for vpub_old in tx.output_values_to_sprout() {
354            if vpub_old != 0 {
355                return Err(TransactionError::DisabledAddToSproutPool);
356            }
357        }
358    }
359
360    Ok(())
361}
362
363/// Check if a transaction has any internal spend conflicts.
364///
365/// An internal spend conflict happens if the transaction spends a UTXO more than once or if it
366/// reveals a nullifier more than once.
367///
368/// Consensus rules:
369///
370/// "each output of a particular transaction
371/// can only be used as an input once in the block chain.
372/// Any subsequent reference is a forbidden double spend-
373/// an attempt to spend the same satoshis twice."
374///
375/// <https://developer.bitcoin.org/devguide/block_chain.html#introduction>
376///
377/// A _nullifier_ *MUST NOT* repeat either within a _transaction_, or across _transactions_ in a
378/// _valid blockchain_ . *Sprout* and *Sapling* and *Orchard* _nulliers_ are considered disjoint,
379/// even if they have the same bit pattern.
380///
381/// <https://zips.z.cash/protocol/protocol.pdf#nullifierset>
382pub fn spend_conflicts(transaction: &Transaction) -> Result<(), TransactionError> {
383    use crate::error::TransactionError::*;
384
385    // All the nullifier accessors yield owned values, so they are wrapped as `Cow::Owned`.
386    // Ironwood and Orchard nullifiers are disjoint.
387    let transparent_outpoints: Vec<_> = transaction.spent_outpoints().collect();
388    let sprout_nullifiers: Vec<_> = transaction.sprout_nullifiers().collect();
389    let sapling_nullifiers: Vec<_> = transaction.sapling_nullifiers().collect();
390    let orchard_nullifiers: Vec<_> = transaction.orchard_nullifiers().collect();
391    let ironwood_nullifiers: Vec<_> = transaction.ironwood_nullifiers().collect();
392
393    check_for_duplicates(
394        transparent_outpoints.into_iter().map(Cow::Owned),
395        DuplicateTransparentSpend,
396    )?;
397    check_for_duplicates(
398        sprout_nullifiers.into_iter().map(Cow::Owned),
399        DuplicateSproutNullifier,
400    )?;
401    check_for_duplicates(
402        sapling_nullifiers.into_iter().map(Cow::Owned),
403        DuplicateSaplingNullifier,
404    )?;
405    check_for_duplicates(
406        orchard_nullifiers.into_iter().map(Cow::Owned),
407        DuplicateOrchardNullifier,
408    )?;
409    check_for_duplicates(
410        ironwood_nullifiers.into_iter().map(Cow::Owned),
411        DuplicateIronwoodNullifier,
412    )?;
413
414    Ok(())
415}
416
417/// Check for duplicate items in a collection.
418///
419/// Each item should be wrapped by a [`Cow`] instance so that this helper function can properly
420/// handle borrowed items and owned items.
421///
422/// If a duplicate is found, an error created by the `error_wrapper` is returned.
423fn check_for_duplicates<'t, T>(
424    items: impl IntoIterator<Item = Cow<'t, T>>,
425    error_wrapper: impl FnOnce(T) -> TransactionError,
426) -> Result<(), TransactionError>
427where
428    T: Clone + Eq + Hash + 't,
429{
430    let mut hash_set = HashSet::new();
431
432    for item in items {
433        if let Some(duplicate) = hash_set.replace(item) {
434            return Err(error_wrapper(duplicate.into_owned()));
435        }
436    }
437
438    Ok(())
439}
440
441/// Checks compatibility with [ZIP-212] shielded Sapling and Orchard coinbase output decryption
442///
443/// Pre-Heartwood: returns `Ok`.
444/// Heartwood-onward: returns `Ok` if all Sapling or Orchard outputs, if any, decrypt successfully with
445/// an all-zeroes outgoing viewing key. Returns `Err` otherwise.
446///
447/// This is used to validate coinbase transactions:
448///
449/// # Consensus
450///
451/// > [Heartwood onward] All Sapling and Orchard outputs in coinbase transactions MUST decrypt to a note
452/// > plaintext, i.e. the procedure in § 4.20.3 ‘Decryption using a Full Viewing Key (Sapling and Orchard)’
453/// > does not return ⊥, using a sequence of 32 zero bytes as the outgoing viewing key. (This implies that before
454/// > Canopy activation, Sapling outputs of a coinbase transaction MUST have note plaintext lead byte equal to
455/// > 0x01.)
456///
457/// > [Canopy onward] Any Sapling or Orchard output of a coinbase transaction decrypted to a note plaintext
458/// > according to the preceding rule MUST have note plaintext lead byte equal to 0x02. (This applies even during
459/// > the "grace period" specified in [ZIP-212].)
460///
461/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
462///
463/// [ZIP-212]: https://zips.z.cash/zip-0212#consensus-rule-change-for-coinbase-transactions
464///
465/// TODO: Currently, a 0x01 lead byte is allowed in the "grace period" mentioned since we're
466/// using `librustzcash` to implement this and it doesn't currently allow changing that behavior.
467/// <https://github.com/ZcashFoundation/zebra/issues/3027>
468pub fn coinbase_outputs_are_decryptable(
469    transaction: &Transaction,
470    network: &Network,
471    height: Height,
472) -> Result<(), TransactionError> {
473    // Do quick checks first so we can avoid an expensive tx conversion.
474
475    // The consensus rule only applies to coinbase txs with shielded outputs.
476    if !transaction.has_shielded_outputs() {
477        return Ok(());
478    }
479
480    // The consensus rule only applies to Heartwood onward.
481    if height
482        < NetworkUpgrade::Heartwood
483            .activation_height(network)
484            .expect("Heartwood height is known")
485    {
486        return Ok(());
487    }
488
489    // The passed tx should have been be a coinbase tx.
490    if !transaction.is_coinbase() {
491        return Err(TransactionError::NotCoinbase);
492    }
493
494    if !zcash_note_encryption::decrypts_successfully(transaction, network, height) {
495        return Err(TransactionError::CoinbaseOutputsNotDecryptable);
496    }
497
498    Ok(())
499}
500
501/// Returns `Ok(())` if the expiry height for the coinbase transaction is valid
502/// according to specifications [7.1] and [ZIP-203].
503///
504/// [7.1]: https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
505/// [ZIP-203]: https://zips.z.cash/zip-0203
506pub fn coinbase_expiry_height(
507    block_height: &Height,
508    coinbase: &Transaction,
509    network: &Network,
510) -> Result<(), TransactionError> {
511    let expiry_height = coinbase.expiry_height();
512
513    if let Some(nu5_activation_height) = NetworkUpgrade::Nu5.activation_height(network) {
514        // # Consensus
515        //
516        // > [NU5 onward] The nExpiryHeight field of a coinbase transaction
517        // > MUST be equal to its block height.
518        //
519        // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
520        if *block_height >= nu5_activation_height {
521            if expiry_height != Some(*block_height) {
522                return Err(TransactionError::CoinbaseExpiryBlockHeight {
523                    expiry_height,
524                    block_height: *block_height,
525                    transaction_hash: coinbase.hash(),
526                });
527            } else {
528                return Ok(());
529            }
530        }
531    }
532
533    // # Consensus
534    //
535    // > [Overwinter to Canopy inclusive, pre-NU5] nExpiryHeight MUST be less than
536    // > or equal to 499999999.
537    //
538    // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
539    validate_expiry_height_max(expiry_height, true, block_height, coinbase)
540}
541
542/// Returns `Ok(())` if the expiry height for a non coinbase transaction is
543/// valid according to specifications [7.1] and [ZIP-203].
544///
545/// [7.1]: https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
546/// [ZIP-203]: https://zips.z.cash/zip-0203
547pub fn non_coinbase_expiry_height(
548    block_height: &Height,
549    transaction: &Transaction,
550) -> Result<(), TransactionError> {
551    if transaction.is_overwintered() {
552        let expiry_height = transaction.expiry_height();
553
554        // # Consensus
555        //
556        // > [Overwinter to Canopy inclusive, pre-NU5] nExpiryHeight MUST be
557        // > less than or equal to 499999999.
558        //
559        // > [NU5 onward] nExpiryHeight MUST be less than or equal to 499999999
560        // > for non-coinbase transactions.
561        //
562        // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
563        validate_expiry_height_max(expiry_height, false, block_height, transaction)?;
564
565        // # Consensus
566        //
567        // > [Overwinter onward] If a transaction is not a coinbase transaction and its
568        // > nExpiryHeight field is nonzero, then it MUST NOT be mined at a block
569        // > height greater than its nExpiryHeight.
570        //
571        // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
572        validate_expiry_height_mined(expiry_height, block_height, transaction)?;
573    }
574    Ok(())
575}
576
577/// Checks that the expiry height of a transaction does not exceed the maximal
578/// value.
579///
580/// Only the `expiry_height` parameter is used for the check. The
581/// remaining parameters are used to give details about the error when the check
582/// fails.
583fn validate_expiry_height_max(
584    expiry_height: Option<Height>,
585    is_coinbase: bool,
586    block_height: &Height,
587    transaction: &Transaction,
588) -> Result<(), TransactionError> {
589    if let Some(expiry_height) = expiry_height {
590        if expiry_height > Height::MAX_EXPIRY_HEIGHT {
591            Err(TransactionError::MaximumExpiryHeight {
592                expiry_height,
593                is_coinbase,
594                block_height: *block_height,
595                transaction_hash: transaction.hash(),
596            })?;
597        }
598    }
599
600    Ok(())
601}
602
603/// Checks that a transaction does not exceed its expiry height.
604///
605/// The `transaction` parameter is only used to give details about the error
606/// when the check fails.
607fn validate_expiry_height_mined(
608    expiry_height: Option<Height>,
609    block_height: &Height,
610    transaction: &Transaction,
611) -> Result<(), TransactionError> {
612    if let Some(expiry_height) = expiry_height {
613        if *block_height > expiry_height {
614            Err(TransactionError::ExpiredTransaction {
615                expiry_height,
616                block_height: *block_height,
617                transaction_hash: transaction.hash(),
618            })?;
619        }
620    }
621
622    Ok(())
623}
624
625/// Accepts a transaction, block height, block UTXOs, and
626/// the transaction's spent UTXOs from the chain.
627///
628/// Returns `Ok(())` if spent transparent coinbase outputs are
629/// valid for the block height, or a [`Err(TransactionError)`](TransactionError)
630pub fn tx_transparent_coinbase_spends_maturity(
631    network: &Network,
632    tx: &Transaction,
633    height: Height,
634    block_new_outputs: Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>>,
635    spent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
636) -> Result<(), TransactionError> {
637    for spend in tx.spent_outpoints() {
638        let utxo = block_new_outputs
639            .get(&spend)
640            .map(|ordered_utxo| ordered_utxo.utxo.clone())
641            .or_else(|| spent_utxos.get(&spend).cloned())
642            .expect("load_spent_utxos_fut.await should return an error if a utxo is missing");
643
644        let spend_restriction = tx.coinbase_spend_restriction(network, height);
645
646        zebra_state::check::transparent_coinbase_spend(spend, spend_restriction, &utxo)?;
647    }
648
649    Ok(())
650}
651
652/// The maximum number of signature operations in the redeem script of a standard P2SH input.
653///
654/// This is zcashd's `MAX_P2SH_SIGOPS` standardness (policy) constant:
655/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.h#L20>
656pub const MAX_P2SH_SIGOPS: u32 = 15;
657
658/// The maximum size in bytes of the scriptSig of a standard transaction input.
659///
660/// This is zcashd's `MAX_STANDARD_SCRIPTSIG_SIZE` standardness (policy) constant:
661/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L92-L99>
662pub const MAX_STANDARD_SCRIPTSIG_SIZE: usize = 1650;
663
664/// Classify a script using the `zcash_script` solver.
665///
666/// Returns `Some(kind)` for standard script types, `None` for non-standard.
667///
668/// Mirrors the classification done by zcashd's `Solver()`.
669pub fn standard_script_kind(lock_script: &transparent::Script) -> Option<solver::ScriptKind> {
670    let code = script::Code(lock_script.as_raw_bytes().to_vec());
671    let component = code.to_component().ok()?.refine().ok()?;
672    solver::standard(&component)
673}
674
675/// Returns the expected number of scriptSig arguments for a given script kind.
676///
677/// Mirrors zcashd's `ScriptSigArgsExpected()`:
678/// <https://github.com/zcash/zcash/blob/v6.11.0/src/script/standard.cpp#L135>
679///
680/// Returns `None` for non-standard types (TX_NONSTANDARD, TX_NULL_DATA).
681pub(super) fn script_sig_args_expected(kind: &solver::ScriptKind) -> Option<usize> {
682    match kind {
683        solver::ScriptKind::PubKey { .. } => Some(1),
684        solver::ScriptKind::PubKeyHash { .. } => Some(2),
685        solver::ScriptKind::ScriptHash { .. } => Some(1),
686        solver::ScriptKind::MultiSig { required, .. } => Some(*required as usize + 1),
687        solver::ScriptKind::NullData { .. } => None,
688    }
689}
690
691/// Extract the redeemed script bytes from a P2SH scriptSig.
692///
693/// The redeemed script is the last data push in the scriptSig.
694/// Returns `None` if the scriptSig has no push operations.
695///
696/// # Precondition
697///
698/// The scriptSig should be push-only (enforced by [`mempool_standard_input_scripts`] before this
699/// function is reached). Non-push opcodes are silently ignored.
700pub(super) fn extract_p2sh_redeemed_script(unlock_script: &transparent::Script) -> Option<Vec<u8>> {
701    let code = script::Code(unlock_script.as_raw_bytes().to_vec());
702    let mut last_push_data: Option<Vec<u8>> = None;
703    for opcode in code.parse().flatten() {
704        if let PossiblyBad::Good(Opcode::PushValue(pv)) = opcode {
705            last_push_data = Some(pv.value());
706        }
707    }
708    last_push_data
709}
710
711/// Count the number of push operations in a script.
712///
713/// For a push-only script (already enforced for mempool scriptSigs),
714/// this equals the stack depth after evaluation.
715pub(super) fn count_script_push_ops(script_bytes: &[u8]) -> usize {
716    let code = script::Code(script_bytes.to_vec());
717    code.parse()
718        .filter(|op| matches!(op, Ok(PossiblyBad::Good(Opcode::PushValue(_)))))
719        .count()
720}
721
722/// Returns `true` if all of a transaction's transparent inputs are standard.
723///
724/// Mirrors zcashd's `AreInputsStandard()`:
725/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L136>
726///
727/// For each input:
728/// 1. The spent output's scriptPubKey must be a known standard type (via the `zcash_script`
729///    solver). Non-standard scripts and OP_RETURN outputs are rejected.
730/// 2. The scriptSig stack depth must match `ScriptSigArgsExpected()`.
731/// 3. For P2SH inputs:
732///    - If the redeemed script is standard, its expected args are added to the total.
733///    - If the redeemed script is non-standard, it must have at most [`MAX_P2SH_SIGOPS`] sigops.
734///
735/// # Correctness
736///
737/// Callers must ensure `spent_outputs.len()` matches the number of transparent inputs.
738/// If the lengths differ, `false` is returned.
739pub fn are_inputs_standard(tx: &Transaction, spent_outputs: &[transparent::Output]) -> bool {
740    if tx.inputs().len() != spent_outputs.len() {
741        return false;
742    }
743    for (input, spent_output) in tx.inputs().iter().zip(spent_outputs.iter()) {
744        let unlock_script = match input {
745            transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
746            transparent::Input::Coinbase { .. } => continue,
747        };
748
749        // Step 1: Classify the spent output's scriptPubKey via the zcash_script solver.
750        let script_kind = match standard_script_kind(&spent_output.lock_script) {
751            Some(kind) => kind,
752            None => return false,
753        };
754
755        // Step 2: Get expected number of scriptSig arguments.
756        // Returns None for TX_NONSTANDARD and TX_NULL_DATA.
757        let mut n_args_expected = match script_sig_args_expected(&script_kind) {
758            Some(n) => n,
759            None => return false,
760        };
761
762        // Step 3: Count actual push operations in scriptSig.
763        // For push-only scripts (enforced before this function), this equals the stack depth.
764        let stack_size = count_script_push_ops(unlock_script.as_raw_bytes());
765
766        // Step 4: P2SH-specific checks.
767        if matches!(script_kind, solver::ScriptKind::ScriptHash { .. }) {
768            let Some(redeemed_bytes) = extract_p2sh_redeemed_script(unlock_script) else {
769                return false;
770            };
771
772            let redeemed_code = script::Code(redeemed_bytes);
773
774            // Classify the redeemed script using the zcash_script solver.
775            let redeemed_kind = {
776                let component = redeemed_code
777                    .to_component()
778                    .ok()
779                    .and_then(|c| c.refine().ok());
780                component.and_then(|c| solver::standard(&c))
781            };
782
783            match redeemed_kind {
784                Some(ref inner_kind) => {
785                    // Standard redeemed script: add its expected args.
786                    match script_sig_args_expected(inner_kind) {
787                        Some(inner) => n_args_expected += inner,
788                        None => return false,
789                    }
790                }
791                None => {
792                    // Non-standard redeemed script: accept if sigops <= limit.
793                    // Matches zcashd: "Any other Script with less than 15 sigops OK:
794                    // ... extra data left on the stack after execution is OK, too"
795                    let sigops = redeemed_code.sig_op_count(true);
796                    if sigops > MAX_P2SH_SIGOPS {
797                        return false;
798                    }
799
800                    // This input is acceptable; move on to the next input.
801                    continue;
802                }
803            }
804        }
805
806        // Step 5: Reject if scriptSig has wrong number of stack items.
807        if stack_size != n_args_expected {
808            return false;
809        }
810    }
811    true
812}
813
814/// Standardness (policy) checks on a mempool transaction's transparent input scripts, applied
815/// *before* the transaction is dispatched to script verification. The goal is to avoid the
816/// expensive verification for non-standard transactions which would be rejected anyway
817/// by `Storage::reject_if_non_standard_tx()`; this is a subset of the checks
818/// in that function.
819///
820/// `spent_outputs` must contain the output spent by each of the transaction's transparent inputs,
821/// in input order.
822///
823/// # Correctness
824///
825/// `spent_outputs.len()` must equal the number of transparent inputs in `tx`: if the lengths
826/// differ, `zip()` silently truncates, and some inputs are not checked.
827pub fn mempool_standard_input_scripts(
828    tx: &Transaction,
829    spent_outputs: &[transparent::Output],
830) -> Result<(), TransactionError> {
831    if tx.inputs().len() != spent_outputs.len() {
832        return Err(TransactionError::Other(format!(
833            "spent_outputs must align with transaction inputs for non-coinbase txs: inputs={}, spent_outputs={}",
834            tx.inputs().len(),
835            spent_outputs.len(),
836        )));
837    }
838
839    for (input_index, input) in tx.inputs().iter().enumerate() {
840        let unlock_script = match input {
841            transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
842            transparent::Input::Coinbase { .. } => continue,
843        };
844
845        // Rule: the scriptSig must be within the standard size limit.
846        let size = unlock_script.as_raw_bytes().len();
847        if size > MAX_STANDARD_SCRIPTSIG_SIZE {
848            return Err(TransactionError::NonStandardScriptSigSize { input_index, size });
849        }
850
851        // Rule: the scriptSig must be push-only.
852        if !script::Code(unlock_script.as_raw_bytes().to_vec()).is_push_only() {
853            return Err(TransactionError::NonStandardScriptSigNotPushOnly { input_index });
854        }
855    }
856
857    // Rule: all transparent inputs must pass `AreInputsStandard()` checks:
858    // https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L137
859    if !are_inputs_standard(tx, spent_outputs) {
860        return Err(TransactionError::NonStandardInputs);
861    }
862
863    Ok(())
864}
865
866/// Checks the `nConsensusBranchId` field.
867///
868/// # Consensus
869///
870/// ## [7.1.2 Transaction Consensus Rules]
871///
872/// > [**NU5** onward] If `effectiveVersion` ≥ 5, the `nConsensusBranchId` field **MUST** match the
873/// > consensus branch ID used for SIGHASH transaction hashes, as specified in [ZIP-244].
874///
875/// ### Notes
876///
877/// - When deserializing transactions, Zebra converts the `nConsensusBranchId` into
878///   [`NetworkUpgrade`].
879///
880/// - The values returned by [`Transaction::version`] match `effectiveVersion` so we use them in
881///   place of `effectiveVersion`. More details in [`Transaction::version`].
882///
883/// [ZIP-244]: <https://zips.z.cash/zip-0244>
884/// [7.1.2 Transaction Consensus Rules]: <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
885pub fn consensus_branch_id(
886    tx: &Transaction,
887    height: Height,
888    network: &Network,
889) -> Result<(), TransactionError> {
890    let current_nu = NetworkUpgrade::current(network, height);
891
892    if current_nu < NetworkUpgrade::Nu5 || tx.version() < 5 {
893        return Ok(());
894    }
895
896    let Some(tx_nu) = tx.network_upgrade() else {
897        return Err(TransactionError::MissingConsensusBranchId);
898    };
899
900    if tx_nu != current_nu {
901        return Err(TransactionError::WrongConsensusBranchId);
902    }
903
904    Ok(())
905}