Skip to main content

pczt/roles/creator/
mod.rs

1//! The Creator role (single entity).
2//!
3//!  - Creates the base PCZT with no information about spends or outputs.
4
5use alloc::collections::BTreeMap;
6
7use crate::{
8    Pczt,
9    common::{
10        FLAG_SHIELDED_MODIFIABLE, FLAG_TRANSPARENT_INPUTS_MODIFIABLE,
11        FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE,
12    },
13    orchard::{Bundle as OrchardBundle, ORCHARD_SPENDS_AND_OUTPUTS_ENABLED},
14};
15
16#[cfg(feature = "zcp-builder")]
17use {
18    crate::common::FLAG_HAS_SIGHASH_SINGLE,
19    ::transparent::sighash::{SIGHASH_ANYONECANPAY, SIGHASH_SINGLE},
20    zcash_protocol::{consensus::NetworkConstants, constants::V4_TX_VERSION},
21};
22
23#[cfg(feature = "orchard")]
24use crate::orchard::bundle_version_for_revision;
25
26#[cfg(zcash_unstable = "nutachyon")]
27use zcash_protocol::constants::{V7_TX_VERSION, V7_VERSION_GROUP_ID};
28use zcash_protocol::{
29    consensus::BranchId,
30    constants::{V5_TX_VERSION, V5_VERSION_GROUP_ID, V6_TX_VERSION, V6_VERSION_GROUP_ID},
31};
32
33/// Initial flags allowing any modification.
34const INITIAL_TX_MODIFIABLE: u8 = FLAG_TRANSPARENT_INPUTS_MODIFIABLE
35    | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE
36    | FLAG_SHIELDED_MODIFIABLE;
37
38/// Errors that can occur when creating a PCZT.
39#[derive(Debug)]
40pub enum Error {
41    /// A v5 transaction's shielded bundle anchors must be set by the Creator,
42    /// because they are transaction effecting data and cannot subsequently
43    /// change.
44    AnchorRequiredForV5,
45    /// The transaction version implied by the consensus branch ID does not carry an
46    /// Ironwood bundle.
47    IronwoodNotSupported,
48    /// The consensus branch ID does not correspond to any known network upgrade.
49    UnknownConsensusBranchId,
50    /// The network upgrade for the consensus branch ID predates the v5
51    /// transaction format, so it cannot be used to create a PCZT.
52    UnsupportedConsensusBranchId,
53    /// The requested Orchard-protocol flags cannot be represented under the
54    /// Orchard or Ironwood bundle version implied by the consensus branch ID.
55    #[cfg(feature = "orchard")]
56    UnrepresentableOrchardFlags,
57}
58
59/// Returns the network upgrade for `consensus_branch_id`, rejecting unrecognized
60/// branch IDs and any upgrade that predates the v5 transaction format.
61fn consensus_branch_id_for_pczt(consensus_branch_id: u32) -> Result<BranchId, Error> {
62    match BranchId::try_from(consensus_branch_id).map_err(|_| Error::UnknownConsensusBranchId)? {
63        BranchId::Sprout
64        | BranchId::Overwinter
65        | BranchId::Sapling
66        | BranchId::Blossom
67        | BranchId::Heartwood
68        | BranchId::Canopy => Err(Error::UnsupportedConsensusBranchId),
69        branch_id => Ok(branch_id),
70    }
71}
72
73pub struct Creator {
74    tx_version: u32,
75    version_group_id: u32,
76    consensus_branch_id: BranchId,
77    fallback_lock_time: Option<u32>,
78    expiry_height: u32,
79    coin_type: u32,
80    orchard_flags: u8,
81    ironwood_flags: u8,
82    sapling_anchor: Option<[u8; 32]>,
83    orchard_anchor: Option<[u8; 32]>,
84    ironwood_anchor: Option<[u8; 32]>,
85}
86
87impl Creator {
88    /// Creates a new PCZT for the given consensus branch ID.
89    ///
90    /// The transaction version is implied by the consensus branch ID: the v6
91    /// transaction format from NU6.3 onward, and the v5 format for earlier upgrades.
92    /// For v5 transactions, `sapling_anchor` and `orchard_anchor` must both be
93    /// [`Option::Some`]. For V6 and later transactions, either anchor may be
94    /// [`Option::None`] and restored later.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`Error::UnknownConsensusBranchId`] if `consensus_branch_id` is not
99    /// a recognized branch ID, or [`Error::UnsupportedConsensusBranchId`] if it
100    /// predates the v5 transaction format.
101    pub fn new(
102        consensus_branch_id: u32,
103        expiry_height: u32,
104        coin_type: u32,
105        sapling_anchor: Option<[u8; 32]>,
106        orchard_anchor: Option<[u8; 32]>,
107    ) -> Result<Self, Error> {
108        let branch_id = consensus_branch_id_for_pczt(consensus_branch_id)?;
109
110        let (tx_version, version_group_id) = match branch_id {
111            // Pre-NU5 branches are rejected by `consensus_branch_id_for_pczt`; NU5
112            // through NU6.2 use the v5 transaction format.
113            BranchId::Sprout
114            | BranchId::Overwinter
115            | BranchId::Sapling
116            | BranchId::Blossom
117            | BranchId::Heartwood
118            | BranchId::Canopy
119            | BranchId::Nu5
120            | BranchId::Nu6
121            | BranchId::Nu6_1
122            | BranchId::Nu6_2 => (V5_TX_VERSION, V5_VERSION_GROUP_ID),
123            BranchId::Nu6_3 => (V6_TX_VERSION, V6_VERSION_GROUP_ID),
124            BranchId::Nu7 => (V6_TX_VERSION, V6_VERSION_GROUP_ID),
125            #[cfg(zcash_unstable = "nutachyon")]
126            BranchId::NuTachyon => (V7_TX_VERSION, V7_VERSION_GROUP_ID),
127        };
128
129        Ok(Self {
130            tx_version,
131            version_group_id,
132            consensus_branch_id: branch_id,
133            fallback_lock_time: None,
134            expiry_height,
135            coin_type,
136            orchard_flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
137            ironwood_flags: crate::orchard::IRONWOOD_SPENDS_OUTPUTS_AND_CROSS_ADDRESS_ENABLED,
138            sapling_anchor,
139            orchard_anchor,
140            ironwood_anchor: None,
141        })
142    }
143
144    /// Returns the bundle version in effect for the given Orchard-protocol value pool
145    /// under this Creator's consensus branch ID, or `None` if the pool is not
146    /// supported under that branch.
147    #[cfg(feature = "orchard")]
148    fn bundle_version(&self, pool: orchard::ValuePool) -> Option<orchard::bundle::BundleVersion> {
149        self.consensus_branch_id
150            .orchard_protocol_revision()
151            .and_then(|revision| bundle_version_for_revision(revision, pool))
152    }
153
154    pub fn with_fallback_lock_time(mut self, fallback: u32) -> Self {
155        self.fallback_lock_time = Some(fallback);
156        self
157    }
158
159    /// Sets the Orchard flags for the PCZT.
160    ///
161    /// The flags are validated against, and encoded under, the Orchard bundle
162    /// version implied by the consensus branch ID passed to [`Creator::new`]
163    /// (which also fixes the note-plaintext version).
164    ///
165    /// # Errors
166    ///
167    /// Returns [`Error::UnrepresentableOrchardFlags`] if `flags` cannot be encoded
168    /// under that bundle version (e.g. cross-address-enabled flags under a
169    /// post-NU6.3 Orchard version, or cross-address-disabled flags under a pre-NU6.3
170    /// Orchard version).
171    #[cfg(feature = "orchard")]
172    pub fn with_orchard_flags(
173        mut self,
174        orchard_flags: orchard::bundle::Flags,
175    ) -> Result<Self, Error> {
176        self.orchard_flags = orchard_flags
177            .to_byte(
178                self.bundle_version(orchard::ValuePool::Orchard)
179                    .expect("`Creator::new` rejects branches that predate NU5"),
180            )
181            .ok_or(Error::UnrepresentableOrchardFlags)?;
182        Ok(self)
183    }
184
185    /// Sets the Ironwood anchor for the PCZT.
186    ///
187    /// # Errors
188    ///
189    /// Returns [`Error::IronwoodNotSupported`] if the transaction version implied by
190    /// the consensus branch ID passed to [`Creator::new`] does not carry an Ironwood
191    /// bundle.
192    pub fn with_ironwood_anchor(mut self, ironwood_anchor: [u8; 32]) -> Result<Self, Error> {
193        let supports_ironwood = match self.tx_version {
194            V6_TX_VERSION => true,
195            #[cfg(zcash_unstable = "nutachyon")]
196            V7_TX_VERSION => true,
197            _ => false,
198        };
199        if !supports_ironwood {
200            return Err(Error::IronwoodNotSupported);
201        }
202        self.ironwood_anchor = Some(ironwood_anchor);
203        Ok(self)
204    }
205
206    /// Sets the Ironwood flags for the PCZT.
207    ///
208    /// # Errors
209    ///
210    /// Returns [`Error::IronwoodNotSupported`] if the transaction version implied by
211    /// the consensus branch ID passed to [`Creator::new`] does not carry an Ironwood
212    /// bundle, or [`Error::UnrepresentableOrchardFlags`] if `flags` cannot be encoded
213    /// under the Ironwood bundle version.
214    #[cfg(feature = "orchard")]
215    pub fn with_ironwood_flags(
216        mut self,
217        ironwood_flags: orchard::bundle::Flags,
218    ) -> Result<Self, Error> {
219        self.ironwood_flags = ironwood_flags
220            .to_byte(
221                self.bundle_version(orchard::ValuePool::Ironwood)
222                    .ok_or(Error::IronwoodNotSupported)?,
223            )
224            .ok_or(Error::UnrepresentableOrchardFlags)?;
225        Ok(self)
226    }
227
228    /// Builds the initial PCZT.
229    ///
230    /// # Errors
231    ///
232    /// Returns [`Error::AnchorRequiredForV5`] if this Creator describes a v5
233    /// transaction and either the Sapling anchor is missing from a bundle with
234    /// spends, or the Orchard anchor is missing from a bundle with actions.
235    pub fn build(self) -> Result<Pczt, Error> {
236        let sapling = crate::sapling::Bundle {
237            spends: vec![],
238            outputs: vec![],
239            value_sum: 0,
240            anchor: self.sapling_anchor,
241            bsk: None,
242        };
243        let orchard = OrchardBundle {
244            actions: vec![],
245            flags: self.orchard_flags,
246            value_sum: (0, false),
247            anchor: self.orchard_anchor,
248            // The note-plaintext version is determined by the Orchard bundle version.
249            #[cfg(feature = "orchard")]
250            note_version: self
251                .bundle_version(orchard::ValuePool::Orchard)
252                .expect("`Creator::new` rejects branches that predate NU5")
253                .note_version(),
254            #[cfg(not(feature = "orchard"))]
255            note_version: crate::orchard::NoteVersion::V2,
256            zkproof: None,
257            bsk: None,
258        };
259
260        if self.tx_version == V5_TX_VERSION
261            && ((sapling.anchor.is_none() && !sapling.spends.is_empty())
262                || (orchard.anchor.is_none() && !orchard.actions.is_empty()))
263        {
264            return Err(Error::AnchorRequiredForV5);
265        }
266
267        Ok(Pczt {
268            global: crate::common::Global {
269                tx_version: self.tx_version,
270                version_group_id: self.version_group_id,
271                consensus_branch_id: self.consensus_branch_id.into(),
272                fallback_lock_time: self.fallback_lock_time,
273                expiry_height: self.expiry_height,
274                coin_type: self.coin_type,
275                tx_modifiable: INITIAL_TX_MODIFIABLE,
276                proprietary: BTreeMap::new(),
277            },
278            transparent: crate::transparent::Bundle {
279                inputs: vec![],
280                outputs: vec![],
281            },
282            sapling,
283            orchard,
284            ironwood: OrchardBundle {
285                flags: self.ironwood_flags,
286                anchor: self.ironwood_anchor,
287                ..crate::orchard::EMPTY_IRONWOOD
288            },
289        })
290    }
291
292    /// Builds a PCZT from the output of a [`Builder`].
293    ///
294    /// Returns `None` if the `TxVersion` is incompatible with PCZTs.
295    ///
296    /// [`Builder`]: zcash_primitives::transaction::builder::Builder
297    #[cfg(feature = "zcp-builder")]
298    pub fn build_from_parts<P: zcash_protocol::consensus::Parameters>(
299        parts: zcash_primitives::transaction::builder::PcztParts<P>,
300    ) -> Option<Pczt> {
301        let tx_version = match parts.version {
302            zcash_primitives::transaction::TxVersion::Sprout(_)
303            | zcash_primitives::transaction::TxVersion::V3 => None,
304            zcash_primitives::transaction::TxVersion::V4 => Some(V4_TX_VERSION),
305            zcash_primitives::transaction::TxVersion::V5 => Some(V5_TX_VERSION),
306            zcash_primitives::transaction::TxVersion::V6 => Some(V6_TX_VERSION),
307            #[cfg(zcash_unstable = "nutachyon")]
308            zcash_primitives::transaction::TxVersion::V7 => Some(V7_TX_VERSION),
309        }?;
310
311        // Spends and outputs not modifiable.
312        let mut tx_modifiable = 0b0000_0000;
313        // Check if any input is using `SIGHASH_SINGLE` (with or without `ANYONECANPAY`).
314        if parts.transparent.as_ref().is_some_and(|bundle| {
315            bundle.inputs().iter().any(|input| {
316                (input.sighash_type().encode() & !SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE
317            })
318        }) {
319            tx_modifiable |= FLAG_HAS_SIGHASH_SINGLE;
320        }
321
322        Some(Pczt {
323            global: crate::common::Global {
324                tx_version,
325                version_group_id: parts.version.version_group_id(),
326                consensus_branch_id: parts.consensus_branch_id.into(),
327                fallback_lock_time: Some(parts.lock_time),
328                expiry_height: parts.expiry_height.into(),
329                coin_type: parts.params.network_type().coin_type(),
330                tx_modifiable,
331                proprietary: BTreeMap::new(),
332            },
333            transparent: parts
334                .transparent
335                .map(crate::transparent::Bundle::serialize_from)
336                .unwrap_or(crate::transparent::EMPTY_BUNDLE),
337            sapling: parts
338                .sapling
339                .map(crate::sapling::Bundle::serialize_from)
340                .unwrap_or(crate::sapling::EMPTY_BUNDLE),
341            orchard: parts
342                .orchard
343                .map(OrchardBundle::serialize_from)
344                .unwrap_or(crate::orchard::EMPTY_ORCHARD),
345            ironwood: parts
346                .ironwood
347                .map(OrchardBundle::serialize_from)
348                .unwrap_or(crate::orchard::EMPTY_IRONWOOD),
349        })
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    #[cfg(zcash_unstable = "nutachyon")]
356    use zcash_protocol::constants::{V7_TX_VERSION, V7_VERSION_GROUP_ID};
357    use zcash_protocol::{
358        consensus::BranchId,
359        constants::{V5_TX_VERSION, V5_VERSION_GROUP_ID, V6_TX_VERSION, V6_VERSION_GROUP_ID},
360    };
361
362    use super::{Creator, Error};
363
364    #[test]
365    fn tx_version_follows_branch() {
366        let pczt = Creator::new(
367            BranchId::Nu6_2.into(),
368            10_000_000,
369            133,
370            Some([0; 32]),
371            Some([0; 32]),
372        )
373        .unwrap()
374        .build()
375        .unwrap();
376        assert_eq!(pczt.global.tx_version, V5_TX_VERSION);
377        assert_eq!(pczt.global.version_group_id, V5_VERSION_GROUP_ID);
378
379        let pczt = Creator::new(
380            BranchId::Nu6_3.into(),
381            10_000_000,
382            133,
383            Some([0; 32]),
384            Some([0; 32]),
385        )
386        .unwrap()
387        .build()
388        .unwrap();
389        assert_eq!(pczt.global.tx_version, V6_TX_VERSION);
390        assert_eq!(pczt.global.version_group_id, V6_VERSION_GROUP_ID);
391
392        #[cfg(zcash_unstable = "nutachyon")]
393        {
394            let pczt = Creator::new(
395                BranchId::NuTachyon.into(),
396                10_000_000,
397                133,
398                Some([0; 32]),
399                Some([0; 32]),
400            )
401            .unwrap()
402            .build()
403            .unwrap();
404            assert_eq!(pczt.global.tx_version, V7_TX_VERSION);
405            assert_eq!(pczt.global.version_group_id, V7_VERSION_GROUP_ID);
406        }
407    }
408
409    #[test]
410    fn optional_anchors_are_supported_for_empty_bundles() {
411        let pczt = Creator::new(BranchId::Nu6_2.into(), 10_000_000, 133, None, Some([0; 32]))
412            .unwrap()
413            .build()
414            .unwrap();
415        assert!(pczt.sapling.anchor.is_none());
416        assert_eq!(pczt.orchard.anchor, Some([0; 32]));
417
418        let pczt = Creator::new(BranchId::Nu6_2.into(), 10_000_000, 133, Some([0; 32]), None)
419            .unwrap()
420            .build()
421            .unwrap();
422        assert_eq!(pczt.sapling.anchor, Some([0; 32]));
423        assert!(pczt.orchard.anchor.is_none());
424
425        let pczt = Creator::new(BranchId::Nu6_3.into(), 10_000_000, 133, None, None)
426            .unwrap()
427            .build()
428            .unwrap();
429
430        assert!(pczt.sapling.anchor.is_none());
431        assert!(pczt.orchard.anchor.is_none());
432    }
433
434    #[test]
435    fn ironwood_anchor_requires_v6() {
436        assert!(matches!(
437            Creator::new(
438                BranchId::Nu6_2.into(),
439                10_000_000,
440                133,
441                Some([0; 32]),
442                Some([0; 32])
443            )
444            .unwrap()
445            .with_ironwood_anchor([1; 32]),
446            Err(Error::IronwoodNotSupported)
447        ));
448
449        let pczt = Creator::new(
450            BranchId::Nu6_3.into(),
451            10_000_000,
452            133,
453            Some([0; 32]),
454            Some([0; 32]),
455        )
456        .unwrap()
457        .with_ironwood_anchor([1; 32])
458        .unwrap()
459        .build()
460        .unwrap();
461        assert_eq!(pczt.ironwood.anchor, Some([1; 32]));
462    }
463}