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 = "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
100pub fn parse(bytes: &[u8]) -> Result<Pczt, ParseError> {
102 Pczt::parse(bytes)
103}
104
105#[derive(Clone, Debug, Getters)]
107pub struct Pczt {
108 #[getset(get = "pub")]
110 pub(crate) global: common::Global,
111
112 #[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
130pub mod v1 {
132 use alloc::vec::Vec;
133 use serde::{Deserialize, Serialize};
134
135 use crate::{common, orchard, sapling, transparent};
136
137 #[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 impl TryFrom<super::Pczt> for Pczt {
155 type Error = super::EncodingError;
156
157 fn try_from(pczt: super::Pczt) -> Result<Self, Self::Error> {
158 if pczt.global.tx_version == zcash_protocol::constants::V6_TX_VERSION {
161 return Err(super::EncodingError::UnsupportedTxVersion);
162 }
163
164 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 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 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
238pub mod v2 {
240 use alloc::vec::Vec;
241 use serde::{Deserialize, Serialize};
242
243 use crate::{common, orchard, sapling, transparent};
244
245 #[derive(Clone, Debug, Serialize, Deserialize)]
247 pub struct Pczt {
248 global: common::Global,
249 transparent: Option<transparent::Bundle>,
252 sapling: Option<sapling::Bundle>,
255 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 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 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 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 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#[derive(Debug)]
403#[non_exhaustive]
404pub enum EncodingError {
405 UnsupportedTxVersion,
408 UnsupportedOrchardNoteVersion,
410 RequiresV2,
412}
413
414impl Pczt {
415 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 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 pub fn serialize(self) -> Result<Vec<u8>, EncodingError> {
457 if self.global.tx_version != zcash_protocol::constants::V6_TX_VERSION
460 && self.ironwood == orchard::EMPTY_IRONWOOD
461 {
462 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 #[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 #[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 .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 TxVersion::Sprout(_) | TxVersion::V3 | TxVersion::V4 | TxVersion::V5 => {
550 if ironwood != crate::orchard::EMPTY_IRONWOOD {
551 return Err(ExtractError::IronwoodNotSupported.into());
552 }
553 }
554 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 #[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#[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#[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#[cfg(any(feature = "io-finalizer", feature = "signer", feature = "tx-extractor"))]
689#[derive(Debug)]
690#[non_exhaustive]
691pub enum ExtractError {
692 IncompatibleLockTimes,
694 IronwoodExtract(::orchard::pczt::TxExtractorError),
696 IronwoodNotSupported,
699 IronwoodParse(crate::orchard::ParseError),
701 OrchardExtract(::orchard::pczt::TxExtractorError),
703 OrchardParse(crate::orchard::ParseError),
705 SaplingExtract(::sapling::pczt::TxExtractorError),
707 SaplingParse(crate::sapling::ParseError),
709 TransparentExtract(::transparent::pczt::TxExtractorError),
712 TransparentParse(::transparent::pczt::ParseError),
714 UnknownConsensusBranchId,
717 UnsupportedConsensusBranchId,
720 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#[derive(Debug)]
777pub enum ParseError {
778 NotPczt,
780 Invalid(postcard::Error),
782 MissingRequiredField(&'static str),
785 TooShort,
787 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 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 assert!(crate::Pczt::parse(&bytes).is_ok());
880
881 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 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 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}