1use 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#[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 pub(crate) fn inner(&self) -> &zp_tx::Transaction {
66 &self.0
67 }
68
69 pub fn tx_version(&self) -> TxVersion {
71 self.0.version()
72 }
73
74 #[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 pub fn is_overwintered(&self) -> bool {
89 !matches!(self.0.version(), TxVersion::Sprout(_))
90 }
91
92 #[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 _ => compat::branch_id_to_network_upgrade(self.0.consensus_branch_id()),
99 }
100 }
101
102 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 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 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 pub fn raw_lock_time(&self) -> u32 {
150 self.0.lock_time()
151 }
152
153 pub fn lock_time_is_time(&self) -> bool {
155 matches!(self.lock_time(), Some(LockTime::Time(_)))
156 }
157
158 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 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 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 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 pub fn has_transparent_inputs(&self) -> bool {
211 !self.inputs().is_empty()
212 }
213
214 pub fn has_transparent_outputs(&self) -> bool {
216 !self.outputs().is_empty()
217 }
218
219 pub fn has_transparent_inputs_or_outputs(&self) -> bool {
221 self.has_transparent_inputs() || self.has_transparent_outputs()
222 }
223
224 pub fn is_coinbase(&self) -> bool {
226 self.transparent_bundle().is_some_and(|b| b.is_coinbase())
227 }
228
229 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 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 pub fn hash(&self) -> Hash {
259 let txid_bytes: [u8; 32] = *self.0.txid().as_ref();
260 Hash(txid_bytes)
261 }
262
263 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 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 pub fn joinsplit_count(&self) -> usize {
291 self.sprout_bundle().map_or(0, |b| b.joinsplits.len())
292 }
293
294 pub fn has_sprout_joinsplit_data(&self) -> bool {
296 self.0.sprout_bundle().is_some()
297 }
298
299 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 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 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 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 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 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 pub fn has_sapling_shielded_data(&self) -> bool {
357 self.0.sapling_bundle().is_some()
358 }
359
360 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 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 pub fn sapling_spends_count(&self) -> usize {
384 self.sapling_bundle()
385 .map_or(0, |b| b.shielded_spends().len())
386 }
387
388 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 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 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 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 pub fn has_orchard_shielded_data(&self) -> bool {
435 self.0.orchard_bundle().is_some()
436 }
437
438 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 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 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 pub fn orchard_flags(&self) -> Option<::orchard::bundle::Flags> {
471 self.0.orchard_bundle().map(|b| *b.flags())
472 }
473
474 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 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 pub fn has_ironwood_shielded_data(&self) -> bool {
505 self.0.ironwood_bundle().is_some()
506 }
507
508 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 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 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 pub fn ironwood_flags(&self) -> Option<::orchard::bundle::Flags> {
540 self.0.ironwood_bundle().map(|b| *b.flags())
541 }
542
543 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 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 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 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 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 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 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 pub fn has_shielded_data(&self) -> bool {
639 self.has_shielded_inputs() || self.has_shielded_outputs()
640 }
641
642 pub fn has_transparent_or_shielded_inputs(&self) -> bool {
644 self.has_transparent_inputs() || self.has_shielded_inputs()
645 }
646
647 pub fn has_transparent_or_shielded_outputs(&self) -> bool {
649 self.has_transparent_outputs() || self.has_shielded_outputs()
650 }
651
652 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 #[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 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 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 pub fn value_balance(
720 &self,
721 utxos: &std::collections::HashMap<transparent::OutPoint, transparent::Utxo>,
722 ) -> Result<ValueBalance<NegativeAllowed>, crate::value_balance::ValueBalanceError> {
723 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 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 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 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 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
883fn 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 let mut limited = reader.take(crate::block::MAX_BLOCK_BYTES);
894
895 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 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 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 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 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
979fn v4_empty_sapling_value_balance(raw_bytes: &[u8], inner: &zp_tx::Transaction) -> Option<i64> {
999 const V4_JOINSPLIT_SIZE: usize = (2 * 8) + (9 * 32) + 192 + (2 * 601);
1002
1003 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 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 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 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
1041struct 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#[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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 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 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 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 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 pub fn set_outputs(&mut self, outputs: Vec<transparent::Output>) {
1486 *self = self.clone().with_transparent_outputs(outputs);
1487 }
1488
1489 #[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 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()); bytes.extend_from_slice(&0x892F_2085u32.to_le_bytes()); bytes.push(0x00); bytes.push(0x00); bytes.extend_from_slice(&500_000_000u32.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0i64.to_le_bytes()); bytes.push(0x00); bytes.push(0x00); 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); }
1539 Transaction::zcash_deserialize(bytes.as_slice())
1540 .expect("manually constructed V4 transaction should deserialize")
1541 }
1542}