1#![cfg_attr(feature = "std", doc = "## Feature flags")]
12#![cfg_attr(feature = "std", doc = document_features::document_features!())]
13#![no_std]
16#![cfg_attr(docsrs, feature(doc_cfg))]
17#![cfg_attr(docsrs, doc(auto_cfg))]
18#![deny(rustdoc::broken_intra_doc_links)]
20
21#[macro_use]
22extern crate alloc;
23#[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 = "nutachyon"
37))]
38use zcash_protocol::constants::{V7_TX_VERSION, V7_VERSION_GROUP_ID};
39#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
40use {
41 common::{Global, determine_lock_time},
42 zcash_primitives::transaction::{Authorization, TransactionData, TxVersion},
43 zcash_protocol::{
44 consensus::{BranchId, OrchardProtocolRevision},
45 constants::{V5_TX_VERSION, V5_VERSION_GROUP_ID},
46 },
47};
48
49#[cfg(any(feature = "io-finalizer", feature = "signer"))]
50use zcash_primitives::transaction::sighash::{PrecomputedSighashData, SignableInput};
51
52pub mod roles;
53
54pub mod common;
55pub mod orchard;
56pub mod sapling;
57pub mod transparent;
58
59pub(crate) const MAGIC_BYTES: &[u8; 4] = b"PCZT";
60pub(crate) const PCZT_VERSION_1: u32 = 1;
61pub(crate) const PCZT_VERSION_2: u32 = 2;
62
63const VERSIONED_HEADER_LEN: usize = 8;
64
65pub(crate) enum HeaderParseError {
66 InvalidMagic,
67 TooShort,
68}
69
70pub(crate) fn parse_header<'a>(
71 bytes: &'a [u8],
72 magic: &[u8; 4],
73) -> Result<(u32, &'a [u8]), HeaderParseError> {
74 if bytes.len() < VERSIONED_HEADER_LEN {
75 return Err(HeaderParseError::TooShort);
76 }
77 if &bytes[..4] != magic {
78 return Err(HeaderParseError::InvalidMagic);
79 }
80
81 let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
82 Ok((version, &bytes[VERSIONED_HEADER_LEN..]))
83}
84
85pub(crate) fn serialize_header(magic: &[u8; 4], version: u32) -> Vec<u8> {
86 let mut bytes = Vec::with_capacity(VERSIONED_HEADER_LEN);
87 bytes.extend_from_slice(magic);
88 bytes.extend_from_slice(&version.to_le_bytes());
89 bytes
90}
91
92pub fn parse(bytes: &[u8]) -> Result<Pczt, ParseError> {
94 Pczt::parse(bytes)
95}
96
97#[derive(Clone, Debug, Getters)]
99pub struct Pczt {
100 #[getset(get = "pub")]
102 pub(crate) global: common::Global,
103
104 #[getset(get = "pub")]
113 pub(crate) transparent: transparent::Bundle,
114 #[getset(get = "pub")]
115 pub(crate) sapling: sapling::Bundle,
116 #[getset(get = "pub")]
117 pub(crate) orchard: orchard::Bundle,
118 #[getset(get = "pub")]
119 pub(crate) ironwood: orchard::Bundle,
120}
121
122pub mod v1 {
124 use alloc::vec::Vec;
125 use serde::{Deserialize, Serialize};
126
127 use crate::{common, orchard, sapling, transparent};
128
129 #[derive(Clone, Debug, Serialize, Deserialize)]
131 pub struct Pczt {
132 global: common::Global,
133 transparent: transparent::Bundle,
134 sapling: sapling::v1::Bundle,
135 orchard: orchard::v1::Bundle,
136 }
137
138 impl Pczt {
139 pub fn serialize(&self) -> Vec<u8> {
140 let bytes = crate::serialize_header(crate::MAGIC_BYTES, crate::PCZT_VERSION_1);
141 postcard::to_extend(&self, bytes).expect("can serialize into memory")
142 }
143 }
144
145 impl TryFrom<super::Pczt> for Pczt {
147 type Error = super::EncodingError;
148
149 fn try_from(pczt: super::Pczt) -> Result<Self, Self::Error> {
150 if match pczt.global.tx_version {
152 zcash_protocol::constants::V6_TX_VERSION => true,
153 #[cfg(zcash_unstable = "nutachyon")]
154 zcash_protocol::constants::V7_TX_VERSION => true,
155 _ => false,
156 } {
157 return Err(super::EncodingError::UnsupportedTxVersion);
158 }
159
160 if pczt.ironwood != orchard::EMPTY_IRONWOOD {
164 return Err(super::EncodingError::UnsupportedTxVersion);
165 }
166
167 Ok(Self {
168 global: pczt.global,
169 transparent: pczt.transparent,
170 sapling: sapling::v1::Bundle::try_from(pczt.sapling)?,
171 orchard: orchard::v1::Bundle::try_from(pczt.orchard)?,
172 })
173 }
174 }
175
176 impl From<Pczt> for super::Pczt {
177 fn from(pczt: Pczt) -> Self {
178 Self {
179 global: pczt.global,
180 transparent: pczt.transparent,
181 sapling: pczt.sapling.into(),
182 orchard: pczt.orchard.into(),
183 ironwood: orchard::EMPTY_IRONWOOD,
184 }
185 }
186 }
187
188 #[cfg(test)]
189 mod tests {
190 use zcash_protocol::consensus::BranchId;
191
192 use crate::roles::creator::Creator;
193
194 #[test]
195 fn v1_refuses_v6_pczts_and_non_canonical_ironwood_bundles() {
196 let pczt = Creator::new(
199 BranchId::Nu6_3.into(),
200 10_000_000,
201 133,
202 Some([0; 32]),
203 Some([0; 32]),
204 )
205 .unwrap()
206 .build()
207 .unwrap();
208 assert!(matches!(
209 super::Pczt::try_from(pczt),
210 Err(crate::EncodingError::UnsupportedTxVersion)
211 ));
212
213 let mut pczt = Creator::new(
216 BranchId::Nu6.into(),
217 10_000_000,
218 133,
219 Some([0; 32]),
220 Some([0; 32]),
221 )
222 .unwrap()
223 .build()
224 .unwrap();
225 pczt.ironwood.bsk = Some(crate::common::SecretKeyBytes::new([1; 32]));
226 assert!(matches!(
227 super::Pczt::try_from(pczt),
228 Err(crate::EncodingError::UnsupportedTxVersion)
229 ));
230 }
231 }
232}
233
234pub mod v2 {
236 use alloc::vec::Vec;
237 use serde::{Deserialize, Serialize};
238
239 use crate::{common, orchard, sapling, transparent};
240
241 #[derive(Clone, Debug, Serialize, Deserialize)]
243 pub struct Pczt {
244 global: common::Global,
245 transparent: Option<transparent::Bundle>,
248 sapling: Option<sapling::Bundle>,
251 orchard: Option<orchard::v2::Bundle>,
256 ironwood: Option<orchard::v2::Bundle>,
257 }
258
259 impl Pczt {
260 pub fn serialize(&self) -> Vec<u8> {
261 let bytes = crate::serialize_header(crate::MAGIC_BYTES, crate::PCZT_VERSION_2);
262 postcard::to_extend(&self, bytes).expect("can serialize into memory")
263 }
264 }
265
266 impl TryFrom<super::Pczt> for Pczt {
269 type Error = super::EncodingError;
270
271 fn try_from(pczt: super::Pczt) -> Result<Self, Self::Error> {
272 Ok(Self {
273 global: pczt.global,
274 transparent: (pczt.transparent != transparent::EMPTY_BUNDLE)
275 .then_some(pczt.transparent),
276 sapling: sapling::v2::encode(pczt.sapling),
277 orchard: orchard::v2::encode(pczt.orchard, &orchard::EMPTY_ORCHARD)?,
278 ironwood: orchard::v2::encode(pczt.ironwood, &orchard::EMPTY_IRONWOOD)?,
279 })
280 }
281 }
282
283 impl Pczt {
284 pub(super) fn into_logical(self) -> Result<super::Pczt, super::ParseError> {
285 Ok(super::Pczt {
286 global: self.global,
287 transparent: self.transparent.unwrap_or(transparent::EMPTY_BUNDLE),
288 sapling: self.sapling.unwrap_or(sapling::EMPTY_BUNDLE),
289 orchard: self
290 .orchard
291 .map(orchard::v2::Bundle::into_logical)
292 .transpose()?
293 .unwrap_or(orchard::EMPTY_ORCHARD),
294 ironwood: self
295 .ironwood
296 .map(orchard::v2::Bundle::into_logical)
297 .transpose()?
298 .unwrap_or(orchard::EMPTY_IRONWOOD),
299 })
300 }
301 }
302
303 #[cfg(test)]
304 mod tests {
305 use zcash_protocol::consensus::BranchId;
306
307 use super::Pczt;
308 use crate::{orchard::NoteVersion, roles::creator::Creator};
309
310 #[test]
311 fn empty_bundles_encode_as_none_and_decode_as_empty() {
312 let pczt = Creator::new(BranchId::Nu6_3.into(), 10_000_000, 133, None, None)
315 .unwrap()
316 .build()
317 .unwrap();
318
319 let encoded = Pczt::try_from(pczt).unwrap();
320
321 assert!(encoded.transparent.is_none());
322 assert!(encoded.sapling.is_none());
323 assert!(encoded.orchard.is_none());
324 assert!(encoded.ironwood.is_none());
325
326 let decoded = crate::parse(&encoded.serialize()).unwrap();
327
328 assert!(decoded.transparent.inputs.is_empty());
329 assert!(decoded.transparent.outputs.is_empty());
330 assert!(decoded.sapling.spends.is_empty());
331 assert!(decoded.sapling.outputs.is_empty());
332 assert!(decoded.sapling.anchor.is_none());
333 assert!(decoded.orchard.actions.is_empty());
334 assert_eq!(decoded.orchard.note_version, NoteVersion::V2);
335 {
336 assert!(decoded.ironwood.actions.is_empty());
337 assert_eq!(decoded.ironwood.note_version, NoteVersion::V3);
338 }
339 }
340
341 #[test]
342 fn anchored_bundles_are_preserved() {
343 let pczt = Creator::new(
347 BranchId::Nu6.into(),
348 10_000_000,
349 133,
350 Some([1; 32]),
351 Some([2; 32]),
352 )
353 .unwrap()
354 .build()
355 .unwrap();
356
357 let encoded = Pczt::try_from(pczt).unwrap();
358
359 assert!(encoded.transparent.is_none());
360 assert!(encoded.sapling.is_some());
361 assert!(encoded.orchard.is_some());
362
363 let decoded = crate::parse(&encoded.serialize()).unwrap();
364
365 assert_eq!(decoded.sapling.anchor, Some([1; 32]));
366 assert_eq!(decoded.orchard.anchor, Some([2; 32]));
367 }
368
369 #[test]
370 fn non_canonical_orchard_flags_and_note_version_prevent_omission() {
371 let mut pczt = Creator::new(
372 BranchId::Nu6.into(),
373 10_000_000,
374 133,
375 Some([0; 32]),
376 Some([0; 32]),
377 )
378 .unwrap()
379 .build()
380 .unwrap();
381 pczt.orchard.flags = 0;
382 pczt.orchard.note_version = NoteVersion::V3;
383
384 let encoded = Pczt::try_from(pczt.clone()).unwrap();
387 assert!(encoded.orchard.is_some());
388
389 let decoded = encoded.into_logical().unwrap();
390 assert_eq!(decoded.orchard, pczt.orchard);
391 assert_eq!(decoded.orchard.flags, 0);
392 assert_eq!(decoded.orchard.note_version, NoteVersion::V3);
393 }
394 }
395}
396
397#[derive(Debug)]
399#[non_exhaustive]
400pub enum EncodingError {
401 UnsupportedTxVersion,
404 UnsupportedOrchardNoteVersion,
406 RequiresV2,
408}
409
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
412pub enum EncodingVersion {
413 V1,
415 V2,
417}
418
419impl Pczt {
420 pub fn has_data_in_pool(&self, pool: PoolType) -> bool {
425 match pool {
426 PoolType::TRANSPARENT => {
427 !self.transparent.inputs().is_empty() || !self.transparent.outputs().is_empty()
428 }
429 PoolType::SAPLING => {
430 !self.sapling.spends().is_empty() || !self.sapling.outputs().is_empty()
431 }
432 PoolType::ORCHARD => !self.orchard.actions().is_empty(),
433 PoolType::IRONWOOD => !self.ironwood.actions().is_empty(),
434 }
435 }
436
437 pub fn parse(bytes: &[u8]) -> Result<Self, ParseError> {
439 let (version, body) = parse_header(bytes, MAGIC_BYTES).map_err(|e| match e {
440 HeaderParseError::InvalidMagic => ParseError::NotPczt,
441 HeaderParseError::TooShort => ParseError::TooShort,
442 })?;
443 match version {
444 PCZT_VERSION_1 => postcard::from_bytes::<v1::Pczt>(body)
445 .map(Pczt::from)
446 .map_err(ParseError::Invalid),
447 PCZT_VERSION_2 => postcard::from_bytes::<v2::Pczt>(body)
448 .map_err(ParseError::Invalid)
449 .and_then(v2::Pczt::into_logical),
450 _ => Err(ParseError::UnknownVersion(version)),
451 }
452 }
453
454 pub fn serialize(self) -> Result<Vec<u8>, EncodingError> {
463 self.serialize_with_version().map(|(_, bytes)| bytes)
464 }
465
466 pub fn serialize_with_version(self) -> Result<(EncodingVersion, Vec<u8>), EncodingError> {
477 let requires_v2 = match self.global.tx_version {
480 zcash_protocol::constants::V6_TX_VERSION => true,
481 #[cfg(zcash_unstable = "nutachyon")]
482 zcash_protocol::constants::V7_TX_VERSION => true,
483 _ => false,
484 };
485 if !requires_v2 && self.ironwood == orchard::EMPTY_IRONWOOD {
486 if let Ok(v1) = v1::Pczt::try_from(self.clone()) {
490 return Ok((EncodingVersion::V1, v1.serialize()));
491 }
492 }
493 Ok((EncodingVersion::V2, v2::Pczt::try_from(self)?.serialize()))
494 }
495
496 #[cfg(feature = "orchard")]
502 pub fn resolve_fields(&mut self) -> Result<(), ::orchard::pczt::ParseError> {
503 self.orchard.resolve_fields()?;
504 self.ironwood.resolve_fields()
505 }
506
507 #[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
514 pub(crate) fn extract_tx_data<A, E>(
515 self,
516 anchor_requirement: common::AnchorRequirement,
517 extract_transparent: impl FnOnce(
518 &::transparent::pczt::Bundle,
519 ) -> Result<
520 Option<::transparent::bundle::Bundle<A::TransparentAuth>>,
521 E,
522 >,
523 extract_sapling: impl FnOnce(
524 &::sapling::pczt::Bundle,
525 ) -> Result<
526 Option<::sapling::Bundle<A::SaplingAuth, zcash_protocol::value::ZatBalance>>,
527 E,
528 >,
529 extract_orchard: impl FnOnce(
530 &::orchard::pczt::Bundle,
531 ) -> Result<
532 Option<::orchard::Bundle<A::OrchardAuth, zcash_protocol::value::ZatBalance>>,
533 E,
534 >,
535 extract_ironwood: impl FnOnce(
536 &::orchard::pczt::Bundle,
537 ) -> Result<
538 Option<::orchard::Bundle<A::OrchardAuth, zcash_protocol::value::ZatBalance>>,
539 E,
540 >,
541 ) -> Result<ParsedPczt<A>, E>
542 where
543 A: Authorization,
544 E: From<ExtractError>,
545 {
546 let Pczt {
547 global,
548 transparent,
549 sapling,
550 orchard,
551 ironwood,
552 } = self;
553
554 let consensus_branch_id = BranchId::try_from(global.consensus_branch_id)
555 .map_err(|_| ExtractError::UnknownConsensusBranchId)?;
556 let orchard_protocol_revision = consensus_branch_id
557 .orchard_protocol_revision()
558 .ok_or(ExtractError::UnsupportedConsensusBranchId)?;
561
562 let version = match (global.tx_version, global.version_group_id) {
563 (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::V5),
564 (V6_TX_VERSION, V6_VERSION_GROUP_ID) => Ok(TxVersion::V6),
565 #[cfg(zcash_unstable = "nutachyon")]
566 (V7_TX_VERSION, V7_VERSION_GROUP_ID) => Ok(TxVersion::V7),
567 (version, version_group_id) => Err(ExtractError::UnsupportedTxVersion {
568 version,
569 version_group_id,
570 }),
571 }?;
572
573 match version {
574 TxVersion::Sprout(_) | TxVersion::V3 | TxVersion::V4 | TxVersion::V5 => {
576 if ironwood != crate::orchard::EMPTY_IRONWOOD {
577 return Err(ExtractError::IronwoodNotSupported.into());
578 }
579 }
580 TxVersion::V6 => {
583 if orchard_protocol_revision < OrchardProtocolRevision::V3 {
584 return Err(ExtractError::UnsupportedConsensusBranchId.into());
585 }
586 }
587 #[cfg(zcash_unstable = "nutachyon")]
588 TxVersion::V7 => {
589 if orchard_protocol_revision < OrchardProtocolRevision::V3 {
590 return Err(ExtractError::UnsupportedConsensusBranchId.into());
591 }
592 }
593 }
594
595 let transparent = transparent
596 .into_parsed()
597 .map_err(ExtractError::TransparentParse)?;
598 let sapling = sapling
599 .into_parsed(anchor_requirement)
600 .map_err(ExtractError::SaplingParse)?;
601 let orchard_bundle_version = crate::orchard::bundle_version_for_revision(
602 orchard_protocol_revision,
603 ::orchard::ValuePool::Orchard,
604 )
605 .expect("the Orchard pool is supported under every protocol revision");
606 let orchard = orchard
607 .into_parsed_with_version(orchard_bundle_version, anchor_requirement)
608 .map_err(ExtractError::OrchardParse)?;
609 let ironwood = ironwood
610 .into_ironwood_parsed(anchor_requirement)
611 .map_err(ExtractError::IronwoodParse)?;
612
613 let lock_time = determine_lock_time(&global, transparent.inputs())
614 .ok_or(ExtractError::IncompatibleLockTimes)?;
615
616 let transparent_bundle = extract_transparent(&transparent)?;
617 let sapling_bundle = extract_sapling(&sapling.bundle)?;
618 let orchard_bundle = extract_orchard(&orchard.bundle)?;
619 let ironwood_bundle = extract_ironwood(&ironwood.bundle)?;
620
621 let tx_data = match version {
622 TxVersion::V6 => TransactionData::from_parts_v6(
623 consensus_branch_id,
624 lock_time,
625 global.expiry_height.into(),
626 transparent_bundle,
627 sapling_bundle,
628 orchard_bundle,
629 ironwood_bundle,
630 ),
631 #[cfg(zcash_unstable = "nutachyon")]
632 TxVersion::V7 => TransactionData::from_parts_v7(
633 consensus_branch_id,
634 lock_time,
635 global.expiry_height.into(),
636 transparent_bundle,
637 sapling_bundle,
638 orchard_bundle,
639 ironwood_bundle,
640 ),
641 _ => TransactionData::from_parts(
642 version,
643 consensus_branch_id,
644 lock_time,
645 global.expiry_height.into(),
646 transparent_bundle,
647 None,
648 sapling_bundle,
649 orchard_bundle,
650 ),
651 };
652
653 Ok(ParsedPczt {
654 global,
655 transparent,
656 sapling,
657 orchard,
658 ironwood,
659 tx_data,
660 })
661 }
662
663 #[cfg(any(feature = "io-finalizer", feature = "signer"))]
665 pub fn into_effects(self) -> Result<TransactionData<EffectsOnly>, ExtractError> {
666 let anchor_requirement =
667 common::AnchorRequirement::for_pre_authorization(self.global.tx_version);
668
669 self.extract_tx_data(
670 anchor_requirement,
671 |t| {
672 t.extract_effects()
673 .map_err(ExtractError::TransparentExtract)
674 },
675 |s| s.extract_effects().map_err(ExtractError::SaplingExtract),
676 |o| o.extract_effects().map_err(ExtractError::OrchardExtract),
677 |i| i.extract_effects().map_err(ExtractError::IronwoodExtract),
678 )
679 .map(|parsed| parsed.tx_data)
680 }
681}
682
683#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
685#[cfg_attr(
686 not(any(feature = "io-finalizer", feature = "signer")),
687 allow(dead_code)
688)]
689pub(crate) struct ParsedPczt<A: Authorization> {
690 pub(crate) global: Global,
691 pub(crate) transparent: ::transparent::pczt::Bundle,
692 pub(crate) sapling: crate::sapling::Parsed,
693 pub(crate) orchard: crate::orchard::Parsed,
694 pub(crate) ironwood: crate::orchard::Parsed,
695 pub(crate) tx_data: TransactionData<A>,
696}
697
698#[cfg(any(feature = "io-finalizer", feature = "signer"))]
699pub struct EffectsOnly;
700
701#[cfg(any(feature = "io-finalizer", feature = "signer"))]
702impl Authorization for EffectsOnly {
703 type TransparentAuth = ::transparent::bundle::EffectsOnly;
704 type SaplingAuth = ::sapling::bundle::EffectsOnly;
705 type OrchardAuth = ::orchard::bundle::EffectsOnly;
706}
707
708#[cfg(any(feature = "io-finalizer", feature = "signer"))]
712pub(crate) fn precompute_sighash_data(
713 global: &Global,
714 tx_data: TransactionData<EffectsOnly>,
715) -> Result<PrecomputedSighashData<EffectsOnly>, ExtractError> {
716 PrecomputedSighashData::new_zip244(tx_data).map_err(|_| ExtractError::UnsupportedTxVersion {
717 version: global.tx_version,
718 version_group_id: global.version_group_id,
719 })
720}
721
722#[cfg(any(feature = "io-finalizer", feature = "signer"))]
724pub(crate) fn sighash(
725 sighash_data: &PrecomputedSighashData<EffectsOnly>,
726 signable_input: &SignableInput,
727) -> [u8; 32] {
728 *sighash_data.signature_hash(signable_input).as_ref()
729}
730
731#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
733#[derive(Debug)]
734#[non_exhaustive]
735pub enum ExtractError {
736 IncompatibleLockTimes,
738 IronwoodExtract(::orchard::pczt::TxExtractorError),
740 IronwoodNotSupported,
743 IronwoodParse(crate::orchard::ParseError),
745 OrchardExtract(::orchard::pczt::TxExtractorError),
747 OrchardParse(crate::orchard::ParseError),
749 SaplingExtract(::sapling::pczt::TxExtractorError),
751 SaplingParse(crate::sapling::ParseError),
753 TransparentExtract(::transparent::pczt::TxExtractorError),
756 TransparentParse(::transparent::pczt::ParseError),
758 UnknownConsensusBranchId,
761 UnsupportedConsensusBranchId,
764 UnsupportedTxVersion { version: u32, version_group_id: u32 },
766}
767
768#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
769impl core::fmt::Display for ExtractError {
770 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
771 match self {
772 ExtractError::IncompatibleLockTimes => {
773 write!(f, "the transparent inputs have incompatible lock times")
774 }
775 ExtractError::IronwoodExtract(e) => {
776 write!(f, "could not extract the Ironwood bundle: {e}")
777 }
778 ExtractError::IronwoodNotSupported => write!(
779 f,
780 "the PCZT carries Ironwood bundle data, but its transaction version has no \
781 Ironwood bundle"
782 ),
783 ExtractError::IronwoodParse(e) => {
784 write!(f, "could not parse the Ironwood bundle: {e:?}")
785 }
786 ExtractError::OrchardExtract(e) => {
787 write!(f, "could not extract the Orchard bundle: {e}")
788 }
789 ExtractError::OrchardParse(e) => write!(f, "could not parse the Orchard bundle: {e:?}"),
790 ExtractError::SaplingExtract(e) => {
791 write!(f, "could not extract the Sapling bundle: {e}")
792 }
793 ExtractError::SaplingParse(e) => write!(f, "could not parse the Sapling bundle: {e:?}"),
794 ExtractError::TransparentExtract(e) => {
795 write!(f, "could not extract the transparent bundle: {e:?}")
796 }
797 ExtractError::TransparentParse(e) => {
798 write!(f, "could not parse the transparent bundle: {e:?}")
799 }
800 ExtractError::UnknownConsensusBranchId => write!(f, "unknown consensus branch ID"),
801 ExtractError::UnsupportedConsensusBranchId => write!(
802 f,
803 "the consensus branch ID predates the v5 transaction format"
804 ),
805 ExtractError::UnsupportedTxVersion {
806 version,
807 version_group_id,
808 } => write!(
809 f,
810 "unsupported transaction version {version} (version group ID {version_group_id})"
811 ),
812 }
813 }
814}
815
816#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
817impl core::error::Error for ExtractError {}
818
819#[derive(Debug)]
821pub enum ParseError {
822 NotPczt,
824 Invalid(postcard::Error),
826 MissingRequiredField(&'static str),
829 TooShort,
831 UnknownVersion(u32),
833}
834
835impl core::fmt::Display for ParseError {
836 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
837 match self {
838 ParseError::NotPczt => write!(f, "the bytes do not contain a PCZT"),
839 ParseError::Invalid(e) => write!(f, "invalid PCZT encoding: {e}"),
840 ParseError::MissingRequiredField(field) => {
841 write!(f, "the PCZT encoding omitted the required field {field}")
842 }
843 ParseError::TooShort => write!(f, "the bytes are too short to contain a PCZT"),
844 ParseError::UnknownVersion(v) => write!(f, "unknown PCZT version {v}"),
845 }
846 }
847}
848
849impl core::error::Error for ParseError {}
850
851#[cfg(all(test, any(feature = "io-finalizer", feature = "signer")))]
852mod extraction_tests {
853 use zcash_primitives::transaction::{
854 TxVersion,
855 sighash::{PrecomputedSighashData, SignableInput},
856 txid::{TxIdDigester, to_txid},
857 };
858 use zcash_protocol::consensus::BranchId;
859
860 use crate::{ExtractError, roles::creator::Creator};
861
862 #[test]
863 fn nu7_pczt_preserves_canonical_v6_digests() {
864 const EXPIRY_HEIGHT: u32 = 1;
866 const COIN_TYPE: u32 = 133;
868 const EXPECTED_DIGEST: &str =
870 "78296c68a370c2e1f058d997c81c7b011c4562c52fc5ca2994ad59cd179fbe9e";
871 let pczt = Creator::new(BranchId::Nu7.into(), EXPIRY_HEIGHT, COIN_TYPE, None, None)
872 .unwrap()
873 .build()
874 .unwrap();
875 let data = pczt.into_effects().unwrap();
876 assert_eq!(data.version(), TxVersion::V6);
877 let digests = data.digest(TxIdDigester);
878 let expected = hex::decode(EXPECTED_DIGEST).unwrap();
879 assert_eq!(
880 to_txid(data.version(), data.consensus_branch_id(), &digests)
881 .expect("Creator::new returns a compatible version")
882 .as_ref()
883 .as_slice(),
884 expected
885 );
886 assert_eq!(
887 PrecomputedSighashData::new_zip244(data)
888 .unwrap()
889 .signature_hash(&SignableInput::Shielded)
890 .as_ref()
891 .as_slice(),
892 expected
893 );
894 }
895
896 #[test]
897 fn v5_pczt_with_ironwood_data_does_not_extract() {
898 let mut pczt = Creator::new(
899 BranchId::Nu6.into(),
900 10_000_000,
901 133,
902 Some([0; 32]),
903 Some([0; 32]),
904 )
905 .unwrap()
906 .build()
907 .unwrap();
908 pczt.ironwood.bsk = Some(crate::common::SecretKeyBytes::new([1; 32]));
909 assert!(matches!(
910 pczt.into_effects(),
911 Err(ExtractError::IronwoodNotSupported)
912 ));
913 }
914
915 #[test]
916 fn v6_pczt_with_pre_nu6_3_branch_does_not_extract() {
917 let mut pczt = Creator::new(
918 BranchId::Nu6_3.into(),
919 10_000_000,
920 133,
921 Some([0; 32]),
922 Some([0; 32]),
923 )
924 .unwrap()
925 .build()
926 .unwrap();
927 pczt.global.consensus_branch_id = BranchId::Nu6_2.into();
928 assert!(matches!(
929 pczt.into_effects(),
930 Err(ExtractError::UnsupportedConsensusBranchId)
931 ));
932 }
933
934 #[test]
935 #[cfg(zcash_unstable = "nutachyon")]
936 fn nu_tachyon_pczt_extracts_as_v7() {
937 let pczt = Creator::new(
938 BranchId::NuTachyon.into(),
939 10_000_000,
940 133,
941 Some([0; 32]),
942 Some([0; 32]),
943 )
944 .unwrap()
945 .build()
946 .unwrap();
947
948 let tx_data = pczt.into_effects().unwrap();
949 assert_eq!(tx_data.version(), TxVersion::V7);
950 assert_eq!(tx_data.consensus_branch_id(), BranchId::NuTachyon);
951 }
952}
953
954#[cfg(test)]
955mod serialize_tests {
956 use zcash_protocol::consensus::BranchId;
957
958 use crate::{EncodingVersion, roles::creator::Creator};
959
960 fn encoding_version(bytes: &[u8]) -> u32 {
961 assert_eq!(&bytes[..4], crate::MAGIC_BYTES);
962 u32::from_le_bytes(bytes[4..8].try_into().unwrap())
963 }
964
965 #[test]
966 fn serialize_emits_minimal_encoding() {
967 let pczt = Creator::new(
969 BranchId::Nu6.into(),
970 10_000_000,
971 133,
972 Some([0; 32]),
973 Some([0; 32]),
974 )
975 .unwrap()
976 .build()
977 .unwrap();
978 let bytes = pczt.clone().serialize().unwrap();
979 assert_eq!(encoding_version(&bytes), crate::PCZT_VERSION_1);
980 assert!(crate::Pczt::parse(&bytes).is_ok());
982
983 let mut with_ironwood = pczt.clone();
985 with_ironwood.ironwood.bsk = Some(crate::common::SecretKeyBytes::new([1; 32]));
986 assert_eq!(
987 encoding_version(&with_ironwood.serialize().unwrap()),
988 crate::PCZT_VERSION_2,
989 );
990
991 let mut with_note_v3 = pczt;
994 with_note_v3.orchard.note_version = crate::orchard::NoteVersion::V3;
995 assert_eq!(
996 encoding_version(&with_note_v3.serialize().unwrap()),
997 crate::PCZT_VERSION_2,
998 );
999
1000 let v6 = Creator::new(
1002 BranchId::Nu6_3.into(),
1003 10_000_000,
1004 133,
1005 Some([0; 32]),
1006 Some([0; 32]),
1007 )
1008 .unwrap()
1009 .build()
1010 .unwrap();
1011 assert_eq!(
1012 encoding_version(&v6.serialize().unwrap()),
1013 crate::PCZT_VERSION_2,
1014 );
1015 }
1016
1017 #[test]
1018 fn serialize_with_version_reports_the_encoding_used() {
1019 let v5 = Creator::new(
1021 BranchId::Nu6.into(),
1022 10_000_000,
1023 133,
1024 Some([0; 32]),
1025 Some([0; 32]),
1026 )
1027 .unwrap()
1028 .build()
1029 .unwrap();
1030
1031 let v6 = Creator::new(
1033 BranchId::Nu6_3.into(),
1034 10_000_000,
1035 133,
1036 Some([0; 32]),
1037 Some([0; 32]),
1038 )
1039 .unwrap()
1040 .build()
1041 .unwrap();
1042
1043 for (pczt, expected, expected_header) in [
1044 (v5, EncodingVersion::V1, crate::PCZT_VERSION_1),
1045 (v6, EncodingVersion::V2, crate::PCZT_VERSION_2),
1046 ] {
1047 let (version, bytes) = pczt.clone().serialize_with_version().unwrap();
1048
1049 assert_eq!(version, expected);
1051 assert_eq!(encoding_version(&bytes), expected_header);
1052
1053 assert_eq!(pczt.serialize().unwrap(), bytes);
1055 }
1056 }
1057}