Skip to main content

pczt/
lib.rs

1//! The Partially Created Zcash Transaction (PCZT) format.
2//!
3//! This format enables splitting up the logical steps of creating a Zcash transaction
4//! across distinct entities. The entity roles roughly match those specified in
5//! [BIP 174: Partially Signed Bitcoin Transaction Format] and [BIP 370: PSBT Version 2],
6//! with additional Zcash-specific roles.
7//!
8//! [BIP 174: Partially Signed Bitcoin Transaction Format]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
9//! [BIP 370: PSBT Version 2]: https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki
10//!
11#![cfg_attr(feature = "std", doc = "## Feature flags")]
12#![cfg_attr(feature = "std", doc = document_features::document_features!())]
13//!
14
15#![no_std]
16#![cfg_attr(docsrs, feature(doc_cfg))]
17#![cfg_attr(docsrs, doc(auto_cfg))]
18// Catch documentation errors caused by code changes.
19#![deny(rustdoc::broken_intra_doc_links)]
20
21#[macro_use]
22extern crate alloc;
23// The crate itself needs only `alloc`; the unit tests lean on `proptest`, which is a `std` crate.
24#[cfg(test)]
25extern crate std;
26
27use alloc::vec::Vec;
28
29use getset::Getters;
30
31use zcash_protocol::PoolType;
32#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
33use zcash_protocol::constants::{V6_TX_VERSION, V6_VERSION_GROUP_ID};
34#[cfg(all(
35    any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"),
36    zcash_unstable = "nu7",
37    feature = "zip-233",
38))]
39use zcash_protocol::value::Zatoshis;
40#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
41use {
42    common::{Global, determine_lock_time},
43    zcash_primitives::transaction::{Authorization, TransactionData, TxVersion},
44    zcash_protocol::{
45        consensus::{BranchId, OrchardProtocolRevision},
46        constants::{V5_TX_VERSION, V5_VERSION_GROUP_ID},
47    },
48};
49
50#[cfg(any(feature = "io-finalizer", feature = "signer"))]
51use zcash_primitives::transaction::sighash_v6::v6_signature_hash;
52#[cfg(any(feature = "io-finalizer", feature = "signer"))]
53use {
54    blake2b_simd::Hash as Blake2bHash,
55    zcash_primitives::transaction::{
56        TxDigests, sighash::SignableInput, sighash_v5::v5_signature_hash,
57    },
58};
59
60pub mod roles;
61
62pub mod common;
63pub mod orchard;
64pub mod sapling;
65pub mod transparent;
66
67pub(crate) const MAGIC_BYTES: &[u8; 4] = b"PCZT";
68pub(crate) const PCZT_VERSION_1: u32 = 1;
69pub(crate) const PCZT_VERSION_2: u32 = 2;
70
71const VERSIONED_HEADER_LEN: usize = 8;
72
73pub(crate) enum HeaderParseError {
74    InvalidMagic,
75    TooShort,
76}
77
78pub(crate) fn parse_header<'a>(
79    bytes: &'a [u8],
80    magic: &[u8; 4],
81) -> Result<(u32, &'a [u8]), HeaderParseError> {
82    if bytes.len() < VERSIONED_HEADER_LEN {
83        return Err(HeaderParseError::TooShort);
84    }
85    if &bytes[..4] != magic {
86        return Err(HeaderParseError::InvalidMagic);
87    }
88
89    let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
90    Ok((version, &bytes[VERSIONED_HEADER_LEN..]))
91}
92
93pub(crate) fn serialize_header(magic: &[u8; 4], version: u32) -> Vec<u8> {
94    let mut bytes = Vec::with_capacity(VERSIONED_HEADER_LEN);
95    bytes.extend_from_slice(magic);
96    bytes.extend_from_slice(&version.to_le_bytes());
97    bytes
98}
99
100/// Parses a PCZT from its encoding.
101pub fn parse(bytes: &[u8]) -> Result<Pczt, ParseError> {
102    Pczt::parse(bytes)
103}
104
105/// A partially-created Zcash transaction.
106#[derive(Clone, Debug, Getters)]
107pub struct Pczt {
108    /// Global fields that are relevant to the transaction as a whole.
109    #[getset(get = "pub")]
110    pub(crate) global: common::Global,
111
112    //
113    // Protocol-specific fields.
114    //
115    // Unlike the `TransactionData` type in `zcash_primitives`, these are not optional.
116    // This is because a PCZT does not always contain a semantically-valid transaction,
117    // and there may be phases where we need to store protocol-specific metadata before
118    // it has been determined whether there are protocol-specific inputs or outputs.
119    //
120    #[getset(get = "pub")]
121    pub(crate) transparent: transparent::Bundle,
122    #[getset(get = "pub")]
123    pub(crate) sapling: sapling::Bundle,
124    #[getset(get = "pub")]
125    pub(crate) orchard: orchard::Bundle,
126    #[getset(get = "pub")]
127    pub(crate) ironwood: orchard::Bundle,
128}
129
130/// Types and operations for the v1 Pczt encoding.
131pub mod v1 {
132    use alloc::vec::Vec;
133    use serde::{Deserialize, Serialize};
134
135    use crate::{common, orchard, sapling, transparent};
136
137    /// The in-memory type used for derived serialization of the v1 Pczt encoding.
138    #[derive(Clone, Debug, Serialize, Deserialize)]
139    pub struct Pczt {
140        global: common::Global,
141        transparent: transparent::Bundle,
142        sapling: sapling::v1::Bundle,
143        orchard: orchard::v1::Bundle,
144    }
145
146    impl Pczt {
147        pub fn serialize(&self) -> Vec<u8> {
148            let bytes = crate::serialize_header(crate::MAGIC_BYTES, crate::PCZT_VERSION_1);
149            postcard::to_extend(&self, bytes).expect("can serialize into memory")
150        }
151    }
152
153    /// Encodes the in-memory [`super::Pczt`] into the v1 serialization type [`Pczt`].
154    impl TryFrom<super::Pczt> for Pczt {
155        type Error = super::EncodingError;
156
157        fn try_from(pczt: super::Pczt) -> Result<Self, Self::Error> {
158            // The v1 format predates the v6 transaction format; a parser of the v1
159            // encoding could parse a v6 PCZT but never extract a transaction from it.
160            if pczt.global.tx_version == zcash_protocol::constants::V6_TX_VERSION {
161                return Err(super::EncodingError::UnsupportedTxVersion);
162            }
163
164            // The v1 format cannot represent an Ironwood bundle in any state other
165            // than the canonical empty one; a parser of the v1 encoding will
166            // reconstruct exactly that value.
167            if pczt.ironwood != orchard::EMPTY_IRONWOOD {
168                return Err(super::EncodingError::UnsupportedTxVersion);
169            }
170
171            Ok(Self {
172                global: pczt.global,
173                transparent: pczt.transparent,
174                sapling: sapling::v1::Bundle::try_from(pczt.sapling)?,
175                orchard: orchard::v1::Bundle::try_from(pczt.orchard)?,
176            })
177        }
178    }
179
180    impl From<Pczt> for super::Pczt {
181        fn from(pczt: Pczt) -> Self {
182            Self {
183                global: pczt.global,
184                transparent: pczt.transparent,
185                sapling: pczt.sapling.into(),
186                orchard: pczt.orchard.into(),
187                ironwood: orchard::EMPTY_IRONWOOD,
188            }
189        }
190    }
191
192    #[cfg(test)]
193    mod tests {
194        use zcash_protocol::consensus::BranchId;
195
196        use crate::roles::creator::Creator;
197
198        #[test]
199        fn v1_refuses_v6_pczts_and_non_canonical_ironwood_bundles() {
200            // A v6 tx cannot be encoded as a v1 PCZT, even when its Ironwood bundle is
201            // canonically empty.
202            let pczt = Creator::new(
203                BranchId::Nu6_3.into(),
204                10_000_000,
205                133,
206                Some([0; 32]),
207                Some([0; 32]),
208            )
209            .unwrap()
210            .build()
211            .unwrap();
212            assert!(matches!(
213                super::Pczt::try_from(pczt),
214                Err(crate::EncodingError::UnsupportedTxVersion)
215            ));
216
217            // A v5 tx carrying non-canonical Ironwood bundle data cannot be encoded
218            // as a v1 PCZT, because the data would be dropped.
219            let mut pczt = Creator::new(
220                BranchId::Nu6.into(),
221                10_000_000,
222                133,
223                Some([0; 32]),
224                Some([0; 32]),
225            )
226            .unwrap()
227            .build()
228            .unwrap();
229            pczt.ironwood.bsk = Some([1; 32]);
230            assert!(matches!(
231                super::Pczt::try_from(pczt),
232                Err(crate::EncodingError::UnsupportedTxVersion)
233            ));
234        }
235    }
236}
237
238/// Types and operations for the v2 Pczt encoding.
239pub mod v2 {
240    use alloc::vec::Vec;
241    use serde::{Deserialize, Serialize};
242
243    use crate::{common, orchard, sapling, transparent};
244
245    /// The in-memory type used for derived serialization of the v2 Pczt encoding.
246    #[derive(Clone, Debug, Serialize, Deserialize)]
247    pub struct Pczt {
248        global: common::Global,
249        // This value is set to `None` if the transparent bundle is empty,
250        // meaning inputs and outputs are empty.
251        transparent: Option<transparent::Bundle>,
252        // This value is set to `None` if the Sapling bundle is empty,
253        // meaning every field has its empty/default value.
254        sapling: Option<sapling::Bundle>,
255        // This value is set to `None` if the Orchard bundle is empty,
256        // meaning actions, value sum, anchor, zkproof, and bsk are all
257        // empty. Flags and note version are not checked, as values can be
258        // defaulted there.
259        orchard: Option<orchard::v2::Bundle>,
260        ironwood: Option<orchard::v2::Bundle>,
261    }
262
263    impl Pczt {
264        pub fn serialize(&self) -> Vec<u8> {
265            let bytes = crate::serialize_header(crate::MAGIC_BYTES, crate::PCZT_VERSION_2);
266            postcard::to_extend(&self, bytes).expect("can serialize into memory")
267        }
268    }
269
270    /// Encodes the in-memory [`super::Pczt`] into the v2 serialization type [`Pczt`],
271    /// omitting empty Transparent, Sapling, and Orchard bundles.
272    impl TryFrom<super::Pczt> for Pczt {
273        type Error = super::EncodingError;
274
275        fn try_from(pczt: super::Pczt) -> Result<Self, Self::Error> {
276            Ok(Self {
277                global: pczt.global,
278                transparent: (pczt.transparent != transparent::EMPTY_BUNDLE)
279                    .then_some(pczt.transparent),
280                sapling: sapling::v2::encode(pczt.sapling),
281                orchard: orchard::v2::encode(pczt.orchard, &orchard::EMPTY_ORCHARD)?,
282                ironwood: orchard::v2::encode(pczt.ironwood, &orchard::EMPTY_IRONWOOD)?,
283            })
284        }
285    }
286
287    impl Pczt {
288        pub(super) fn into_logical(self) -> Result<super::Pczt, super::ParseError> {
289            Ok(super::Pczt {
290                global: self.global,
291                transparent: self.transparent.unwrap_or(transparent::EMPTY_BUNDLE),
292                sapling: self.sapling.unwrap_or(sapling::EMPTY_BUNDLE),
293                orchard: self
294                    .orchard
295                    .map(orchard::v2::Bundle::into_logical)
296                    .transpose()?
297                    .unwrap_or(orchard::EMPTY_ORCHARD),
298                ironwood: self
299                    .ironwood
300                    .map(orchard::v2::Bundle::into_logical)
301                    .transpose()?
302                    .unwrap_or(orchard::EMPTY_IRONWOOD),
303            })
304        }
305    }
306
307    #[cfg(test)]
308    mod tests {
309        use zcash_protocol::consensus::BranchId;
310
311        use super::Pczt;
312        use crate::{orchard::NoteVersion, roles::creator::Creator};
313
314        #[test]
315        fn empty_bundles_encode_as_none_and_decode_as_empty() {
316            // Absent anchors: the shielded bundles carry no anchor and no
317            // spends/actions, so they are fully empty and omitted.
318            let pczt = Creator::new(BranchId::Nu6_3.into(), 10_000_000, 133, None, None)
319                .unwrap()
320                .build()
321                .unwrap();
322
323            let encoded = Pczt::try_from(pczt).unwrap();
324
325            assert!(encoded.transparent.is_none());
326            assert!(encoded.sapling.is_none());
327            assert!(encoded.orchard.is_none());
328            assert!(encoded.ironwood.is_none());
329
330            let decoded = crate::parse(&encoded.serialize()).unwrap();
331
332            assert!(decoded.transparent.inputs.is_empty());
333            assert!(decoded.transparent.outputs.is_empty());
334            assert!(decoded.sapling.spends.is_empty());
335            assert!(decoded.sapling.outputs.is_empty());
336            assert!(decoded.sapling.anchor.is_none());
337            assert!(decoded.orchard.actions.is_empty());
338            assert_eq!(decoded.orchard.note_version, NoteVersion::V2);
339            {
340                assert!(decoded.ironwood.actions.is_empty());
341                assert_eq!(decoded.ironwood.note_version, NoteVersion::V3);
342            }
343        }
344
345        #[test]
346        fn anchored_bundles_are_preserved() {
347            // A Sapling/Orchard bundle with a non-empty anchor differs from its
348            // empty form, so it must not be omitted even with no spends/actions,
349            // and the anchor must survive the v2 round-trip.
350            let pczt = Creator::new(
351                BranchId::Nu6.into(),
352                10_000_000,
353                133,
354                Some([1; 32]),
355                Some([2; 32]),
356            )
357            .unwrap()
358            .build()
359            .unwrap();
360
361            let encoded = Pczt::try_from(pczt).unwrap();
362
363            assert!(encoded.transparent.is_none());
364            assert!(encoded.sapling.is_some());
365            assert!(encoded.orchard.is_some());
366
367            let decoded = crate::parse(&encoded.serialize()).unwrap();
368
369            assert_eq!(decoded.sapling.anchor, Some([1; 32]));
370            assert_eq!(decoded.orchard.anchor, Some([2; 32]));
371        }
372
373        #[test]
374        fn non_canonical_orchard_flags_and_note_version_prevent_omission() {
375            let mut pczt = Creator::new(
376                BranchId::Nu6.into(),
377                10_000_000,
378                133,
379                Some([0; 32]),
380                Some([0; 32]),
381            )
382            .unwrap()
383            .build()
384            .unwrap();
385            pczt.orchard.flags = 0;
386            pczt.orchard.note_version = NoteVersion::V3;
387
388            // A bundle whose flags or note version differ from the canonical empty
389            // bundle is not omitted, so that those fields round-trip losslessly.
390            let encoded = Pczt::try_from(pczt.clone()).unwrap();
391            assert!(encoded.orchard.is_some());
392
393            let decoded = encoded.into_logical().unwrap();
394            assert_eq!(decoded.orchard, pczt.orchard);
395            assert_eq!(decoded.orchard.flags, 0);
396            assert_eq!(decoded.orchard.note_version, NoteVersion::V3);
397        }
398    }
399}
400
401/// Errors that can occur while serializing a PCZT.
402#[derive(Debug)]
403#[non_exhaustive]
404pub enum EncodingError {
405    /// The requested transaction version cannot be represented in this PCZT
406    /// encoding.
407    UnsupportedTxVersion,
408    /// The v1 PCZT encoding does not support this Orchard note plaintext version.
409    UnsupportedOrchardNoteVersion,
410    /// The PCZT contains data that can only be represented in v2.
411    RequiresV2,
412}
413
414impl Pczt {
415    /// Whether this PCZT carries any inputs or outputs in the given pool.
416    ///
417    /// Every bundle is always present as a value, so its existence says nothing; this asks whether
418    /// it holds anything.
419    pub fn has_data_in_pool(&self, pool: PoolType) -> bool {
420        match pool {
421            PoolType::TRANSPARENT => {
422                !self.transparent.inputs().is_empty() || !self.transparent.outputs().is_empty()
423            }
424            PoolType::SAPLING => {
425                !self.sapling.spends().is_empty() || !self.sapling.outputs().is_empty()
426            }
427            PoolType::ORCHARD => !self.orchard.actions().is_empty(),
428            PoolType::IRONWOOD => !self.ironwood.actions().is_empty(),
429        }
430    }
431
432    /// Parses a PCZT from its encoding.
433    pub fn parse(bytes: &[u8]) -> Result<Self, ParseError> {
434        let (version, body) = parse_header(bytes, MAGIC_BYTES).map_err(|e| match e {
435            HeaderParseError::InvalidMagic => ParseError::NotPczt,
436            HeaderParseError::TooShort => ParseError::TooShort,
437        })?;
438        match version {
439            PCZT_VERSION_1 => postcard::from_bytes::<v1::Pczt>(body)
440                .map(Pczt::from)
441                .map_err(ParseError::Invalid),
442            PCZT_VERSION_2 => postcard::from_bytes::<v2::Pczt>(body)
443                .map_err(ParseError::Invalid)
444                .and_then(v2::Pczt::into_logical),
445            _ => Err(ParseError::UnknownVersion(version)),
446        }
447    }
448
449    /// Serializes this PCZT in the minimal encoding version capable of
450    /// representing its content: the v1 encoding whenever the PCZT is
451    /// representable in it (maximizing compatibility with receivers that
452    /// predate the v2 encoding), and the v2 encoding otherwise.
453    ///
454    /// To force a specific PCZT version, use [`v1::Pczt`] or [`v2::Pczt`]
455    /// directly.
456    pub fn serialize(self) -> Result<Vec<u8>, EncodingError> {
457        // Fast pre-checks for the conditions that most commonly rule out the
458        // v1 encoding, avoiding the speculative clone below.
459        if self.global.tx_version != zcash_protocol::constants::V6_TX_VERSION
460            && self.ironwood == orchard::EMPTY_IRONWOOD
461        {
462            // The full v1-representability conditions live in the bundle
463            // conversions; attempting the conversion is the single source of
464            // truth for them.
465            if let Ok(v1) = v1::Pczt::try_from(self.clone()) {
466                return Ok(v1.serialize());
467            }
468        }
469        Ok(v2::Pczt::try_from(self)?.serialize())
470    }
471
472    /// Resolves derived or compact field representations carried by this PCZT.
473    ///
474    /// For improved efficiency, callers that will pass the same PCZT through
475    /// multiple roles should call this once up front. Parsing also resolves fields
476    /// defensively.
477    #[cfg(feature = "orchard")]
478    pub fn resolve_fields(&mut self) -> Result<(), ::orchard::pczt::ParseError> {
479        self.orchard.resolve_fields()?;
480        self.ironwood.resolve_fields()
481    }
482
483    /// Parses this PCZT's bundles and constructs a `TransactionData` using caller-provided
484    /// bundle extraction closures.
485    ///
486    /// This handles bundle parsing, version validation, consensus branch ID parsing,
487    /// lock time computation, and final assembly, delegating bundle extraction to the
488    /// caller via closures that receive references to the parsed bundles.
489    #[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
490    pub(crate) fn extract_tx_data<A, E>(
491        self,
492        anchor_requirement: common::AnchorRequirement,
493        extract_transparent: impl FnOnce(
494            &::transparent::pczt::Bundle,
495        ) -> Result<
496            Option<::transparent::bundle::Bundle<A::TransparentAuth>>,
497            E,
498        >,
499        extract_sapling: impl FnOnce(
500            &::sapling::pczt::Bundle,
501        ) -> Result<
502            Option<::sapling::Bundle<A::SaplingAuth, zcash_protocol::value::ZatBalance>>,
503            E,
504        >,
505        extract_orchard: impl FnOnce(
506            &::orchard::pczt::Bundle,
507        ) -> Result<
508            Option<::orchard::Bundle<A::OrchardAuth, zcash_protocol::value::ZatBalance>>,
509            E,
510        >,
511        extract_ironwood: impl FnOnce(
512            &::orchard::pczt::Bundle,
513        ) -> Result<
514            Option<::orchard::Bundle<A::OrchardAuth, zcash_protocol::value::ZatBalance>>,
515            E,
516        >,
517    ) -> Result<ParsedPczt<A>, E>
518    where
519        A: Authorization,
520        E: From<ExtractError>,
521    {
522        let Pczt {
523            global,
524            transparent,
525            sapling,
526            orchard,
527            ironwood,
528        } = self;
529
530        let consensus_branch_id = BranchId::try_from(global.consensus_branch_id)
531            .map_err(|_| ExtractError::UnknownConsensusBranchId)?;
532        let orchard_protocol_revision = consensus_branch_id
533            .orchard_protocol_revision()
534            // The v5 and v6 transaction formats do not exist prior to NU5, so no
535            // transaction could be extracted under such a branch in any case.
536            .ok_or(ExtractError::UnsupportedConsensusBranchId)?;
537
538        let version = match (global.tx_version, global.version_group_id) {
539            (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::V5),
540            (V6_TX_VERSION, V6_VERSION_GROUP_ID) => Ok(TxVersion::V6),
541            (version, version_group_id) => Err(ExtractError::UnsupportedTxVersion {
542                version,
543                version_group_id,
544            }),
545        }?;
546
547        match version {
548            // Only the v6 transaction format carries an Ironwood bundle.
549            TxVersion::Sprout(_) | TxVersion::V3 | TxVersion::V4 | TxVersion::V5 => {
550                if ironwood != crate::orchard::EMPTY_IRONWOOD {
551                    return Err(ExtractError::IronwoodNotSupported.into());
552                }
553            }
554            // The v6 transaction format does not exist prior to NU6.3 (the first
555            // upgrade under which the Orchard protocol is at revision V3).
556            TxVersion::V6 => {
557                if orchard_protocol_revision < OrchardProtocolRevision::V3 {
558                    return Err(ExtractError::UnsupportedConsensusBranchId.into());
559                }
560            }
561        }
562
563        let transparent = transparent
564            .into_parsed()
565            .map_err(ExtractError::TransparentParse)?;
566        let sapling = sapling
567            .into_parsed(anchor_requirement)
568            .map_err(ExtractError::SaplingParse)?;
569        let orchard_bundle_version = crate::orchard::bundle_version_for_revision(
570            orchard_protocol_revision,
571            ::orchard::ValuePool::Orchard,
572        )
573        .expect("the Orchard pool is supported under every protocol revision");
574        let orchard = orchard
575            .into_parsed_with_version(orchard_bundle_version, anchor_requirement)
576            .map_err(ExtractError::OrchardParse)?;
577        let ironwood = ironwood
578            .into_ironwood_parsed(anchor_requirement)
579            .map_err(ExtractError::IronwoodParse)?;
580
581        let lock_time = determine_lock_time(&global, transparent.inputs())
582            .ok_or(ExtractError::IncompatibleLockTimes)?;
583
584        let transparent_bundle = extract_transparent(&transparent)?;
585        let sapling_bundle = extract_sapling(&sapling.bundle)?;
586        let orchard_bundle = extract_orchard(&orchard.bundle)?;
587        let ironwood_bundle = extract_ironwood(&ironwood.bundle)?;
588
589        let tx_data = match version {
590            TxVersion::V6 => TransactionData::from_parts_v6(
591                consensus_branch_id,
592                lock_time,
593                global.expiry_height.into(),
594                #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
595                Zatoshis::ZERO,
596                transparent_bundle,
597                sapling_bundle,
598                orchard_bundle,
599                ironwood_bundle,
600            ),
601            _ => TransactionData::from_parts(
602                version,
603                consensus_branch_id,
604                lock_time,
605                global.expiry_height.into(),
606                #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
607                Zatoshis::ZERO,
608                transparent_bundle,
609                None,
610                sapling_bundle,
611                orchard_bundle,
612            ),
613        };
614
615        Ok(ParsedPczt {
616            global,
617            transparent,
618            sapling,
619            orchard,
620            ironwood,
621            tx_data,
622        })
623    }
624
625    /// Gets the effects of this transaction.
626    #[cfg(any(feature = "io-finalizer", feature = "signer"))]
627    pub fn into_effects(self) -> Result<TransactionData<EffectsOnly>, ExtractError> {
628        let anchor_requirement =
629            common::AnchorRequirement::for_pre_authorization(self.global.tx_version);
630
631        self.extract_tx_data(
632            anchor_requirement,
633            |t| {
634                t.extract_effects()
635                    .map_err(ExtractError::TransparentExtract)
636            },
637            |s| s.extract_effects().map_err(ExtractError::SaplingExtract),
638            |o| o.extract_effects().map_err(ExtractError::OrchardExtract),
639            |i| i.extract_effects().map_err(ExtractError::IronwoodExtract),
640        )
641        .map(|parsed| parsed.tx_data)
642    }
643}
644
645/// The result of parsing a PCZT and constructing its `TransactionData`.
646#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
647#[cfg_attr(
648    not(any(feature = "io-finalizer", feature = "signer")),
649    allow(dead_code)
650)]
651pub(crate) struct ParsedPczt<A: Authorization> {
652    pub(crate) global: Global,
653    pub(crate) transparent: ::transparent::pczt::Bundle,
654    pub(crate) sapling: crate::sapling::Parsed,
655    pub(crate) orchard: crate::orchard::Parsed,
656    pub(crate) ironwood: crate::orchard::Parsed,
657    pub(crate) tx_data: TransactionData<A>,
658}
659
660#[cfg(any(feature = "io-finalizer", feature = "signer"))]
661pub struct EffectsOnly;
662
663#[cfg(any(feature = "io-finalizer", feature = "signer"))]
664impl Authorization for EffectsOnly {
665    type TransparentAuth = ::transparent::bundle::EffectsOnly;
666    type SaplingAuth = ::sapling::bundle::EffectsOnly;
667    type OrchardAuth = ::orchard::bundle::EffectsOnly;
668}
669
670/// Helper to produce the correct sighash for a PCZT.
671#[cfg(any(feature = "io-finalizer", feature = "signer"))]
672pub(crate) fn sighash(
673    tx_data: &TransactionData<EffectsOnly>,
674    signable_input: &SignableInput,
675    txid_parts: &TxDigests<Blake2bHash>,
676) -> [u8; 32] {
677    match tx_data.version() {
678        TxVersion::V5 => v5_signature_hash(tx_data, signable_input, txid_parts),
679        TxVersion::V6 => v6_signature_hash(tx_data, signable_input, txid_parts),
680        _ => unreachable!("PCZT only supports v5 and v6 transaction data"),
681    }
682    .as_ref()
683    .try_into()
684    .expect("correct length")
685}
686
687/// Errors that can occur while parsing PCZT bundles and extracting transaction data.
688#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
689#[derive(Debug)]
690#[non_exhaustive]
691pub enum ExtractError {
692    /// The PCZT's transparent inputs have incompatible lock time requirements.
693    IncompatibleLockTimes,
694    /// An error occurred extracting the Ironwood protocol bundle from the Ironwood PCZT bundle.
695    IronwoodExtract(::orchard::pczt::TxExtractorError),
696    /// The PCZT carries Ironwood bundle data, but its transaction version does not
697    /// support an Ironwood bundle.
698    IronwoodNotSupported,
699    /// An error occurred parsing the Ironwood PCZT bundle from the PCZT data.
700    IronwoodParse(crate::orchard::ParseError),
701    /// An error occurred extracting the Orchard protocol bundle from the Orchard PCZT bundle.
702    OrchardExtract(::orchard::pczt::TxExtractorError),
703    /// An error occurred parsing the Orchard PCZT bundle from the PCZT data.
704    OrchardParse(crate::orchard::ParseError),
705    /// An error occurred extracting the Sapling protocol bundle from the Sapling PCZT bundle.
706    SaplingExtract(::sapling::pczt::TxExtractorError),
707    /// An error occurred parsing the Sapling PCZT bundle from the PCZT data.
708    SaplingParse(crate::sapling::ParseError),
709    /// An error occurred extracting the transparent protocol bundle from the
710    /// transparent PCZT bundle.
711    TransparentExtract(::transparent::pczt::TxExtractorError),
712    /// An error occurred parsing the transparent PCZT bundle from the PCZT data.
713    TransparentParse(::transparent::pczt::ParseError),
714    /// The consensus branch ID requested by the PCZT does not correspond to a
715    /// known network upgrade.
716    UnknownConsensusBranchId,
717    /// The network upgrade for the PCZT's consensus branch ID predates the v5
718    /// transaction format, so no transaction can be extracted from it.
719    UnsupportedConsensusBranchId,
720    /// The PCZT specifies an unsupported transaction version.
721    UnsupportedTxVersion { version: u32, version_group_id: u32 },
722}
723
724#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
725impl core::fmt::Display for ExtractError {
726    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
727        match self {
728            ExtractError::IncompatibleLockTimes => {
729                write!(f, "the transparent inputs have incompatible lock times")
730            }
731            ExtractError::IronwoodExtract(e) => {
732                write!(f, "could not extract the Ironwood bundle: {e}")
733            }
734            ExtractError::IronwoodNotSupported => write!(
735                f,
736                "the PCZT carries Ironwood bundle data, but its transaction version has no \
737                 Ironwood bundle"
738            ),
739            ExtractError::IronwoodParse(e) => {
740                write!(f, "could not parse the Ironwood bundle: {e:?}")
741            }
742            ExtractError::OrchardExtract(e) => {
743                write!(f, "could not extract the Orchard bundle: {e}")
744            }
745            ExtractError::OrchardParse(e) => write!(f, "could not parse the Orchard bundle: {e:?}"),
746            ExtractError::SaplingExtract(e) => {
747                write!(f, "could not extract the Sapling bundle: {e}")
748            }
749            ExtractError::SaplingParse(e) => write!(f, "could not parse the Sapling bundle: {e:?}"),
750            ExtractError::TransparentExtract(e) => {
751                write!(f, "could not extract the transparent bundle: {e:?}")
752            }
753            ExtractError::TransparentParse(e) => {
754                write!(f, "could not parse the transparent bundle: {e:?}")
755            }
756            ExtractError::UnknownConsensusBranchId => write!(f, "unknown consensus branch ID"),
757            ExtractError::UnsupportedConsensusBranchId => write!(
758                f,
759                "the consensus branch ID predates the v5 transaction format"
760            ),
761            ExtractError::UnsupportedTxVersion {
762                version,
763                version_group_id,
764            } => write!(
765                f,
766                "unsupported transaction version {version} (version group ID {version_group_id})"
767            ),
768        }
769    }
770}
771
772#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
773impl core::error::Error for ExtractError {}
774
775/// Errors that can occur while parsing a PCZT.
776#[derive(Debug)]
777pub enum ParseError {
778    /// The bytes do not contain a PCZT.
779    NotPczt,
780    /// The PCZT encoding was invalid.
781    Invalid(postcard::Error),
782    /// The PCZT encoding omitted a field that is required by the logical PCZT
783    /// type.
784    MissingRequiredField(&'static str),
785    /// The bytes are too short to contain a PCZT.
786    TooShort,
787    /// The PCZT has an unknown version.
788    UnknownVersion(u32),
789}
790
791impl core::fmt::Display for ParseError {
792    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
793        match self {
794            ParseError::NotPczt => write!(f, "the bytes do not contain a PCZT"),
795            ParseError::Invalid(e) => write!(f, "invalid PCZT encoding: {e}"),
796            ParseError::MissingRequiredField(field) => {
797                write!(f, "the PCZT encoding omitted the required field {field}")
798            }
799            ParseError::TooShort => write!(f, "the bytes are too short to contain a PCZT"),
800            ParseError::UnknownVersion(v) => write!(f, "unknown PCZT version {v}"),
801        }
802    }
803}
804
805impl core::error::Error for ParseError {}
806
807#[cfg(all(test, any(feature = "io-finalizer", feature = "signer")))]
808mod extraction_tests {
809    use zcash_protocol::consensus::BranchId;
810
811    use crate::{ExtractError, roles::creator::Creator};
812
813    #[test]
814    fn v5_pczt_with_ironwood_data_does_not_extract() {
815        let mut pczt = Creator::new(
816            BranchId::Nu6.into(),
817            10_000_000,
818            133,
819            Some([0; 32]),
820            Some([0; 32]),
821        )
822        .unwrap()
823        .build()
824        .unwrap();
825        pczt.ironwood.bsk = Some([1; 32]);
826        assert!(matches!(
827            pczt.into_effects(),
828            Err(ExtractError::IronwoodNotSupported)
829        ));
830    }
831
832    #[test]
833    fn v6_pczt_with_pre_nu6_3_branch_does_not_extract() {
834        let mut pczt = Creator::new(
835            BranchId::Nu6_3.into(),
836            10_000_000,
837            133,
838            Some([0; 32]),
839            Some([0; 32]),
840        )
841        .unwrap()
842        .build()
843        .unwrap();
844        pczt.global.consensus_branch_id = BranchId::Nu6_2.into();
845        assert!(matches!(
846            pczt.into_effects(),
847            Err(ExtractError::UnsupportedConsensusBranchId)
848        ));
849    }
850}
851
852#[cfg(test)]
853mod serialize_tests {
854    use zcash_protocol::consensus::BranchId;
855
856    use crate::roles::creator::Creator;
857
858    fn encoding_version(bytes: &[u8]) -> u32 {
859        assert_eq!(&bytes[..4], crate::MAGIC_BYTES);
860        u32::from_le_bytes(bytes[4..8].try_into().unwrap())
861    }
862
863    #[test]
864    fn serialize_emits_minimal_encoding() {
865        // A v1-representable (v5, canonical-empty Ironwood) PCZT serializes as v1.
866        let pczt = Creator::new(
867            BranchId::Nu6.into(),
868            10_000_000,
869            133,
870            Some([0; 32]),
871            Some([0; 32]),
872        )
873        .unwrap()
874        .build()
875        .unwrap();
876        let bytes = pczt.clone().serialize().unwrap();
877        assert_eq!(encoding_version(&bytes), crate::PCZT_VERSION_1);
878        // The minimal encoding still round-trips through the ordinary parser.
879        assert!(crate::Pczt::parse(&bytes).is_ok());
880
881        // Non-canonical Ironwood data forces the v2 encoding.
882        let mut with_ironwood = pczt.clone();
883        with_ironwood.ironwood.bsk = Some([1; 32]);
884        assert_eq!(
885            encoding_version(&with_ironwood.serialize().unwrap()),
886            crate::PCZT_VERSION_2,
887        );
888
889        // An Orchard note-plaintext version the v1 encoding cannot carry
890        // forces the v2 encoding.
891        let mut with_note_v3 = pczt;
892        with_note_v3.orchard.note_version = crate::orchard::NoteVersion::V3;
893        assert_eq!(
894            encoding_version(&with_note_v3.serialize().unwrap()),
895            crate::PCZT_VERSION_2,
896        );
897
898        // A v6 transaction forces the v2 encoding.
899        let v6 = Creator::new(
900            BranchId::Nu6_3.into(),
901            10_000_000,
902            133,
903            Some([0; 32]),
904            Some([0; 32]),
905        )
906        .unwrap()
907        .build()
908        .unwrap();
909        assert_eq!(
910            encoding_version(&v6.serialize().unwrap()),
911            crate::PCZT_VERSION_2,
912        );
913    }
914}