Skip to main content

zebra_chain/
transaction.rs

1//! Transactions and transaction-related structures.
2
3use std::fmt;
4
5pub use zcash_primitives::transaction::TxVersion;
6
7use zcash_primitives::transaction::{self as zp_tx};
8use zcash_protocol::value::ZatBalance;
9
10mod auth_digest;
11pub(crate) mod compat;
12mod hash;
13mod joinsplit;
14mod lock_time;
15mod memo;
16mod serialize;
17mod sighash;
18mod unmined;
19
20#[cfg(any(test, feature = "proptest-impl"))]
21#[allow(clippy::unwrap_in_result)]
22pub mod arbitrary;
23#[cfg(test)]
24mod tests;
25
26pub use crate::sapling::FieldNotPresent;
27pub use auth_digest::AuthDigest;
28pub use compat::{sprout_joinsplit_key_proof_and_ciphertexts, SPROUT_CIPHERTEXT_SIZE};
29pub use hash::{Hash, WtxId};
30pub use joinsplit::JoinSplitData;
31pub use lock_time::LockTime;
32pub use memo::Memo;
33pub use serialize::{
34    SerializedTransaction, MIN_TRANSPARENT_TX_SIZE, MIN_TRANSPARENT_TX_V4_SIZE,
35    MIN_TRANSPARENT_TX_V5_SIZE,
36};
37pub use sighash::{HashType, SigHash, SigHasher};
38pub use unmined::{
39    zip317, UnminedTx, UnminedTxId, VerifiedUnminedTx, MEMPOOL_TRANSACTION_COST_THRESHOLD,
40};
41
42use crate::{
43    amount::{Amount, NegativeAllowed, NonNegative},
44    block,
45    parameters::NetworkUpgrade,
46    transparent,
47    value_balance::ValueBalance,
48    Error,
49};
50
51/// A Zcash transaction, wrapping `zcash_primitives::transaction::Transaction`.
52#[derive(Debug)]
53pub struct Transaction(pub(crate) zp_tx::Transaction);
54
55impl std::ops::Deref for Transaction {
56    type Target = zp_tx::TransactionData<zp_tx::Authorized>;
57
58    fn deref(&self) -> &Self::Target {
59        &self.0
60    }
61}
62
63impl Transaction {
64    /// Access the inner `zcash_primitives::transaction::Transaction`.
65    pub(crate) fn inner(&self) -> &zp_tx::Transaction {
66        &self.0
67    }
68
69    /// Returns the transaction version.
70    pub fn tx_version(&self) -> TxVersion {
71        self.0.version()
72    }
73
74    /// Returns the numeric version of this transaction.
75    #[allow(unreachable_patterns)]
76    pub fn version(&self) -> u32 {
77        match self.0.version() {
78            TxVersion::Sprout(v) => v,
79            TxVersion::V3 => 3,
80            TxVersion::V4 => 4,
81            TxVersion::V5 => 5,
82            TxVersion::V6 => 6,
83            _ => panic!("unsupported transaction version"),
84        }
85    }
86
87    /// Returns `true` if this is an overwinter or later transaction.
88    pub fn is_overwintered(&self) -> bool {
89        !matches!(self.0.version(), TxVersion::Sprout(_))
90    }
91
92    /// Get the network upgrade for this transaction, if any (V5+).
93    #[allow(unreachable_patterns)]
94    pub fn network_upgrade(&self) -> Option<NetworkUpgrade> {
95        match self.tx_version() {
96            TxVersion::Sprout(_) | TxVersion::V3 | TxVersion::V4 => None,
97            // V5+ transactions embed the consensus branch ID
98            _ => compat::branch_id_to_network_upgrade(self.0.consensus_branch_id()),
99        }
100    }
101
102    /// Compute the sighash for this transaction.
103    ///
104    /// Returns an error if `network_upgrade` doesn't match the transaction's consensus branch ID.
105    pub fn sighash(
106        &self,
107        network_upgrade: NetworkUpgrade,
108        hash_type: sighash::HashType,
109        all_previous_outputs: std::sync::Arc<Vec<transparent::Output>>,
110        input_index_script_code: Option<(usize, Vec<u8>)>,
111    ) -> Result<sighash::SigHash, Error> {
112        let hasher = sighash::SigHasher::new(self, network_upgrade, all_previous_outputs)?;
113        Ok(hasher.sighash(hash_type, input_index_script_code))
114    }
115
116    /// Returns a [`SigHasher`] for this transaction.
117    ///
118    /// Returns an error if `network_upgrade` doesn't match the transaction's consensus branch ID.
119    pub fn sighasher(
120        &self,
121        network_upgrade: NetworkUpgrade,
122        all_previous_outputs: std::sync::Arc<Vec<transparent::Output>>,
123    ) -> Result<sighash::SigHasher, Error> {
124        sighash::SigHasher::new(self, network_upgrade, all_previous_outputs)
125    }
126
127    /// Get this transaction's lock time.
128    pub fn lock_time(&self) -> Option<LockTime> {
129        let lock_time = compat::u32_to_lock_time(self.0.lock_time());
130
131        if lock_time == LockTime::unlocked() {
132            return None;
133        }
134
135        let has_sequence_number_enabling_lock_time = self
136            .inputs()
137            .iter()
138            .map(transparent::Input::sequence)
139            .any(|seq| seq != u32::MAX);
140
141        if has_sequence_number_enabling_lock_time {
142            Some(lock_time)
143        } else {
144            None
145        }
146    }
147
148    /// Get the raw lock time value as a `u32`.
149    pub fn raw_lock_time(&self) -> u32 {
150        self.0.lock_time()
151    }
152
153    /// Returns `true` if `lock_time` is a [`LockTime::Time`] and is not disabled by sequence numbers.
154    pub fn lock_time_is_time(&self) -> bool {
155        matches!(self.lock_time(), Some(LockTime::Time(_)))
156    }
157
158    /// Get the expiry height for this transaction, if any (V3+).
159    ///
160    /// Returns `None` if the transaction is Sprout, or if `nExpiryHeight == 0`
161    /// (which means "no expiry" per the Zcash protocol spec).
162    ///
163    /// Returns the raw wire value, which can exceed [`block::Height::MAX`]: the ZIP-203
164    /// maximum of 499,999,999 is a verifier rule, not a limit of this accessor, so an
165    /// out-of-range value must reach the verifier to be rejected.
166    pub fn expiry_height(&self) -> Option<block::Height> {
167        match self.tx_version() {
168            TxVersion::Sprout(_) => None,
169            _ => match u32::from(self.0.expiry_height()) {
170                0 => None,
171                raw => Some(block::Height(raw)),
172            },
173        }
174    }
175
176    /// Get the version group ID for this transaction, if any.
177    pub fn version_group_id(&self) -> Option<u32> {
178        match self.tx_version() {
179            TxVersion::Sprout(_) => None,
180            v => Some(v.version_group_id()),
181        }
182    }
183
184    /// Get the transparent inputs, converted to Zebra types.
185    pub fn inputs(&self) -> Vec<transparent::Input> {
186        let bundle = self.0.transparent_bundle();
187        match bundle {
188            Some(b) => b
189                .vin
190                .iter()
191                .map(|txin| {
192                    compat::txin_to_input(txin)
193                        .expect("librustzcash TxIn should be convertible to Zebra Input")
194                })
195                .collect(),
196            None => Vec::new(),
197        }
198    }
199
200    /// Get the transparent outputs, converted to Zebra types.
201    pub fn outputs(&self) -> Vec<transparent::Output> {
202        let bundle = self.0.transparent_bundle();
203        match bundle {
204            Some(b) => b.vout.iter().map(compat::txout_to_output).collect(),
205            None => Vec::new(),
206        }
207    }
208
209    /// Returns `true` if this transaction has transparent inputs.
210    pub fn has_transparent_inputs(&self) -> bool {
211        !self.inputs().is_empty()
212    }
213
214    /// Returns `true` if this transaction has transparent outputs.
215    pub fn has_transparent_outputs(&self) -> bool {
216        !self.outputs().is_empty()
217    }
218
219    /// Returns `true` if this transaction has transparent inputs or outputs.
220    pub fn has_transparent_inputs_or_outputs(&self) -> bool {
221        self.has_transparent_inputs() || self.has_transparent_outputs()
222    }
223
224    /// Returns `true` if this is a coinbase transaction.
225    pub fn is_coinbase(&self) -> bool {
226        self.transparent_bundle().is_some_and(|b| b.is_coinbase())
227    }
228
229    /// Returns `true` if this transaction has valid inputs for a non-coinbase
230    /// transaction, that is, none of its transparent inputs has a null prevout.
231    ///
232    /// # Consensus
233    ///
234    /// > A transparent input in a non-coinbase transaction MUST NOT have a null prevout.
235    ///
236    /// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
237    ///
238    /// Note that a transaction can return `false` from both [`Transaction::is_coinbase`] and
239    /// this method, for example a transaction with a null-prevout input alongside other
240    /// inputs. Such transactions are rejected by the verifier.
241    pub fn is_valid_non_coinbase(&self) -> bool {
242        self.transparent_bundle().is_none_or(|bundle| {
243            bundle
244                .vin
245                .iter()
246                .all(|txin| *txin.prevout() != zcash_transparent::bundle::OutPoint::NULL)
247        })
248    }
249
250    /// Returns the outpoints spent by this transaction's transparent inputs.
251    pub fn spent_outpoints(&self) -> impl Iterator<Item = transparent::OutPoint> + '_ {
252        self.inputs()
253            .into_iter()
254            .filter_map(|input| input.outpoint())
255    }
256
257    /// Compute the hash (txid) of this transaction.
258    pub fn hash(&self) -> Hash {
259        let txid_bytes: [u8; 32] = *self.0.txid().as_ref();
260        Hash(txid_bytes)
261    }
262
263    /// Compute the authorizing data commitment for this transaction.
264    ///
265    /// Returns `None` for pre-V5 transactions (which don't have auth digests).
266    pub fn auth_digest(&self) -> Option<AuthDigest> {
267        match self.tx_version() {
268            TxVersion::Sprout(_) | TxVersion::V3 | TxVersion::V4 => None,
269            _ => {
270                let hash = self.0.auth_commitment();
271                let bytes: &[u8] = hash.as_ref();
272                let digest_bytes: [u8; 32] = bytes.try_into().ok()?;
273                Some(AuthDigest(digest_bytes))
274            }
275        }
276    }
277
278    /// Compute the unmined transaction ID for this transaction.
279    pub fn unmined_id(&self) -> UnminedTxId {
280        match self.auth_digest() {
281            Some(auth_digest) => UnminedTxId::Witnessed(WtxId {
282                id: self.hash(),
283                auth_digest,
284            }),
285            None => UnminedTxId::Legacy(self.hash()),
286        }
287    }
288
289    /// Returns the number of JoinSplit descriptions in this transaction.
290    pub fn joinsplit_count(&self) -> usize {
291        self.sprout_bundle().map_or(0, |b| b.joinsplits.len())
292    }
293
294    /// Returns `true` if this transaction has Sprout JoinSplit data.
295    pub fn has_sprout_joinsplit_data(&self) -> bool {
296        self.0.sprout_bundle().is_some()
297    }
298
299    /// Iterate over the Sprout JoinSplit descriptions (librustzcash type).
300    pub fn sprout_joinsplit_descriptions(
301        &self,
302    ) -> impl Iterator<Item = &zcash_primitives::transaction::components::sprout::JsDescription> + '_
303    {
304        self.sprout_bundle()
305            .into_iter()
306            .flat_map(|b| b.joinsplits.iter())
307    }
308
309    /// Access the Sprout nullifiers in this transaction.
310    pub fn sprout_nullifiers(&self) -> impl Iterator<Item = crate::sprout::Nullifier> + '_ {
311        self.sprout_bundle()
312            .into_iter()
313            .flat_map(|b| b.joinsplits.iter())
314            .flat_map(|js| js.nullifiers().iter().copied())
315            .map(crate::sprout::Nullifier::from)
316    }
317
318    /// Access the Sprout note commitments in this transaction.
319    pub fn sprout_note_commitments(
320        &self,
321    ) -> impl Iterator<Item = crate::sprout::commitment::NoteCommitment> + '_ {
322        self.sprout_bundle()
323            .into_iter()
324            .flat_map(|b| b.joinsplits.iter())
325            .flat_map(|js| js.commitments().iter().copied())
326            .map(crate::sprout::commitment::NoteCommitment::from)
327    }
328
329    /// Returns vpub_old values (amounts entering the Sprout pool).
330    pub fn output_values_to_sprout(&self) -> Vec<i64> {
331        self.sprout_bundle()
332            .into_iter()
333            .flat_map(|b| b.joinsplits.iter())
334            .map(|js| js.vpub_old().into())
335            .collect()
336    }
337
338    /// Returns vpub_new values (amounts leaving the Sprout pool).
339    pub fn input_values_from_sprout(&self) -> Vec<i64> {
340        self.sprout_bundle()
341            .into_iter()
342            .flat_map(|b| b.joinsplits.iter())
343            .map(|js| js.vpub_new().into())
344            .collect()
345    }
346
347    /// Access the JoinSplit public validating key, if any.
348    pub fn sprout_joinsplit_pub_key(
349        &self,
350    ) -> Option<crate::primitives::ed25519::VerificationKeyBytes> {
351        self.sprout_bundle()
352            .map(|b| crate::primitives::ed25519::VerificationKeyBytes::from(b.joinsplit_pubkey))
353    }
354
355    /// Returns `true` if this transaction has Sapling shielded data.
356    pub fn has_sapling_shielded_data(&self) -> bool {
357        self.0.sapling_bundle().is_some()
358    }
359
360    /// Access the Sapling nullifiers in this transaction.
361    pub fn sapling_nullifiers(&self) -> impl Iterator<Item = crate::sapling::Nullifier> + '_ {
362        self.sapling_bundle()
363            .into_iter()
364            .flat_map(|b| b.shielded_spends().iter())
365            .map(|spend| crate::sapling::Nullifier::from(spend.nullifier().0))
366    }
367
368    /// Access the Sapling spend descriptions (librustzcash type).
369    ///
370    /// The spend description type uses `GrothProofBytes` for proofs and
371    /// `redjubjub::Signature<SpendAuth>` for auth sigs.
372    pub fn sapling_spends(
373        &self,
374    ) -> impl Iterator<
375        Item = &sapling_crypto::bundle::SpendDescription<sapling_crypto::bundle::Authorized>,
376    > + '_ {
377        self.sapling_bundle()
378            .into_iter()
379            .flat_map(|b| b.shielded_spends().iter())
380    }
381
382    /// Returns the number of Sapling spends.
383    pub fn sapling_spends_count(&self) -> usize {
384        self.sapling_bundle()
385            .map_or(0, |b| b.shielded_spends().len())
386    }
387
388    /// Access the Sapling output descriptions (librustzcash type).
389    pub fn sapling_outputs(
390        &self,
391    ) -> impl Iterator<
392        Item = &sapling_crypto::bundle::OutputDescription<sapling_crypto::bundle::GrothProofBytes>,
393    > + '_ {
394        self.sapling_bundle()
395            .into_iter()
396            .flat_map(|b| b.shielded_outputs().iter())
397    }
398
399    /// Access the Sapling note commitments in this transaction.
400    pub fn sapling_note_commitments(
401        &self,
402    ) -> impl Iterator<Item = sapling_crypto::note::ExtractedNoteCommitment> + '_ {
403        self.sapling_outputs().map(|output| *output.cmu())
404    }
405
406    /// Iterate over deduplicated Sapling anchors as zebra tree roots.
407    pub fn sapling_anchors(&self) -> Vec<crate::sapling::tree::Root> {
408        let mut seen = Vec::new();
409        for spend in self.sapling_spends() {
410            let bytes = spend.anchor().to_bytes();
411            let root = crate::sapling::tree::Root::try_from(bytes)
412                .expect("sapling anchor from valid transaction should be a valid tree root");
413            if !seen.contains(&root) {
414                seen.push(root);
415            }
416        }
417        seen
418    }
419
420    /// Get the Sapling value balance.
421    pub fn sapling_value_balance(&self) -> ValueBalance<NegativeAllowed> {
422        let balance = self
423            .sapling_bundle()
424            .map(|b| *b.value_balance())
425            .unwrap_or(ZatBalance::zero());
426
427        let amount: Amount<NegativeAllowed> = Amount::try_from(i64::from(balance))
428            .expect("sapling value balance should be a valid Amount");
429
430        ValueBalance::from_sapling_amount(amount)
431    }
432
433    /// Returns `true` if this transaction has Orchard shielded data.
434    pub fn has_orchard_shielded_data(&self) -> bool {
435        self.0.orchard_bundle().is_some()
436    }
437
438    /// Access the Orchard nullifiers in this transaction.
439    pub fn orchard_nullifiers(&self) -> impl Iterator<Item = crate::orchard::Nullifier> + '_ {
440        self.orchard_bundle()
441            .into_iter()
442            .flat_map(|b| b.actions().iter())
443            .map(|action| {
444                crate::orchard::Nullifier::try_from(action.nullifier().to_bytes())
445                    .expect("orchard nullifier from valid transaction")
446            })
447    }
448
449    /// Access the Orchard actions (librustzcash type).
450    pub fn orchard_actions(
451        &self,
452    ) -> impl Iterator<
453        Item = &::orchard::Action<
454            <::orchard::bundle::Authorized as ::orchard::bundle::Authorization>::SpendAuth,
455        >,
456    > + '_ {
457        self.orchard_bundle()
458            .into_iter()
459            .flat_map(|b| b.actions().iter())
460    }
461
462    /// Access Orchard note commitments.
463    pub fn orchard_note_commitments(
464        &self,
465    ) -> impl Iterator<Item = ::orchard::note::ExtractedNoteCommitment> + '_ {
466        self.orchard_actions().map(|action| *action.cmx())
467    }
468
469    /// Access the Orchard flags, if any.
470    pub fn orchard_flags(&self) -> Option<::orchard::bundle::Flags> {
471        self.0.orchard_bundle().map(|b| *b.flags())
472    }
473
474    /// Access the Orchard anchor as a zebra tree root, if any.
475    pub fn orchard_anchor(&self) -> Option<crate::orchard::tree::Root> {
476        self.0.orchard_bundle().and_then(|b| {
477            let bytes = b.anchor().to_bytes();
478            crate::orchard::tree::Root::try_from(bytes).ok()
479        })
480    }
481
482    /// Get the Orchard value balance.
483    pub fn orchard_value_balance(&self) -> ValueBalance<NegativeAllowed> {
484        let balance = self
485            .orchard_bundle()
486            .map(|b| *b.value_balance())
487            .unwrap_or(ZatBalance::zero());
488
489        let amount: Amount<NegativeAllowed> = Amount::try_from(i64::from(balance))
490            .expect("orchard value balance should be a valid Amount");
491
492        ValueBalance::from_orchard_amount(amount)
493    }
494
495    // Ironwood (NU6.3 onward).
496    //
497    // Ironwood reuses the Orchard action and proof system, so `zcash_primitives` models it with
498    // the same `orchard::Bundle` type, kept in a separate `ironwood_bundle` field. These
499    // accessors mirror the Orchard ones above, but convert into Zebra's type-distinct
500    // `crate::ironwood` newtypes so the two pools' nullifiers and note commitments can never be
501    // interchanged.
502
503    /// Returns `true` if this transaction has an Ironwood bundle.
504    pub fn has_ironwood_shielded_data(&self) -> bool {
505        self.0.ironwood_bundle().is_some()
506    }
507
508    /// Access the Ironwood nullifiers in this transaction.
509    pub fn ironwood_nullifiers(&self) -> impl Iterator<Item = crate::ironwood::Nullifier> + '_ {
510        self.ironwood_actions().map(|action| {
511            let nullifier = crate::orchard::Nullifier::try_from(action.nullifier().to_bytes())
512                .expect("ironwood nullifier from valid transaction");
513            crate::ironwood::Nullifier::from(nullifier)
514        })
515    }
516
517    /// Access the Ironwood actions (librustzcash type).
518    pub fn ironwood_actions(
519        &self,
520    ) -> impl Iterator<
521        Item = &::orchard::Action<
522            <::orchard::bundle::Authorized as ::orchard::bundle::Authorization>::SpendAuth,
523        >,
524    > + '_ {
525        self.0
526            .ironwood_bundle()
527            .into_iter()
528            .flat_map(|b| b.actions().iter())
529    }
530
531    /// Access Ironwood note commitments.
532    pub fn ironwood_note_commitments(
533        &self,
534    ) -> impl Iterator<Item = ::orchard::note::ExtractedNoteCommitment> + '_ {
535        self.ironwood_actions().map(|action| *action.cmx())
536    }
537
538    /// Access the Ironwood flags, if any.
539    pub fn ironwood_flags(&self) -> Option<::orchard::bundle::Flags> {
540        self.0.ironwood_bundle().map(|b| *b.flags())
541    }
542
543    /// Access the Ironwood anchor as a zebra tree root, if any.
544    pub fn ironwood_anchor(&self) -> Option<crate::orchard::tree::Root> {
545        self.0.ironwood_bundle().and_then(|b| {
546            let bytes = b.anchor().to_bytes();
547            crate::orchard::tree::Root::try_from(bytes).ok()
548        })
549    }
550
551    /// Returns `true` unless this transaction has an Ironwood bundle that enables neither
552    /// spends nor outputs.
553    pub fn has_enough_ironwood_flags(&self) -> bool {
554        if !self.has_ironwood_shielded_data() {
555            return true;
556        }
557
558        self.ironwood_flags()
559            .is_some_and(|flags| flags.spends_enabled() || flags.outputs_enabled())
560    }
561
562    /// Get the Ironwood value balance.
563    ///
564    /// Positive values are added to this transaction's value pool, and removed from the Ironwood
565    /// chain value pool. Negative values are removed from this transaction, and added to the
566    /// Ironwood pool. This is zero for transactions without an Ironwood bundle.
567    ///
568    /// <https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions>
569    pub fn ironwood_value_balance(&self) -> ValueBalance<NegativeAllowed> {
570        let balance = self
571            .0
572            .ironwood_bundle()
573            .map(|b| *b.value_balance())
574            .unwrap_or(ZatBalance::zero());
575
576        let amount: Amount<NegativeAllowed> = Amount::try_from(i64::from(balance))
577            .expect("ironwood value balance should be a valid Amount");
578
579        ValueBalance::from_ironwood_amount(amount)
580    }
581
582    /// Returns `true` if the Orchard bundle's Halo2 proof has the canonical size for its
583    /// action count, or if there is no Orchard bundle.
584    ///
585    /// A proof that is present but not canonically sized can be padded with arbitrary trailing
586    /// data without affecting its validity (GHSA-jfw5-j458-pfv6). Bundles are deserialized
587    /// leniently, so this is checked by the verifier rather than during parsing.
588    pub fn orchard_proof_size_is_canonical(&self) -> bool {
589        self.0.orchard_bundle().is_none_or(|bundle| {
590            bundle.authorization().proof().as_ref().len()
591                == crate::orchard::shielded_data::expected_proof_size(bundle.actions().len())
592        })
593    }
594
595    /// Returns `true` if the Ironwood bundle's Halo2 proof has the canonical size for its
596    /// action count, or if there is no Ironwood bundle.
597    ///
598    /// See [`Self::orchard_proof_size_is_canonical`]; Ironwood reuses the Orchard circuit, so it
599    /// has the same expected proof size.
600    pub fn ironwood_proof_size_is_canonical(&self) -> bool {
601        self.0.ironwood_bundle().is_none_or(|bundle| {
602            bundle.authorization().proof().as_ref().len()
603                == crate::orchard::shielded_data::expected_proof_size(bundle.actions().len())
604        })
605    }
606
607    /// Returns `true` if this transaction has shielded inputs.
608    pub fn has_shielded_inputs(&self) -> bool {
609        self.has_sprout_joinsplit_data()
610            || self
611                .sapling_bundle()
612                .is_some_and(|b| !b.shielded_spends().is_empty())
613            || self
614                .orchard_bundle()
615                .is_some_and(|b| b.flags().spends_enabled() && !b.actions().is_empty())
616            || self
617                .0
618                .ironwood_bundle()
619                .is_some_and(|b| b.flags().spends_enabled() && !b.actions().is_empty())
620    }
621
622    /// Returns `true` if this transaction has shielded outputs.
623    pub fn has_shielded_outputs(&self) -> bool {
624        self.has_sprout_joinsplit_data()
625            || self
626                .sapling_bundle()
627                .is_some_and(|b| !b.shielded_outputs().is_empty())
628            || self
629                .orchard_bundle()
630                .is_some_and(|b| b.flags().outputs_enabled() && !b.actions().is_empty())
631            || self
632                .0
633                .ironwood_bundle()
634                .is_some_and(|b| b.flags().outputs_enabled() && !b.actions().is_empty())
635    }
636
637    /// Does this transaction have shielded inputs or outputs?
638    pub fn has_shielded_data(&self) -> bool {
639        self.has_shielded_inputs() || self.has_shielded_outputs()
640    }
641
642    /// Returns `true` if this transaction has transparent or shielded inputs.
643    pub fn has_transparent_or_shielded_inputs(&self) -> bool {
644        self.has_transparent_inputs() || self.has_shielded_inputs()
645    }
646
647    /// Returns `true` if this transaction has transparent or shielded outputs.
648    pub fn has_transparent_or_shielded_outputs(&self) -> bool {
649        self.has_transparent_outputs() || self.has_shielded_outputs()
650    }
651
652    /// Returns `true` if the Orchard flags are consistent.
653    pub fn has_enough_orchard_flags(&self) -> bool {
654        match self.0.orchard_bundle() {
655            Some(bundle) => {
656                let flags = bundle.flags();
657                flags.spends_enabled() || flags.outputs_enabled()
658            }
659            None => true,
660        }
661    }
662
663    /// Return the transparent value balance,
664    /// the change in the transaction value pool due to transparent inputs and outputs.
665    #[allow(clippy::unwrap_in_result)]
666    pub fn transparent_value_balance_from_outputs(
667        &self,
668        outputs: &std::collections::HashMap<transparent::OutPoint, transparent::Output>,
669    ) -> Result<ValueBalance<NegativeAllowed>, crate::value_balance::ValueBalanceError> {
670        use crate::amount::Error as AmountError;
671
672        let input_value = self
673            .inputs()
674            .iter()
675            .map(|i| i.value_from_outputs(outputs))
676            .sum::<Result<Amount<NonNegative>, AmountError>>()
677            .map_err(crate::value_balance::ValueBalanceError::Transparent)?
678            .constrain()
679            .expect("conversion from NonNegative to NegativeAllowed is always valid");
680
681        let output_value = self
682            .outputs()
683            .iter()
684            .map(|o| o.value())
685            .sum::<Result<Amount<NonNegative>, AmountError>>()
686            .map_err(crate::value_balance::ValueBalanceError::Transparent)?
687            .constrain()
688            .expect("conversion from NonNegative to NegativeAllowed is always valid");
689
690        (input_value - output_value)
691            .map(ValueBalance::from_transparent_amount)
692            .map_err(crate::value_balance::ValueBalanceError::Transparent)
693    }
694
695    /// Return the sprout value balance.
696    ///
697    /// Errors when the aggregate JoinSplit value balance (or any prefix of it, matching the
698    /// pre-refactor fold) leaves the valid monetary range. Aggregated here because
699    /// `zcash_primitives` collapses that case to `None`, conflating it with "no Sprout
700    /// bundle". The net-sum bound matches zcashd from Canopy onward, where `vpub_old` is
701    /// always zero; pre-Canopy heights are checkpointed.
702    pub fn sprout_value_balance(
703        &self,
704    ) -> Result<ValueBalance<NegativeAllowed>, crate::value_balance::ValueBalanceError> {
705        let total = self
706            .sprout_joinsplit_descriptions()
707            .try_fold(Amount::<NegativeAllowed>::zero(), |total, js| {
708                // Not `js.net_value()`: its `expect` trusts upstream parse bounds. The
709                // subtraction can't overflow `i64`, each `vpub` is at most `MAX_MONEY`.
710                let net = Amount::try_from(i64::from(js.vpub_new()) - i64::from(js.vpub_old()))?;
711                total + net
712            })
713            .map_err(crate::value_balance::ValueBalanceError::Sprout)?;
714
715        Ok(ValueBalance::from_sprout_amount(total))
716    }
717
718    /// Get the overall value balance for this transaction.
719    pub fn value_balance(
720        &self,
721        utxos: &std::collections::HashMap<transparent::OutPoint, transparent::Utxo>,
722    ) -> Result<ValueBalance<NegativeAllowed>, crate::value_balance::ValueBalanceError> {
723        // Collect only the outputs this transaction spends. Cloning the whole UTXO map here
724        // would make block validation quadratic in the number of transactions, since
725        // `remaining_transaction_value` calls this once per transaction.
726        let outputs: std::collections::HashMap<_, _> = self
727            .spent_outpoints()
728            .filter_map(|outpoint| {
729                utxos
730                    .get(&outpoint)
731                    .map(|utxo| (outpoint, utxo.output.clone()))
732            })
733            .collect();
734
735        let transparent = self.transparent_value_balance_from_outputs(&outputs)?;
736        let sprout = self.sprout_value_balance()?;
737        let sapling = self.sapling_value_balance();
738        let orchard = self.orchard_value_balance();
739        let ironwood = self.ironwood_value_balance();
740
741        transparent + sprout + sapling + orchard + ironwood
742    }
743
744    /// Returns the [`transparent::CoinbaseSpendRestriction`] for this transaction,
745    /// assuming it is mined at `spend_height`.
746    pub fn coinbase_spend_restriction(
747        &self,
748        network: &crate::parameters::Network,
749        spend_height: block::Height,
750    ) -> transparent::CoinbaseSpendRestriction {
751        if self.outputs().is_empty() || network.should_allow_unshielded_coinbase_spends() {
752            transparent::CoinbaseSpendRestriction::CheckCoinbaseMaturity { spend_height }
753        } else {
754            transparent::CoinbaseSpendRestriction::DisallowCoinbaseSpend
755        }
756    }
757}
758
759impl PartialEq for Transaction {
760    fn eq(&self, other: &Self) -> bool {
761        self.0.txid() == other.0.txid()
762    }
763}
764
765impl Eq for Transaction {}
766
767impl std::fmt::Display for Transaction {
768    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
769        let mut fmter = f.debug_struct("Transaction");
770
771        fmter.field("version", &self.version());
772
773        if let Some(network_upgrade) = self.network_upgrade() {
774            fmter.field("network_upgrade", &network_upgrade);
775        }
776
777        if let Some(lock_time) = self.lock_time() {
778            fmter.field("lock_time", &lock_time);
779        }
780
781        if let Some(expiry_height) = self.expiry_height() {
782            fmter.field("expiry_height", &expiry_height);
783        }
784
785        fmter.field("transparent_inputs", &self.inputs().len());
786        fmter.field("transparent_outputs", &self.outputs().len());
787        fmter.field("sprout_joinsplits", &self.joinsplit_count());
788        fmter.field("sapling_spends", &self.sapling_spends_count());
789        fmter.field("sapling_outputs", &self.sapling_outputs().count());
790        fmter.field("orchard_actions", &self.orchard_actions().count());
791        fmter.field("ironwood_actions", &self.ironwood_actions().count());
792
793        fmter.field("unmined_id", &self.unmined_id());
794
795        fmter.finish()
796    }
797}
798
799impl From<&Transaction> for Hash {
800    fn from(transaction: &Transaction) -> Self {
801        transaction.hash()
802    }
803}
804
805impl From<std::sync::Arc<Transaction>> for Hash {
806    fn from(transaction: std::sync::Arc<Transaction>) -> Self {
807        transaction.hash()
808    }
809}
810
811impl From<&Transaction> for UnminedTxId {
812    fn from(transaction: &Transaction) -> Self {
813        transaction.unmined_id()
814    }
815}
816
817impl From<std::sync::Arc<Transaction>> for UnminedTxId {
818    fn from(transaction: std::sync::Arc<Transaction>) -> Self {
819        transaction.unmined_id()
820    }
821}
822
823impl TryFrom<&Transaction> for AuthDigest {
824    type Error = &'static str;
825
826    /// Computes the authorizing data commitment for a transaction.
827    ///
828    /// Returns an error if passed a pre-V5 transaction (which has no auth digest).
829    fn try_from(transaction: &Transaction) -> Result<Self, Self::Error> {
830        transaction
831            .auth_digest()
832            .ok_or("pre-V5 transactions do not have an auth digest")
833    }
834}
835
836impl crate::serialization::ZcashSerialize for Transaction {
837    fn zcash_serialize<W: std::io::Write>(&self, writer: W) -> Result<(), std::io::Error> {
838        self.0.write(writer)
839    }
840}
841
842impl crate::serialization::ZcashDeserializeWithContext<zcash_protocol::consensus::BranchId>
843    for Transaction
844{
845    /// Deserialize a transaction with a known consensus branch ID.
846    ///
847    /// Runs the same parse-time consensus checks as
848    /// [`Transaction::zcash_deserialize`](crate::serialization::ZcashDeserialize::zcash_deserialize).
849    fn zcash_deserialize_with_context<R: std::io::Read>(
850        reader: R,
851        &branch_id: &zcash_protocol::consensus::BranchId,
852    ) -> Result<Self, crate::serialization::SerializationError> {
853        deserialize_and_check(reader, branch_id)
854    }
855}
856
857impl crate::serialization::ZcashDeserialize for Transaction {
858    /// Deserialize a transaction without network context.
859    ///
860    /// # Branch ID handling
861    ///
862    /// - **V5+ transactions**: the consensus branch ID is read from the wire. Correct.
863    /// - **V1-V4 transactions**: the branch ID is NOT on the wire, so a default
864    ///   (`BranchId::Canopy`) is stored.  This does not affect parsing or txid
865    ///   computation, but the stored `consensus_branch_id` field will be wrong for
866    ///   transactions mined before Canopy.  Use
867    ///   `ZcashDeserializeWithContext<BranchId>` when the correct branch ID is known.
868    ///
869    /// # Callers
870    ///
871    /// Prefer `ZcashDeserializeWithContext<BranchId>` when the correct branch ID is
872    /// known.  This context-free impl exists for:
873    /// - Block deserialization (branch ID is corrected afterward)
874    /// - Network message parsing and RPC (transactions are re-validated with the
875    ///   correct branch ID before sighash computation)
876    fn zcash_deserialize<R: std::io::Read>(
877        reader: R,
878    ) -> Result<Self, crate::serialization::SerializationError> {
879        deserialize_and_check(reader, zcash_protocol::consensus::BranchId::Canopy)
880    }
881}
882
883/// Parses a transaction and runs the parse-time consensus checks that `zcash_primitives`
884/// does not enforce. Both deserialization impls go through this function, so a transaction
885/// cannot reach a [`Transaction`] value without passing the checks.
886fn deserialize_and_check<R: std::io::Read>(
887    reader: R,
888    branch_id: zcash_protocol::consensus::BranchId,
889) -> Result<Transaction, crate::serialization::SerializationError> {
890    use std::io::Read as _;
891
892    // Limit to MAX_BLOCK_BYTES: a transaction larger than a block is always invalid.
893    let mut limited = reader.take(crate::block::MAX_BLOCK_BYTES);
894
895    // Only V4 transactions need their bytes recorded, for the `valueBalanceSapling` check
896    // below. Reading the 4-byte header up front and putting it back lets every other
897    // version parse without copying the transaction a second time, which matters during
898    // the initial block download.
899    let mut header = [0u8; 4];
900    limited.read_exact(&mut header)?;
901    let is_v4 = {
902        let header = u32::from_le_bytes(header);
903        let overwintered = header & 0x8000_0000 != 0;
904        overwintered && (header & 0x7FFF_FFFF) == 4
905    };
906    let with_header = std::io::Read::chain(&header[..], limited);
907
908    let (inner, raw_bytes) = if is_v4 {
909        let mut recording = RecordingReader::new(with_header);
910        let inner = zp_tx::Transaction::read(&mut recording, branch_id)?;
911        (inner, recording.into_recorded())
912    } else {
913        (
914            zp_tx::Transaction::read(with_header, branch_id)?,
915            Vec::new(),
916        )
917    };
918
919    // Validate coinbase inputs: the script length must be in bounds and the height
920    // encoding must parse correctly. zcash_primitives accepts raw bytes without
921    // validating either, so we validate explicitly here to preserve Zebra's
922    // parse-time checks.
923    if let Some(bundle) = inner.transparent_bundle() {
924        for txin in &bundle.vin {
925            if *txin.prevout() == zcash_transparent::bundle::OutPoint::NULL {
926                let script_bytes = &txin.script_sig().0 .0;
927                // The genesis coinbase predates BIP-34: its 77-byte script has no height
928                // prefix, so skip the height parse, matching `compat::txin_to_input`.
929                if script_bytes.as_slice() != transparent::serialize::GENESIS_COINBASE_SCRIPT_SIG {
930                    transparent::serialize::parse_coinbase_height(script_bytes)?;
931                }
932            }
933        }
934    }
935
936    // # Consensus
937    //
938    // > A coinbase transaction MUST NOT have any Spend descriptions.
939    //
940    // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
941    //
942    // `zcash_primitives` does not enforce this while parsing, so it is checked here to keep
943    // Zebra's parse-time rejection (GHSA-rgwx-8r98-p34c). Upstream decodes spend descriptions
944    // one at a time rather than pre-allocating from the claimed count, so a rejected
945    // transaction cannot force an outsized allocation before reaching this check.
946    if inner
947        .transparent_bundle()
948        .is_some_and(|bundle| bundle.is_coinbase())
949        && inner
950            .sapling_bundle()
951            .is_some_and(|bundle| !bundle.shielded_spends().is_empty())
952    {
953        return Err(crate::serialization::SerializationError::Parse(
954            "coinbase transaction must not have Sapling spends",
955        ));
956    }
957
958    // # Consensus
959    //
960    // > [Sapling onward] If effectiveVersion < 5 and nSpendsSapling + nOutputsSapling > 0,
961    // > then valueBalanceSapling MUST be 0.
962    //
963    // <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
964    //
965    // `zcash_primitives` reads `valueBalanceSapling` but discards it when there are no
966    // Sapling spends or outputs, so the value is not recoverable from the parsed
967    // transaction and has to be read back out of the bytes that were consumed.
968    if inner.version() == TxVersion::V4 && inner.sapling_bundle().is_none() {
969        if let Some(value_balance) = v4_empty_sapling_value_balance(&raw_bytes, &inner) {
970            if value_balance != 0 {
971                return Err(crate::serialization::SerializationError::BadTransactionBalance);
972            }
973        }
974    }
975
976    Ok(Transaction(inner))
977}
978
979/// Reads the `valueBalanceSapling` field of a V4 transaction that has no Sapling spends or
980/// outputs, from the bytes that were consumed while parsing it.
981///
982/// Returns `None` if the bytes do not have the expected shape, in which case the caller should
983/// not treat the transaction as having a bad balance — a malformed encoding would already have
984/// failed to parse.
985///
986/// # Layout
987///
988/// The caller has established that the Sapling bundle is empty, which on the wire means
989/// `nSpendsSapling` and `nOutputsSapling` are both the single byte `0x00`, and that no
990/// `bindingSigSapling` follows. So the tail of a V4 transaction is:
991///
992/// ```text
993/// .. | valueBalanceSapling (8) | 0x00 | 0x00 | nJoinSplit | vJoinSplit.. | [pubkey | sig]
994/// ```
995///
996/// Locating the field from the end of the transaction avoids re-parsing the variable-length
997/// transparent inputs and outputs that precede it.
998fn v4_empty_sapling_value_balance(raw_bytes: &[u8], inner: &zp_tx::Transaction) -> Option<i64> {
999    /// The serialized size of one V4 JoinSplit description, which always carries a Groth16
1000    /// proof: two 8-byte values, nine 32-byte fields, the proof, and two note ciphertexts.
1001    const V4_JOINSPLIT_SIZE: usize = (2 * 8) + (9 * 32) + 192 + (2 * 601);
1002
1003    /// `joinSplitPubKey` and `joinSplitSig`, present only when there is at least one JoinSplit.
1004    const JOINSPLIT_AUTH_SIZE: usize = 32 + 64;
1005
1006    let joinsplit_count = inner
1007        .sprout_bundle()
1008        .map_or(0, |bundle| bundle.joinsplits.len());
1009
1010    // The CompactSize encoding of the JoinSplit count.
1011    let count_size: usize = match joinsplit_count {
1012        0..=252 => 1,
1013        253..=0xFFFF => 3,
1014        0x1_0000..=0xFFFF_FFFF => 5,
1015        _ => 9,
1016    };
1017
1018    let mut sprout_size =
1019        count_size.checked_add(joinsplit_count.checked_mul(V4_JOINSPLIT_SIZE)?)?;
1020    if joinsplit_count > 0 {
1021        sprout_size = sprout_size.checked_add(JOINSPLIT_AUTH_SIZE)?;
1022    }
1023
1024    // Step back over the Sprout section and the two zero Sapling counts.
1025    let counts_start = raw_bytes.len().checked_sub(sprout_size)?.checked_sub(2)?;
1026    let value_balance_start = counts_start.checked_sub(8)?;
1027
1028    // Confirm the offsets landed where expected before trusting the field.
1029    if raw_bytes.get(counts_start..counts_start + 2)? != [0x00, 0x00] {
1030        return None;
1031    }
1032
1033    let field: [u8; 8] = raw_bytes
1034        .get(value_balance_start..counts_start)?
1035        .try_into()
1036        .ok()?;
1037
1038    Some(i64::from_le_bytes(field))
1039}
1040
1041/// An `io::Read` wrapper that records every byte consumed, for post-parse validation.
1042struct RecordingReader<R> {
1043    inner: R,
1044    recorded: Vec<u8>,
1045}
1046
1047impl<R: std::io::Read> RecordingReader<R> {
1048    fn new(inner: R) -> Self {
1049        Self {
1050            inner,
1051            recorded: Vec::new(),
1052        }
1053    }
1054
1055    fn into_recorded(self) -> Vec<u8> {
1056        self.recorded
1057    }
1058}
1059
1060impl<R: std::io::Read> std::io::Read for RecordingReader<R> {
1061    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1062        let n = self.inner.read(buf)?;
1063        self.recorded.extend_from_slice(&buf[..n]);
1064        Ok(n)
1065    }
1066}
1067
1068impl Clone for Transaction {
1069    fn clone(&self) -> Self {
1070        Transaction(self.0.clone())
1071    }
1072}
1073
1074// Human-readable Serialize for elasticsearch, tests, and snapshots.
1075// Produces structured output matching the old Transaction enum format
1076// so that RON snapshot tests remain human-readable.
1077#[cfg(any(test, feature = "proptest-impl", feature = "elasticsearch"))]
1078impl serde::Serialize for Transaction {
1079    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1080        use serde::ser::SerializeStructVariant;
1081
1082        let version = self.version();
1083        // The field counts match the variants of the `Transaction` enum this output mirrors,
1084        // so V6 must be named and counted separately from V5: it has its own Ironwood bundle.
1085        let (variant_name, field_count) = match version {
1086            1 => ("V1", 3),
1087            2 => ("V2", 4),
1088            3 => ("V3", 5),
1089            4 => ("V4", 6),
1090            5 => ("V5", 7),
1091            _ => ("V6", 8),
1092        };
1093
1094        let mut sv = serializer.serialize_struct_variant(
1095            "Transaction",
1096            version.saturating_sub(1),
1097            variant_name,
1098            field_count,
1099        )?;
1100
1101        // V5+ has network_upgrade as the first field (unwrap since V5 always has one)
1102        if version >= 5 {
1103            let nu = self
1104                .network_upgrade()
1105                .unwrap_or(crate::parameters::NetworkUpgrade::Nu5);
1106            sv.serialize_field("network_upgrade", &nu)?;
1107        }
1108
1109        sv.serialize_field("lock_time", &compat::u32_to_lock_time(self.0.lock_time()))?;
1110
1111        // V3+ has expiry_height (use Height(0) when nExpiryHeight == 0, matching old format)
1112        if version >= 3 {
1113            let eh =
1114                compat::block_height_to_height(self.0.expiry_height()).unwrap_or(block::Height(0));
1115            sv.serialize_field("expiry_height", &eh)?;
1116        }
1117
1118        sv.serialize_field("inputs", &self.inputs())?;
1119        sv.serialize_field("outputs", &self.outputs())?;
1120
1121        if (2..=4).contains(&version) {
1122            let has_joinsplit = self.has_sprout_joinsplit_data();
1123            sv.serialize_field::<Option<()>>(
1124                "joinsplit_data",
1125                if has_joinsplit { &Some(()) } else { &None },
1126            )?;
1127        }
1128
1129        if version >= 4 {
1130            let has_sapling = self.has_sapling_shielded_data();
1131            sv.serialize_field::<Option<()>>(
1132                "sapling_shielded_data",
1133                if has_sapling { &Some(()) } else { &None },
1134            )?;
1135        }
1136
1137        if version >= 5 {
1138            let has_orchard = self.has_orchard_shielded_data();
1139            sv.serialize_field::<Option<()>>(
1140                "orchard_shielded_data",
1141                if has_orchard { &Some(()) } else { &None },
1142            )?;
1143        }
1144
1145        if version >= 6 {
1146            let has_ironwood = self.has_ironwood_shielded_data();
1147            sv.serialize_field::<Option<()>>(
1148                "ironwood_shielded_data",
1149                if has_ironwood { &Some(()) } else { &None },
1150            )?;
1151        }
1152
1153        sv.end()
1154    }
1155}
1156
1157#[cfg(any(test, feature = "proptest-impl"))]
1158impl Transaction {
1159    /// Build a V1 transaction from transparent components. Used in tests.
1160    pub fn test_v1(
1161        inputs: Vec<transparent::Input>,
1162        outputs: Vec<transparent::Output>,
1163        lock_time: LockTime,
1164    ) -> Self {
1165        Self::build_transparent(
1166            zcash_primitives::transaction::TxVersion::Sprout(1),
1167            zcash_protocol::consensus::BranchId::Sprout,
1168            compat::lock_time_to_u32(&lock_time),
1169            zcash_protocol::consensus::BlockHeight::from_u32(0),
1170            inputs,
1171            outputs,
1172        )
1173    }
1174
1175    /// Build a V2 transaction from transparent components. Used in tests.
1176    pub fn test_v2(
1177        inputs: Vec<transparent::Input>,
1178        outputs: Vec<transparent::Output>,
1179        lock_time: LockTime,
1180    ) -> Self {
1181        Self::build_transparent(
1182            zcash_primitives::transaction::TxVersion::Sprout(2),
1183            zcash_protocol::consensus::BranchId::Sprout,
1184            compat::lock_time_to_u32(&lock_time),
1185            zcash_protocol::consensus::BlockHeight::from_u32(0),
1186            inputs,
1187            outputs,
1188        )
1189    }
1190
1191    /// Build a V3 (Overwinter) transaction from transparent components. Used in tests.
1192    pub fn test_v3(
1193        inputs: Vec<transparent::Input>,
1194        outputs: Vec<transparent::Output>,
1195        lock_time: LockTime,
1196        expiry_height: block::Height,
1197    ) -> Self {
1198        Self::build_transparent(
1199            zcash_primitives::transaction::TxVersion::V3,
1200            zcash_protocol::consensus::BranchId::Overwinter,
1201            compat::lock_time_to_u32(&lock_time),
1202            compat::height_to_block_height(expiry_height),
1203            inputs,
1204            outputs,
1205        )
1206    }
1207
1208    /// Build a V4 (Sapling) transaction from transparent components. Used in tests.
1209    pub fn test_v4(
1210        inputs: Vec<transparent::Input>,
1211        outputs: Vec<transparent::Output>,
1212        lock_time: LockTime,
1213        expiry_height: block::Height,
1214    ) -> Self {
1215        Self::build_transparent(
1216            zcash_primitives::transaction::TxVersion::V4,
1217            zcash_protocol::consensus::BranchId::Canopy,
1218            compat::lock_time_to_u32(&lock_time),
1219            compat::height_to_block_height(expiry_height),
1220            inputs,
1221            outputs,
1222        )
1223    }
1224
1225    /// Build a V5 (NU5) transaction from transparent components. Used in tests.
1226    pub fn test_v5(
1227        network_upgrade: crate::parameters::NetworkUpgrade,
1228        inputs: Vec<transparent::Input>,
1229        outputs: Vec<transparent::Output>,
1230        lock_time: LockTime,
1231        expiry_height: block::Height,
1232    ) -> Self {
1233        let branch_id = network_upgrade
1234            .branch_id()
1235            .and_then(|cbid| zcash_protocol::consensus::BranchId::try_from(cbid).ok())
1236            .unwrap_or(zcash_protocol::consensus::BranchId::Nu5);
1237        Self::build_transparent(
1238            zcash_primitives::transaction::TxVersion::V5,
1239            branch_id,
1240            compat::lock_time_to_u32(&lock_time),
1241            compat::height_to_block_height(expiry_height),
1242            inputs,
1243            outputs,
1244        )
1245    }
1246
1247    /// Build a transparent-only V6 transaction, for tests.
1248    pub fn test_v6(
1249        network_upgrade: crate::parameters::NetworkUpgrade,
1250        inputs: Vec<transparent::Input>,
1251        outputs: Vec<transparent::Output>,
1252        lock_time: LockTime,
1253        expiry_height: block::Height,
1254    ) -> Self {
1255        let branch_id = network_upgrade
1256            .branch_id()
1257            .and_then(|cbid| zcash_protocol::consensus::BranchId::try_from(cbid).ok())
1258            .unwrap_or(zcash_protocol::consensus::BranchId::Nu6_3);
1259        Self::build_transparent(
1260            zcash_primitives::transaction::TxVersion::V6,
1261            branch_id,
1262            compat::lock_time_to_u32(&lock_time),
1263            compat::height_to_block_height(expiry_height),
1264            inputs,
1265            outputs,
1266        )
1267    }
1268
1269    fn build_transparent(
1270        version: zcash_primitives::transaction::TxVersion,
1271        branch_id: zcash_protocol::consensus::BranchId,
1272        lock_time: u32,
1273        expiry_height: zcash_protocol::consensus::BlockHeight,
1274        inputs: Vec<transparent::Input>,
1275        outputs: Vec<transparent::Output>,
1276    ) -> Self {
1277        let vin: Vec<_> = inputs.iter().map(compat::input_to_txin).collect();
1278        let vout: Vec<_> = outputs.iter().map(compat::output_to_txout).collect();
1279        let transparent_bundle = if vin.is_empty() && vout.is_empty() {
1280            None
1281        } else {
1282            Some(zcash_transparent::bundle::Bundle {
1283                vin,
1284                vout,
1285                authorization: zcash_transparent::bundle::Authorized,
1286            })
1287        };
1288        let tx_data = zp_tx::TransactionData::from_parts(
1289            version,
1290            branch_id,
1291            lock_time,
1292            expiry_height,
1293            transparent_bundle,
1294            None,
1295            None,
1296            None,
1297        );
1298        Transaction(tx_data.freeze().expect("built from valid components"))
1299    }
1300
1301    /// Build a V5 transaction with an Orchard bundle, for tests.
1302    ///
1303    /// The bundle must have been constructed for [`orchard::bundle::BundleVersion::orchard_v2`]
1304    /// or [`orchard::bundle::BundleVersion::orchard_v3`], matching `network_upgrade`.
1305    #[cfg(any(test, feature = "proptest-impl"))]
1306    pub fn test_v5_with_orchard(
1307        network_upgrade: crate::parameters::NetworkUpgrade,
1308        inputs: Vec<transparent::Input>,
1309        outputs: Vec<transparent::Output>,
1310        lock_time: LockTime,
1311        expiry_height: block::Height,
1312        orchard_bundle: Option<::orchard::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1313    ) -> Self {
1314        let branch_id = network_upgrade
1315            .branch_id()
1316            .and_then(|cbid| zcash_protocol::consensus::BranchId::try_from(cbid).ok())
1317            .unwrap_or(zcash_protocol::consensus::BranchId::Nu5);
1318
1319        let tx_data = zp_tx::TransactionData::from_parts(
1320            zp_tx::TxVersion::V5,
1321            branch_id,
1322            compat::lock_time_to_u32(&lock_time),
1323            compat::height_to_block_height(expiry_height),
1324            Self::transparent_bundle_from(inputs, outputs),
1325            None,
1326            None,
1327            orchard_bundle,
1328        );
1329
1330        Transaction(tx_data.freeze().expect("built from valid components"))
1331    }
1332
1333    /// Build a V6 transaction with Orchard and Ironwood bundles, for tests.
1334    ///
1335    /// `orchard_bundle` must have been constructed for
1336    /// [`orchard::bundle::BundleVersion::orchard_v3`] and `ironwood_bundle` for
1337    /// [`orchard::bundle::BundleVersion::ironwood_v3`]; the two slots are not interchangeable.
1338    #[cfg(any(test, feature = "proptest-impl"))]
1339    pub fn test_v6_with_bundles(
1340        network_upgrade: crate::parameters::NetworkUpgrade,
1341        inputs: Vec<transparent::Input>,
1342        outputs: Vec<transparent::Output>,
1343        lock_time: LockTime,
1344        expiry_height: block::Height,
1345        orchard_bundle: Option<::orchard::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1346        ironwood_bundle: Option<::orchard::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1347    ) -> Self {
1348        let branch_id = network_upgrade
1349            .branch_id()
1350            .and_then(|cbid| zcash_protocol::consensus::BranchId::try_from(cbid).ok())
1351            .unwrap_or(zcash_protocol::consensus::BranchId::Nu6_3);
1352
1353        let tx_data = zp_tx::TransactionData::from_parts_v6(
1354            branch_id,
1355            compat::lock_time_to_u32(&lock_time),
1356            compat::height_to_block_height(expiry_height),
1357            Self::transparent_bundle_from(inputs, outputs),
1358            None,
1359            orchard_bundle,
1360            ironwood_bundle,
1361        );
1362
1363        Transaction(tx_data.freeze().expect("built from valid components"))
1364    }
1365
1366    /// Converts Zebra transparent inputs and outputs into a librustzcash bundle,
1367    /// returning `None` if both are empty.
1368    #[cfg(any(test, feature = "proptest-impl"))]
1369    fn transparent_bundle_from(
1370        inputs: Vec<transparent::Input>,
1371        outputs: Vec<transparent::Output>,
1372    ) -> Option<zcash_transparent::bundle::Bundle<zcash_transparent::bundle::Authorized>> {
1373        let vin: Vec<_> = inputs.iter().map(compat::input_to_txin).collect();
1374        let vout: Vec<_> = outputs.iter().map(compat::output_to_txout).collect();
1375
1376        (!vin.is_empty() || !vout.is_empty()).then_some(zcash_transparent::bundle::Bundle {
1377            vin,
1378            vout,
1379            authorization: zcash_transparent::bundle::Authorized,
1380        })
1381    }
1382
1383    /// Rebuild this transaction with new transparent inputs.
1384    pub fn with_transparent_inputs(self, inputs: Vec<transparent::Input>) -> Self {
1385        let vin = inputs
1386            .iter()
1387            .map(crate::transaction::compat::input_to_txin)
1388            .collect();
1389        let vout = self
1390            .0
1391            .transparent_bundle()
1392            .map(|b| b.vout.clone())
1393            .unwrap_or_default();
1394        let transparent_bundle = Some(zcash_transparent::bundle::Bundle {
1395            vin,
1396            vout,
1397            authorization: zcash_transparent::bundle::Authorized,
1398        });
1399        self.rebuild_with_transparent(transparent_bundle)
1400    }
1401
1402    /// Rebuild this transaction with new transparent outputs.
1403    pub fn with_transparent_outputs(self, outputs: Vec<transparent::Output>) -> Self {
1404        let vin = self
1405            .0
1406            .transparent_bundle()
1407            .map(|b| b.vin.clone())
1408            .unwrap_or_default();
1409        let vout: Vec<_> = outputs
1410            .iter()
1411            .map(crate::transaction::compat::output_to_txout)
1412            .collect();
1413        let transparent_bundle = if vin.is_empty() && vout.is_empty() {
1414            None
1415        } else {
1416            Some(zcash_transparent::bundle::Bundle {
1417                vin,
1418                vout,
1419                authorization: zcash_transparent::bundle::Authorized,
1420            })
1421        };
1422        self.rebuild_with_transparent(transparent_bundle)
1423    }
1424
1425    fn rebuild_with_transparent(
1426        self,
1427        transparent_bundle: Option<
1428            zcash_transparent::bundle::Bundle<zcash_transparent::bundle::Authorized>,
1429        >,
1430    ) -> Self {
1431        let data = &*self.0;
1432        let tx_data = compat::transaction_data_from_parts(
1433            data.version(),
1434            data.consensus_branch_id(),
1435            data.lock_time(),
1436            data.expiry_height(),
1437            transparent_bundle,
1438            data.sprout_bundle().cloned(),
1439            data.sapling_bundle().cloned(),
1440            data.orchard_bundle().cloned(),
1441            data.ironwood_bundle().cloned(),
1442        );
1443        Transaction(tx_data.freeze().expect("rebuilt from valid transaction"))
1444    }
1445
1446    /// Rebuild this transaction with a different expiry height (recomputes txid).
1447    pub fn set_expiry_height(&mut self, height: block::Height) {
1448        let data = self.0.clone().into_data();
1449        let new_data = compat::transaction_data_from_parts(
1450            data.version(),
1451            data.consensus_branch_id(),
1452            data.lock_time(),
1453            compat::height_to_block_height(height),
1454            data.transparent_bundle().cloned(),
1455            data.sprout_bundle().cloned(),
1456            data.sapling_bundle().cloned(),
1457            data.orchard_bundle().cloned(),
1458            data.ironwood_bundle().cloned(),
1459        );
1460        self.0 = new_data.freeze().expect("rebuilt from valid transaction");
1461    }
1462
1463    /// Rebuild this transaction with a different network upgrade / branch ID (recomputes txid).
1464    pub fn set_network_upgrade(&mut self, nu: NetworkUpgrade) {
1465        let branch_id = nu
1466            .branch_id()
1467            .and_then(|cbid| zcash_protocol::consensus::BranchId::try_from(cbid).ok())
1468            .expect("network upgrade must have a valid branch ID");
1469        let data = self.0.clone().into_data();
1470        let new_data = compat::transaction_data_from_parts(
1471            data.version(),
1472            branch_id,
1473            data.lock_time(),
1474            data.expiry_height(),
1475            data.transparent_bundle().cloned(),
1476            data.sprout_bundle().cloned(),
1477            data.sapling_bundle().cloned(),
1478            data.orchard_bundle().cloned(),
1479            data.ironwood_bundle().cloned(),
1480        );
1481        self.0 = new_data.freeze().expect("rebuilt from valid transaction");
1482    }
1483
1484    /// Replace all transparent outputs (recomputes txid).
1485    pub fn set_outputs(&mut self, outputs: Vec<transparent::Output>) {
1486        *self = self.clone().with_transparent_outputs(outputs);
1487    }
1488
1489    /// Rebuild this transaction with a replaced Orchard bundle (recomputes txid).
1490    ///
1491    /// Test helper for synthesizing transactions with malformed orchard data
1492    /// (e.g. duplicated actions) that would otherwise be unreachable through
1493    /// normal construction paths.
1494    #[cfg(any(test, feature = "proptest-impl"))]
1495    pub fn with_orchard_bundle(
1496        self,
1497        bundle: Option<::orchard::Bundle<::orchard::bundle::Authorized, ZatBalance>>,
1498    ) -> Self {
1499        let data = &*self.0;
1500        let tx_data = compat::transaction_data_from_parts(
1501            data.version(),
1502            data.consensus_branch_id(),
1503            data.lock_time(),
1504            data.expiry_height(),
1505            data.transparent_bundle().cloned(),
1506            data.sprout_bundle().cloned(),
1507            data.sapling_bundle().cloned(),
1508            bundle,
1509            data.ironwood_bundle().cloned(),
1510        );
1511        Transaction(tx_data.freeze().expect("rebuilt from valid transaction"))
1512    }
1513
1514    /// Build a V4 transaction with optional JoinSplit data via byte-level serialization.
1515    ///
1516    /// Transparent inputs/outputs and sapling shielded data are empty.
1517    /// Used by tests that need V4 transactions with sprout data.
1518    pub fn test_v4_with_joinsplit_data(
1519        joinsplit_data: Option<&JoinSplitData<crate::primitives::Groth16Proof>>,
1520    ) -> Self {
1521        use crate::serialization::{ZcashDeserialize, ZcashSerialize};
1522
1523        let mut bytes: Vec<u8> = Vec::new();
1524        bytes.extend_from_slice(&0x8000_0004u32.to_le_bytes()); // V4 overwintered
1525        bytes.extend_from_slice(&0x892F_2085u32.to_le_bytes()); // Sapling versionGroupId
1526        bytes.push(0x00); // nTransparentInputs
1527        bytes.push(0x00); // nTransparentOutputs
1528        bytes.extend_from_slice(&500_000_000u32.to_le_bytes()); // nLockTime
1529        bytes.extend_from_slice(&0u32.to_le_bytes()); // nExpiryHeight
1530        bytes.extend_from_slice(&0i64.to_le_bytes()); // valueBalanceSapling
1531        bytes.push(0x00); // nSpendsSapling
1532        bytes.push(0x00); // nOutputsSapling
1533        if let Some(jsd) = joinsplit_data {
1534            jsd.zcash_serialize(&mut bytes)
1535                .expect("joinsplit_data serialization should succeed");
1536        } else {
1537            bytes.push(0x00); // nJoinSplits
1538        }
1539        Transaction::zcash_deserialize(bytes.as_slice())
1540            .expect("manually constructed V4 transaction should deserialize")
1541    }
1542}