1use alloc::{collections::BTreeMap, string::String, vec::Vec};
4use core::{cmp::Ordering, fmt};
5
6use getset::Getters;
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
8
9#[cfg(feature = "orchard")]
11pub(crate) use orchard::note::NoteVersion;
12
13#[cfg(feature = "orchard")]
14use {
15 ::orchard::{
16 Address, Note, ValuePool,
17 bundle::BundleVersion,
18 note::{ExtractedNoteCommitment, Nullifier, RandomSeed, Rho},
19 note_encryption::{
20 COMPACT_NOTE_SIZE, CompactAction, CompactNoteCiphertextBytes, IronwoodDomain,
21 NoteBytesData, NoteCiphertextBytes, OrchardDomain, OrchardNoteEncryption,
22 },
23 value::{NoteValue, ValueCommitTrapdoor, ValueCommitment},
24 },
25 ff::PrimeField,
26 zcash_note_encryption::{
27 Domain, EphemeralKeyBytes, ShieldedOutput, note_bytes::NoteBytes,
28 try_output_recovery_with_pkd_esk,
29 },
30 zcash_protocol::consensus::{BranchId, OrchardProtocolRevision},
31};
32
33use crate::{
34 common::{Global, SecretKeyBytes, Zip32Derivation},
35 roles::combiner::{merge_map, merge_optional},
36};
37
38#[cfg(not(feature = "orchard"))]
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub(crate) enum NoteVersion {
41 V2,
42 V3,
43}
44
45#[derive(Clone, Debug, PartialEq, Getters)]
47pub struct Bundle {
48 #[getset(get = "pub")]
53 pub(crate) actions: Vec<Action>,
54
55 #[getset(get = "pub")]
65 pub(crate) flags: u8,
66
67 #[getset(get = "pub")]
73 pub(crate) value_sum: (u64, bool),
74
75 #[getset(get = "pub")]
79 pub(crate) anchor: Option<[u8; 32]>,
80
81 pub(crate) note_version: NoteVersion,
83
84 #[getset(get = "pub")]
88 pub(crate) zkproof: Option<Vec<u8>>,
89
90 pub(crate) bsk: Option<SecretKeyBytes>,
95}
96
97pub(crate) const ORCHARD_SPENDS_AND_OUTPUTS_ENABLED: u8 = 0b0000_0011;
101
102pub(crate) const IRONWOOD_SPENDS_OUTPUTS_AND_CROSS_ADDRESS_ENABLED: u8 = 0b0000_0111;
106
107pub(crate) const MEMO_SIZE: usize = 512;
109
110pub(crate) const DEFAULT_ANCHOR: [u8; 32] = [0; 32];
111
112pub(crate) const EMPTY_ORCHARD: Bundle = Bundle {
117 actions: Vec::new(),
118 flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
119 value_sum: (0, false),
120 anchor: None,
121 note_version: NoteVersion::V2,
122 zkproof: None,
123 bsk: None,
124};
125
126pub(crate) const EMPTY_IRONWOOD: Bundle = Bundle {
128 actions: Vec::new(),
129 flags: IRONWOOD_SPENDS_OUTPUTS_AND_CROSS_ADDRESS_ENABLED,
130 value_sum: (0, false),
131 anchor: None,
132 note_version: NoteVersion::V3,
133 zkproof: None,
134 bsk: None,
135};
136
137#[derive(Debug, PartialEq, Eq)]
139#[non_exhaustive]
140enum MemoPlaintextError {
141 TooLong,
143 NotStripped,
145}
146
147impl fmt::Display for MemoPlaintextError {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 match self {
150 MemoPlaintextError::TooLong => {
151 write!(f, "memo plaintext exceeds {MEMO_SIZE} bytes")
152 }
153 MemoPlaintextError::NotStripped => {
154 write!(f, "memo plaintext has trailing zero bytes")
155 }
156 }
157 }
158}
159
160#[cfg(feature = "orchard")]
168pub(crate) struct Parsed {
169 pub(crate) bundle: orchard::pczt::Bundle,
170 pub(crate) wire_anchor: Option<[u8; 32]>,
171}
172
173#[cfg(feature = "orchard")]
174impl Parsed {
175 pub(crate) fn reserialize(self) -> Bundle {
182 Bundle {
183 anchor: self.wire_anchor,
184 ..Bundle::serialize_from(self.bundle)
185 }
186 }
187}
188
189#[cfg(all(test, feature = "orchard"))]
191pub(crate) mod testing {
192 #[cfg(any(feature = "prover", all(feature = "signer", feature = "io-finalizer")))]
193 use {
194 super::{Action, EncCiphertext, Output, Spend},
195 alloc::collections::BTreeMap,
196 ff::Field,
197 pasta_curves::pallas,
198 };
199
200 pub(crate) fn value_commitment(value: u64, rcv: [u8; 32]) -> [u8; 32] {
204 let rcv = orchard::value::ValueCommitTrapdoor::from_bytes(rcv)
205 .into_option()
206 .unwrap();
207 let value_sum =
208 orchard::value::NoteValue::from_raw(value) - orchard::value::NoteValue::from_raw(0);
209 orchard::value::ValueCommitment::derive(value_sum, rcv).to_bytes()
210 }
211
212 #[cfg(any(feature = "prover", all(feature = "signer", feature = "io-finalizer")))]
216 pub(crate) fn randomized_verification_key() -> [u8; 32] {
217 let sk = orchard::keys::SpendingKey::from_bytes([7; 32]).unwrap();
218 let ask = orchard::keys::SpendAuthorizingKey::from(&sk);
219 let randomized_signing_key = ask.randomize(&pallas::Scalar::ONE);
220 let rk: orchard::primitives::redpallas::VerificationKey<
221 orchard::primitives::redpallas::SpendAuth,
222 > = (&randomized_signing_key).into();
223 (&rk).into()
224 }
225
226 #[cfg(any(feature = "prover", all(feature = "signer", feature = "io-finalizer")))]
229 pub(crate) fn dummy_action() -> Action {
230 Action {
231 cv_net: Some(value_commitment(0, [3; 32])),
232 spend: Spend {
233 nullifier: [2; 32],
234 rk: randomized_verification_key(),
235 spend_auth_sig: None,
236 recipient: None,
237 value: None,
238 rho: None,
239 rseed: None,
240 fvk: None,
241 witness: None,
242 alpha: None,
243 zip32_derivation: None,
244 dummy_sk: None,
245 proprietary: BTreeMap::new(),
246 },
247 output: Output {
248 cmx: Some([4; 32]),
249 ephemeral_key: [5; 32],
250 enc_ciphertext: EncCiphertext::Encrypted(alloc::vec![6; 580]),
251 out_ciphertext: alloc::vec![7; 80],
252 recipient: None,
253 value: None,
254 rseed: None,
255 ock: None,
256 zip32_derivation: None,
257 user_address: None,
258 proprietary: BTreeMap::new(),
259 },
260 rcv: None,
261 }
262 }
263}
264
265#[derive(Clone, Debug, PartialEq, Eq)]
271pub struct MemoPlaintext(Vec<u8>);
272
273impl MemoPlaintext {
274 pub fn from_memo(memo: [u8; MEMO_SIZE]) -> Self {
276 let len = memo.iter().rposition(|b| *b != 0).map_or(0, |i| i + 1);
277 Self(memo[..len].to_vec())
278 }
279
280 fn from_stripped_bytes(bytes: Vec<u8>) -> Result<Self, MemoPlaintextError> {
285 if bytes.len() > MEMO_SIZE {
286 Err(MemoPlaintextError::TooLong)
287 } else if bytes.last() == Some(&0) {
288 Err(MemoPlaintextError::NotStripped)
289 } else {
290 Ok(Self(bytes))
291 }
292 }
293
294 pub fn as_stripped_bytes(&self) -> &[u8] {
296 &self.0
297 }
298
299 pub fn to_memo(&self) -> [u8; MEMO_SIZE] {
301 let mut memo = [0; MEMO_SIZE];
302 memo[..self.0.len()].copy_from_slice(&self.0);
303 memo
304 }
305}
306
307impl Serialize for MemoPlaintext {
308 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
309 where
310 S: Serializer,
311 {
312 self.0.serialize(serializer)
313 }
314}
315
316impl<'de> Deserialize<'de> for MemoPlaintext {
317 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
318 where
319 D: Deserializer<'de>,
320 {
321 let bytes = Vec::<u8>::deserialize(deserializer)?;
322 Self::from_stripped_bytes(bytes).map_err(de::Error::custom)
323 }
324}
325
326#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
335pub enum EncCiphertext {
336 Encrypted(Vec<u8>),
338 MemoPlaintext(MemoPlaintext),
340}
341
342impl EncCiphertext {
343 pub fn into_encrypted(self) -> Option<Vec<u8>> {
345 match self {
346 EncCiphertext::Encrypted(ciphertext) => Some(ciphertext),
347 EncCiphertext::MemoPlaintext(_) => None,
348 }
349 }
350}
351
352#[cfg(feature = "orchard")]
353fn recover_memo_plaintext_from_ciphertext_and_action(
354 action: &Action,
355 note_version: NoteVersion,
356) -> Option<MemoPlaintext> {
357 struct OutputRecoveryData {
358 cmx: ExtractedNoteCommitment,
359 ephemeral_key: [u8; 32],
360 enc_ciphertext: NoteCiphertextBytes,
361 }
362
363 impl<D> ShieldedOutput<D> for OutputRecoveryData
364 where
365 D: Domain<
366 ExtractedCommitment = ExtractedNoteCommitment,
367 NoteCiphertextBytes = NoteCiphertextBytes,
368 CompactNoteCiphertextBytes = CompactNoteCiphertextBytes,
369 >,
370 {
371 fn ephemeral_key(&self) -> EphemeralKeyBytes {
372 EphemeralKeyBytes(self.ephemeral_key)
373 }
374
375 fn cmstar(&self) -> &ExtractedNoteCommitment {
376 &self.cmx
377 }
378
379 fn enc_ciphertext(&self) -> Option<&NoteCiphertextBytes> {
380 Some(&self.enc_ciphertext)
381 }
382
383 fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
384 NoteBytesData(
385 self.enc_ciphertext.0[..COMPACT_NOTE_SIZE]
386 .try_into()
387 .expect("COMPACT_NOTE_SIZE <= ENC_CIPHERTEXT_SIZE"),
388 )
389 }
390 }
391
392 fn recover_with_domain<D>(
393 domain: &D,
394 note: &Note,
395 output: &OutputRecoveryData,
396 ) -> Option<MemoPlaintext>
397 where
398 D: Domain<
399 Note = Note,
400 Memo = [u8; MEMO_SIZE],
401 ExtractedCommitment = ExtractedNoteCommitment,
402 NoteCiphertextBytes = NoteCiphertextBytes,
403 CompactNoteCiphertextBytes = CompactNoteCiphertextBytes,
404 >,
405 {
406 let pk_d = D::get_pk_d(note);
407 let esk = D::derive_esk(note)?;
408
409 try_output_recovery_with_pkd_esk(domain, pk_d, esk, output)
410 .map(|(_, _, memo)| MemoPlaintext::from_memo(memo))
411 }
412
413 let enc_ciphertext = match &action.output.enc_ciphertext {
414 EncCiphertext::Encrypted(ciphertext) => NoteCiphertextBytes::from_slice(ciphertext)?,
415 EncCiphertext::MemoPlaintext(_) => return None,
417 };
418 let recipient = Option::from(Address::from_raw_address_bytes(
419 action.output.recipient.as_ref()?,
420 ))?;
421 let rho = Option::from(Rho::from_bytes(&action.spend.nullifier))?;
422 let rseed = Option::from(RandomSeed::from_bytes(*action.output.rseed.as_ref()?, &rho))?;
423 let note = Option::from(Note::from_parts(
424 recipient,
425 NoteValue::from_raw(action.output.value?),
426 rho,
427 rseed,
428 note_version,
429 ))?;
430
431 let nullifier = Option::from(Nullifier::from_bytes(&action.spend.nullifier))?;
432 let cmx = Option::from(ExtractedNoteCommitment::from_bytes(&action.output.cmx?))?;
435 let output = OutputRecoveryData {
436 cmx,
437 ephemeral_key: action.output.ephemeral_key,
438 enc_ciphertext,
439 };
440 let compact_action = CompactAction::from_parts(
441 nullifier,
442 cmx,
443 EphemeralKeyBytes(action.output.ephemeral_key),
444 output.enc_ciphertext.0[..COMPACT_NOTE_SIZE]
445 .try_into()
446 .ok()?,
447 );
448
449 match note_version {
450 NoteVersion::V2 => recover_with_domain(
451 &OrchardDomain::for_compact_action(&compact_action),
452 ¬e,
453 &output,
454 ),
455 NoteVersion::V3 => recover_with_domain(
456 &IronwoodDomain::for_compact_action(&compact_action),
457 ¬e,
458 &output,
459 ),
460 }
461}
462
463#[cfg(feature = "orchard")]
464impl Action {
465 pub(crate) fn replace_enc_ciphertext_with_decrypted_memo_plaintext(
466 &mut self,
467 note_version: NoteVersion,
468 ) {
469 if let Some(memo) = recover_memo_plaintext_from_ciphertext_and_action(self, note_version) {
470 self.output.enc_ciphertext = EncCiphertext::MemoPlaintext(memo);
471 }
472 }
473
474 pub(crate) fn compact_resolvable_fields(&mut self, note_version: NoteVersion) {
475 let original_enc_ciphertext = self.output.enc_ciphertext.clone();
476 self.replace_enc_ciphertext_with_decrypted_memo_plaintext(note_version);
477 if self.output.enc_ciphertext != original_enc_ciphertext {
478 let mut resolved = self.output.clone();
479 if resolved
480 .encrypt_ciphertext_from_memo(note_version, self.spend.nullifier)
481 .is_err()
482 || resolved.enc_ciphertext != original_enc_ciphertext
483 {
484 self.output.enc_ciphertext = original_enc_ciphertext;
485 }
486 }
487
488 if let Some(original_cv_net) = self.cv_net {
489 let mut resolved = self.clone();
490 resolved.cv_net = None;
491 if resolved.resolve_cv_net().is_ok() && resolved.cv_net == Some(original_cv_net) {
492 self.cv_net = None;
493 }
494 }
495
496 if let Some(original_cmx) = self.output.cmx {
497 let mut resolved = self.output.clone();
498 resolved.cmx = None;
499 if resolved
500 .resolve_cmx(note_version, self.spend.nullifier)
501 .is_ok()
502 && resolved.cmx == Some(original_cmx)
503 {
504 self.output.cmx = None;
505 }
506 }
507 }
508}
509
510#[derive(Clone, Debug, PartialEq, Getters)]
512pub struct Action {
513 #[getset(get = "pub")]
521 pub(crate) cv_net: Option<[u8; 32]>,
522 #[getset(get = "pub")]
523 pub(crate) spend: Spend,
524 #[getset(get = "pub")]
525 pub(crate) output: Output,
526
527 pub(crate) rcv: Option<[u8; 32]>,
537}
538
539#[derive(Clone, Debug, PartialEq, Getters)]
541pub struct Spend {
542 #[getset(get = "pub")]
549 pub(crate) nullifier: [u8; 32],
550 #[getset(get = "pub")]
551 pub(crate) rk: [u8; 32],
552
553 #[getset(get = "pub")]
557 pub(crate) spend_auth_sig: Option<[u8; 64]>,
558
559 pub(crate) recipient: Option<[u8; 43]>,
566
567 pub(crate) value: Option<u64>,
576
577 pub(crate) rho: Option<[u8; 32]>,
582
583 pub(crate) rseed: Option<[u8; 32]>,
588
589 pub(crate) fvk: Option<[u8; 96]>,
594
595 #[getset(get = "pub")]
600 pub(crate) witness: Option<(u32, [[u8; 32]; 32])>,
601
602 pub(crate) alpha: Option<[u8; 32]>,
609
610 pub(crate) zip32_derivation: Option<Zip32Derivation>,
613
614 #[getset(get = "pub")]
620 pub(crate) dummy_sk: Option<SecretKeyBytes>,
621
622 #[getset(get = "pub")]
624 pub(crate) proprietary: BTreeMap<String, Vec<u8>>,
625}
626
627#[derive(Clone, Debug, PartialEq, Getters)]
629pub struct Output {
630 #[getset(get = "pub")]
637 pub(crate) cmx: Option<[u8; 32]>,
638 #[getset(get = "pub")]
639 pub(crate) ephemeral_key: [u8; 32],
640 #[getset(get = "pub")]
643 pub(crate) enc_ciphertext: EncCiphertext,
644 #[getset(get = "pub")]
648 pub(crate) out_ciphertext: Vec<u8>,
649
650 #[getset(get = "pub")]
657 pub(crate) recipient: Option<[u8; 43]>,
658
659 #[getset(get = "pub")]
667 pub(crate) value: Option<u64>,
668
669 #[getset(get = "pub")]
674 pub(crate) rseed: Option<[u8; 32]>,
675
676 pub(crate) ock: Option<[u8; 32]>,
683
684 pub(crate) zip32_derivation: Option<Zip32Derivation>,
686
687 #[getset(get = "pub")]
693 pub(crate) user_address: Option<String>,
694
695 #[getset(get = "pub")]
697 pub(crate) proprietary: BTreeMap<String, Vec<u8>>,
698}
699
700pub mod v1 {
702 use alloc::{collections::BTreeMap, string::String, vec::Vec};
703
704 use serde::{Deserialize, Serialize};
705 use serde_with::serde_as;
706
707 use crate::common::Zip32Derivation;
708
709 use super::NoteVersion;
710
711 #[derive(Clone, Debug, Serialize, Deserialize)]
713 pub struct Bundle {
714 actions: Vec<Action>,
715 flags: u8,
716 value_sum: (u64, bool),
717 anchor: [u8; 32],
718 zkproof: Option<Vec<u8>>,
719 bsk: Option<crate::common::SecretKeyBytes>,
720 }
721
722 #[derive(Clone, Debug, Serialize, Deserialize)]
724 pub(crate) struct Action {
725 cv_net: [u8; 32],
726 spend: Spend,
727 output: Output,
728 rcv: Option<[u8; 32]>,
729 }
730
731 #[serde_as]
733 #[derive(Clone, Debug, Serialize, Deserialize)]
734 pub(crate) struct Spend {
735 nullifier: [u8; 32],
736 rk: [u8; 32],
737 #[serde_as(as = "Option<[_; 64]>")]
738 spend_auth_sig: Option<[u8; 64]>,
739 #[serde_as(as = "Option<[_; 43]>")]
740 recipient: Option<[u8; 43]>,
741 value: Option<u64>,
742 rho: Option<[u8; 32]>,
743 rseed: Option<[u8; 32]>,
744 #[serde_as(as = "Option<[_; 96]>")]
745 fvk: Option<[u8; 96]>,
746 witness: Option<(u32, [[u8; 32]; 32])>,
747 alpha: Option<[u8; 32]>,
748 zip32_derivation: Option<Zip32Derivation>,
749 dummy_sk: Option<crate::common::SecretKeyBytes>,
750 proprietary: BTreeMap<String, Vec<u8>>,
751 }
752
753 #[serde_as]
755 #[derive(Clone, Debug, Serialize, Deserialize)]
756 pub(crate) struct Output {
757 cmx: [u8; 32],
758 ephemeral_key: [u8; 32],
759 enc_ciphertext: Vec<u8>,
760 out_ciphertext: Vec<u8>,
761 #[serde_as(as = "Option<[_; 43]>")]
762 recipient: Option<[u8; 43]>,
763 value: Option<u64>,
764 rseed: Option<[u8; 32]>,
765 ock: Option<[u8; 32]>,
766 zip32_derivation: Option<Zip32Derivation>,
767 user_address: Option<String>,
768 proprietary: BTreeMap<String, Vec<u8>>,
769 }
770
771 impl TryFrom<super::Bundle> for Bundle {
772 type Error = crate::EncodingError;
773
774 fn try_from(bundle: super::Bundle) -> Result<Self, Self::Error> {
775 if bundle.note_version != NoteVersion::V2 {
776 return Err(crate::EncodingError::UnsupportedOrchardNoteVersion);
777 }
778
779 let anchor = match bundle.anchor {
780 Some(anchor) => anchor,
781 None if bundle.actions.is_empty() => super::DEFAULT_ANCHOR,
782 None => return Err(crate::EncodingError::RequiresV2),
783 };
784
785 Ok(Self {
786 actions: bundle
787 .actions
788 .into_iter()
789 .map(Action::try_from)
790 .collect::<Result<Vec<_>, _>>()?,
791 flags: bundle.flags,
792 value_sum: bundle.value_sum,
793 anchor,
794 zkproof: bundle.zkproof,
795 bsk: bundle.bsk,
796 })
797 }
798 }
799
800 impl From<Bundle> for super::Bundle {
801 fn from(bundle: Bundle) -> Self {
802 let anchor = if bundle.actions.is_empty() && bundle.anchor == super::DEFAULT_ANCHOR {
809 None
810 } else {
811 Some(bundle.anchor)
812 };
813
814 Self {
815 actions: bundle
816 .actions
817 .into_iter()
818 .map(super::Action::from)
819 .collect(),
820 flags: bundle.flags,
821 value_sum: bundle.value_sum,
822 anchor,
823 note_version: NoteVersion::V2,
824 zkproof: bundle.zkproof,
825 bsk: bundle.bsk,
826 }
827 }
828 }
829
830 impl TryFrom<super::Action> for Action {
831 type Error = crate::EncodingError;
832
833 fn try_from(action: super::Action) -> Result<Self, Self::Error> {
834 Ok(Self {
835 cv_net: action.cv_net.ok_or(crate::EncodingError::RequiresV2)?,
836 spend: Spend::from(action.spend),
837 output: Output::try_from(action.output)?,
838 rcv: action.rcv,
839 })
840 }
841 }
842
843 impl From<Action> for super::Action {
844 fn from(action: Action) -> Self {
845 Self {
846 cv_net: Some(action.cv_net),
847 spend: super::Spend::from(action.spend),
848 output: super::Output::from(action.output),
849 rcv: action.rcv,
850 }
851 }
852 }
853
854 impl From<super::Spend> for Spend {
855 fn from(spend: super::Spend) -> Self {
856 Self {
857 nullifier: spend.nullifier,
858 rk: spend.rk,
859 spend_auth_sig: spend.spend_auth_sig,
860 recipient: spend.recipient,
861 value: spend.value,
862 rho: spend.rho,
863 rseed: spend.rseed,
864 fvk: spend.fvk,
865 witness: spend.witness,
866 alpha: spend.alpha,
867 zip32_derivation: spend.zip32_derivation,
868 dummy_sk: spend.dummy_sk,
869 proprietary: spend.proprietary,
870 }
871 }
872 }
873
874 impl From<Spend> for super::Spend {
875 fn from(spend: Spend) -> Self {
876 Self {
877 nullifier: spend.nullifier,
878 rk: spend.rk,
879 spend_auth_sig: spend.spend_auth_sig,
880 recipient: spend.recipient,
881 value: spend.value,
882 rho: spend.rho,
883 rseed: spend.rseed,
884 fvk: spend.fvk,
885 witness: spend.witness,
886 alpha: spend.alpha,
887 zip32_derivation: spend.zip32_derivation,
888 dummy_sk: spend.dummy_sk,
889 proprietary: spend.proprietary,
890 }
891 }
892 }
893
894 impl TryFrom<super::Output> for Output {
895 type Error = crate::EncodingError;
896
897 fn try_from(output: super::Output) -> Result<Self, Self::Error> {
898 let enc_ciphertext = output
899 .enc_ciphertext
900 .into_encrypted()
901 .ok_or(crate::EncodingError::RequiresV2)?;
902
903 Ok(Self {
904 cmx: output.cmx.ok_or(crate::EncodingError::RequiresV2)?,
905 ephemeral_key: output.ephemeral_key,
906 enc_ciphertext,
907 out_ciphertext: output.out_ciphertext,
908 recipient: output.recipient,
909 value: output.value,
910 rseed: output.rseed,
911 ock: output.ock,
912 zip32_derivation: output.zip32_derivation,
913 user_address: output.user_address,
914 proprietary: output.proprietary,
915 })
916 }
917 }
918
919 impl From<Output> for super::Output {
920 fn from(output: Output) -> Self {
921 Self {
922 cmx: Some(output.cmx),
923 ephemeral_key: output.ephemeral_key,
924 enc_ciphertext: super::EncCiphertext::Encrypted(output.enc_ciphertext),
925 out_ciphertext: output.out_ciphertext,
926 recipient: output.recipient,
927 value: output.value,
928 rseed: output.rseed,
929 ock: output.ock,
930 zip32_derivation: output.zip32_derivation,
931 user_address: output.user_address,
932 proprietary: output.proprietary,
933 }
934 }
935 }
936}
937
938pub(crate) mod v2 {
940 use alloc::{collections::BTreeMap, string::String, vec::Vec};
941
942 use getset::Getters;
943 use serde::{Deserialize, Serialize};
944 use serde_with::serde_as;
945
946 use super::NoteVersion;
947
948 #[derive(Clone, Copy, Debug, Serialize, Deserialize)]
950 enum SerializedNoteVersion {
951 V2,
952 V3,
953 }
954
955 impl From<NoteVersion> for SerializedNoteVersion {
956 fn from(note_version: NoteVersion) -> Self {
957 match note_version {
958 NoteVersion::V2 => Self::V2,
959 NoteVersion::V3 => Self::V3,
960 }
961 }
962 }
963
964 impl From<SerializedNoteVersion> for NoteVersion {
965 fn from(note_version: SerializedNoteVersion) -> Self {
966 match note_version {
967 SerializedNoteVersion::V2 => Self::V2,
968 SerializedNoteVersion::V3 => Self::V3,
969 }
970 }
971 }
972
973 #[derive(Clone, Debug, Serialize, Deserialize, Getters)]
975 pub struct Bundle {
976 actions: Vec<Action>,
977 flags: u8,
978 value_sum: (u64, bool),
979 anchor: Option<[u8; 32]>,
980 note_version: SerializedNoteVersion,
981 zkproof: Option<Vec<u8>>,
982 bsk: Option<crate::common::SecretKeyBytes>,
983 }
984
985 #[derive(Clone, Debug, Serialize, Deserialize)]
987 pub(crate) struct Action {
988 cv_net: Option<[u8; 32]>,
989 spend: Spend,
990 output: Output,
991 rcv: Option<[u8; 32]>,
992 }
993
994 #[serde_as]
996 #[derive(Clone, Debug, Serialize, Deserialize)]
997 pub(crate) struct Spend {
998 #[serde_as(as = "Option<[_; 32]>")]
999 nullifier: Option<[u8; 32]>,
1000 #[serde_as(as = "Option<[_; 32]>")]
1001 rk: Option<[u8; 32]>,
1002 #[serde_as(as = "Option<[_; 64]>")]
1003 spend_auth_sig: Option<[u8; 64]>,
1004 #[serde_as(as = "Option<[_; 43]>")]
1005 recipient: Option<[u8; 43]>,
1006 value: Option<u64>,
1007 rho: Option<[u8; 32]>,
1008 rseed: Option<[u8; 32]>,
1009 #[serde_as(as = "Option<[_; 96]>")]
1010 fvk: Option<[u8; 96]>,
1011 witness: Option<(u32, [[u8; 32]; 32])>,
1012 alpha: Option<[u8; 32]>,
1013 zip32_derivation: Option<crate::common::Zip32Derivation>,
1014 dummy_sk: Option<crate::common::SecretKeyBytes>,
1015 proprietary: BTreeMap<String, Vec<u8>>,
1016 }
1017
1018 #[serde_as]
1020 #[derive(Clone, Debug, Serialize, Deserialize)]
1021 pub(crate) struct Output {
1022 cmx: Option<[u8; 32]>,
1023 ephemeral_key: [u8; 32],
1024 enc_ciphertext: super::EncCiphertext,
1025 out_ciphertext: Vec<u8>,
1026 #[serde_as(as = "Option<[_; 43]>")]
1027 recipient: Option<[u8; 43]>,
1028 value: Option<u64>,
1029 rseed: Option<[u8; 32]>,
1030 ock: Option<[u8; 32]>,
1031 zip32_derivation: Option<crate::common::Zip32Derivation>,
1032 user_address: Option<String>,
1033 proprietary: BTreeMap<String, Vec<u8>>,
1034 }
1035
1036 impl TryFrom<super::Bundle> for Bundle {
1037 type Error = crate::EncodingError;
1038
1039 fn try_from(bundle: super::Bundle) -> Result<Self, Self::Error> {
1040 Ok(Self {
1041 actions: bundle
1042 .actions
1043 .into_iter()
1044 .map(Action::from)
1045 .collect::<Vec<_>>(),
1046 flags: bundle.flags,
1047 value_sum: bundle.value_sum,
1048 anchor: bundle.anchor,
1049 note_version: bundle.note_version.into(),
1050 zkproof: bundle.zkproof,
1051 bsk: bundle.bsk,
1052 })
1053 }
1054 }
1055
1056 impl Bundle {
1057 pub(crate) fn into_logical(self) -> Result<super::Bundle, crate::ParseError> {
1058 Ok(super::Bundle {
1059 actions: self
1060 .actions
1061 .into_iter()
1062 .map(Action::into_logical)
1063 .collect::<Result<Vec<_>, _>>()?,
1064 flags: self.flags,
1065 value_sum: self.value_sum,
1066 anchor: self.anchor,
1067 note_version: self.note_version.into(),
1068 zkproof: self.zkproof,
1069 bsk: self.bsk,
1070 })
1071 }
1072 }
1073
1074 impl From<super::Action> for Action {
1075 fn from(action: super::Action) -> Self {
1076 Self {
1077 cv_net: action.cv_net,
1078 spend: Spend::from(action.spend),
1079 output: Output::from(action.output),
1080 rcv: action.rcv,
1081 }
1082 }
1083 }
1084
1085 impl Action {
1086 fn into_logical(self) -> Result<super::Action, crate::ParseError> {
1087 Ok(super::Action {
1088 cv_net: self.cv_net,
1089 spend: self.spend.into_logical()?,
1090 output: super::Output::from(self.output),
1091 rcv: self.rcv,
1092 })
1093 }
1094 }
1095
1096 impl From<super::Spend> for Spend {
1097 fn from(spend: super::Spend) -> Self {
1098 Self {
1099 nullifier: Some(spend.nullifier),
1100 rk: Some(spend.rk),
1101 spend_auth_sig: spend.spend_auth_sig,
1102 recipient: spend.recipient,
1103 value: spend.value,
1104 rho: spend.rho,
1105 rseed: spend.rseed,
1106 fvk: spend.fvk,
1107 witness: spend.witness,
1108 alpha: spend.alpha,
1109 zip32_derivation: spend.zip32_derivation,
1110 dummy_sk: spend.dummy_sk,
1111 proprietary: spend.proprietary,
1112 }
1113 }
1114 }
1115
1116 impl Spend {
1117 fn into_logical(self) -> Result<super::Spend, crate::ParseError> {
1118 Ok(super::Spend {
1119 nullifier: self
1120 .nullifier
1121 .ok_or(crate::ParseError::MissingRequiredField(
1122 "orchard.actions[].spend.nullifier",
1123 ))?,
1124 rk: self.rk.ok_or(crate::ParseError::MissingRequiredField(
1125 "orchard.actions[].spend.rk",
1126 ))?,
1127 spend_auth_sig: self.spend_auth_sig,
1128 recipient: self.recipient,
1129 value: self.value,
1130 rho: self.rho,
1131 rseed: self.rseed,
1132 fvk: self.fvk,
1133 witness: self.witness,
1134 alpha: self.alpha,
1135 zip32_derivation: self.zip32_derivation,
1136 dummy_sk: self.dummy_sk,
1137 proprietary: self.proprietary,
1138 })
1139 }
1140 }
1141
1142 impl From<super::Output> for Output {
1143 fn from(output: super::Output) -> Self {
1144 Self {
1145 cmx: output.cmx,
1146 ephemeral_key: output.ephemeral_key,
1147 enc_ciphertext: output.enc_ciphertext,
1148 out_ciphertext: output.out_ciphertext,
1149 recipient: output.recipient,
1150 value: output.value,
1151 rseed: output.rseed,
1152 ock: output.ock,
1153 zip32_derivation: output.zip32_derivation,
1154 user_address: output.user_address,
1155 proprietary: output.proprietary,
1156 }
1157 }
1158 }
1159
1160 impl From<Output> for super::Output {
1161 fn from(output: Output) -> Self {
1162 Self {
1163 cmx: output.cmx,
1164 ephemeral_key: output.ephemeral_key,
1165 enc_ciphertext: output.enc_ciphertext,
1166 out_ciphertext: output.out_ciphertext,
1167 recipient: output.recipient,
1168 value: output.value,
1169 rseed: output.rseed,
1170 ock: output.ock,
1171 zip32_derivation: output.zip32_derivation,
1172 user_address: output.user_address,
1173 proprietary: output.proprietary,
1174 }
1175 }
1176 }
1177
1178 pub(crate) fn encode(
1187 bundle: super::Bundle,
1188 empty: &super::Bundle,
1189 ) -> Result<Option<Bundle>, crate::EncodingError> {
1190 (!is_default_empty(&bundle, empty))
1191 .then(|| Bundle::try_from(bundle))
1192 .transpose()
1193 }
1194
1195 fn is_default_empty(bundle: &super::Bundle, empty: &super::Bundle) -> bool {
1196 let mut bundle = bundle.clone();
1197 if bundle.anchor == Some(super::DEFAULT_ANCHOR) {
1198 bundle.anchor = None;
1199 }
1200
1201 bundle == *empty
1202 }
1203
1204 #[cfg(test)]
1205 mod tests {
1206 use alloc::{collections::BTreeMap, vec::Vec};
1207
1208 #[cfg(feature = "orchard")]
1209 use {
1210 crate::roles::{creator::Creator, redactor::Redactor},
1211 ::orchard::{
1212 Note,
1213 keys::{FullViewingKey, Scope, SpendingKey},
1214 note::{ExtractedNoteCommitment, RandomSeed, Rho},
1215 note_encryption::{OrchardDomain, OrchardNoteEncryption},
1216 value::NoteValue,
1217 },
1218 zcash_note_encryption::Domain,
1219 zcash_protocol::consensus::BranchId,
1220 };
1221
1222 use super::super::{
1223 Action as LogicalAction, Bundle as LogicalBundle, EMPTY_ORCHARD, EncCiphertext,
1224 MEMO_SIZE, MemoPlaintext, NoteVersion, ORCHARD_SPENDS_AND_OUTPUTS_ENABLED, Output,
1225 Spend,
1226 };
1227
1228 fn logical_action(cv_net: Option<[u8; 32]>, cmx: Option<[u8; 32]>) -> LogicalAction {
1229 LogicalAction {
1230 cv_net,
1231 spend: Spend {
1232 nullifier: [1; 32],
1233 rk: [2; 32],
1234 spend_auth_sig: None,
1235 recipient: None,
1236 value: None,
1237 rho: None,
1238 rseed: None,
1239 fvk: None,
1240 witness: None,
1241 alpha: None,
1242 zip32_derivation: None,
1243 dummy_sk: None,
1244 proprietary: BTreeMap::new(),
1245 },
1246 output: Output {
1247 cmx,
1248 ephemeral_key: [4; 32],
1249 enc_ciphertext: EncCiphertext::Encrypted(Vec::new()),
1250 out_ciphertext: Vec::new(),
1251 recipient: None,
1252 value: None,
1253 rseed: None,
1254 ock: None,
1255 zip32_derivation: None,
1256 user_address: None,
1257 proprietary: BTreeMap::new(),
1258 },
1259 rcv: None,
1260 }
1261 }
1262
1263 fn logical_bundle(anchor: Option<[u8; 32]>, cv_net: Option<[u8; 32]>) -> LogicalBundle {
1264 logical_bundle_with_cmx(anchor, cv_net, Some([3; 32]))
1265 }
1266
1267 fn logical_bundle_with_cmx(
1268 anchor: Option<[u8; 32]>,
1269 cv_net: Option<[u8; 32]>,
1270 cmx: Option<[u8; 32]>,
1271 ) -> LogicalBundle {
1272 LogicalBundle {
1273 actions: vec![logical_action(cv_net, cmx)],
1274 flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
1275 value_sum: (0, false),
1276 anchor,
1277 note_version: NoteVersion::V2,
1278 zkproof: None,
1279 bsk: None,
1280 }
1281 }
1282
1283 #[test]
1284 fn anchor_cv_net_and_cmx_round_trip_optional_encoding() {
1285 for (anchor, cv_net, cmx) in [
1286 (None, None, None),
1287 (Some([5; 32]), Some([6; 32]), Some([7; 32])),
1288 ] {
1289 let bundle = logical_bundle_with_cmx(anchor, cv_net, cmx);
1290
1291 let encoded = super::Bundle::try_from(bundle.clone()).unwrap();
1292 assert_eq!(encoded.anchor, anchor);
1293 assert_eq!(encoded.actions[0].cv_net, cv_net);
1294 assert_eq!(encoded.actions[0].output.cmx, cmx);
1295
1296 let decoded = encoded.into_logical().unwrap();
1297 assert_eq!(decoded, bundle);
1298 }
1299 }
1300
1301 #[test]
1302 fn missing_spend_nullifier_or_rk_is_rejected() {
1303 let bundle = logical_bundle(Some([5; 32]), Some([6; 32]));
1304
1305 for (clear_field, missing_field) in [
1306 (
1307 (|spend: &mut super::Spend| spend.nullifier = None) as fn(&mut super::Spend),
1308 "orchard.actions[].spend.nullifier",
1309 ),
1310 (
1311 |spend: &mut super::Spend| spend.rk = None,
1312 "orchard.actions[].spend.rk",
1313 ),
1314 ] {
1315 let mut encoded = super::Bundle::try_from(bundle.clone()).unwrap();
1316 assert_eq!(encoded.actions[0].spend.nullifier, Some([1; 32]));
1317 assert_eq!(encoded.actions[0].spend.rk, Some([2; 32]));
1318
1319 clear_field(&mut encoded.actions[0].spend);
1320
1321 assert!(matches!(
1322 encoded.into_logical(),
1323 Err(crate::ParseError::MissingRequiredField(field)) if field == missing_field
1324 ));
1325 }
1326 }
1327
1328 #[test]
1329 fn memo_plaintext_strips_and_expands_trailing_zeroes() {
1330 let mut memo = [0; MEMO_SIZE];
1331 memo[..5].copy_from_slice(b"hello");
1332
1333 let plaintext = MemoPlaintext::from_memo(memo);
1334
1335 assert_eq!(plaintext.as_stripped_bytes(), b"hello");
1336 assert_eq!(plaintext.to_memo(), memo);
1337 }
1338
1339 #[cfg(feature = "orchard")]
1340 fn decryptable_action_with_memo(memo: [u8; MEMO_SIZE]) -> LogicalAction {
1341 let mut nullifier = [0; 32];
1342 nullifier[0] = 1;
1343 let rho = Option::from(Rho::from_bytes(&nullifier)).unwrap();
1344 let (_, rseed) = (0u8..)
1345 .find_map(|i| {
1346 let mut rseed = [0; 32];
1347 rseed[0] = i;
1348 Option::from(RandomSeed::from_bytes(rseed, &rho)).map(|parsed| (rseed, parsed))
1349 })
1350 .unwrap();
1351 let recipient = FullViewingKey::from(&SpendingKey::from_bytes([0; 32]).unwrap())
1352 .address_at(0u32, Scope::External);
1353 let value = NoteValue::from_raw(100_000);
1354 let note = Option::from(Note::from_parts(
1355 recipient,
1356 value,
1357 rho,
1358 rseed,
1359 NoteVersion::V2,
1360 ))
1361 .unwrap();
1362
1363 let encryptor = OrchardNoteEncryption::new(None, note, memo);
1364
1365 LogicalAction {
1366 cv_net: Some([0; 32]),
1367 spend: Spend {
1368 nullifier,
1369 rk: [2; 32],
1370 spend_auth_sig: None,
1371 recipient: None,
1372 value: None,
1373 rho: None,
1374 rseed: None,
1375 fvk: None,
1376 witness: None,
1377 alpha: None,
1378 zip32_derivation: None,
1379 dummy_sk: None,
1380 proprietary: BTreeMap::new(),
1381 },
1382 output: Output {
1383 cmx: Some(ExtractedNoteCommitment::from(note.commitment()).to_bytes()),
1384 ephemeral_key: OrchardDomain::epk_bytes(encryptor.epk()).0,
1385 enc_ciphertext: EncCiphertext::Encrypted(
1386 encryptor.encrypt_note_plaintext().0.to_vec(),
1387 ),
1388 out_ciphertext: Vec::new(),
1389 recipient: Some(recipient.to_raw_address_bytes()),
1390 value: Some(value.inner()),
1391 rseed: Some(*note.rseed().as_bytes()),
1392 ock: None,
1393 zip32_derivation: None,
1394 user_address: None,
1395 proprietary: BTreeMap::new(),
1396 },
1397 rcv: None,
1398 }
1399 }
1400
1401 #[cfg(feature = "orchard")]
1409 fn fabricated_same_address_action() -> LogicalAction {
1410 let mut nullifier = [0; 32];
1411 nullifier[0] = 1;
1412 let rho = Option::from(Rho::from_bytes(&nullifier)).unwrap();
1413 let (_, rseed) = (0u8..)
1414 .find_map(|i| {
1415 let mut rseed = [0; 32];
1416 rseed[0] = i;
1417 Option::from(RandomSeed::from_bytes(rseed, &rho)).map(|parsed| (rseed, parsed))
1418 })
1419 .unwrap();
1420 let recipient = FullViewingKey::from(&SpendingKey::from_bytes([0; 32]).unwrap())
1421 .address_at(0u32, Scope::External);
1422 let value = NoteValue::from_raw(0);
1423 let note = Option::from(Note::from_parts(
1424 recipient,
1425 value,
1426 rho,
1427 rseed,
1428 NoteVersion::V2,
1429 ))
1430 .unwrap();
1431
1432 let encryptor = OrchardNoteEncryption::new(None, note, [0; MEMO_SIZE]);
1435 let mut enc_ciphertext = encryptor.encrypt_note_plaintext().0.to_vec();
1436 enc_ciphertext.fill(0xab);
1437
1438 LogicalAction {
1439 cv_net: Some([0; 32]),
1440 spend: Spend {
1441 nullifier,
1442 rk: [2; 32],
1443 spend_auth_sig: None,
1444 recipient: None,
1445 value: None,
1446 rho: None,
1447 rseed: None,
1448 fvk: None,
1449 witness: None,
1450 alpha: None,
1451 zip32_derivation: None,
1452 dummy_sk: None,
1453 proprietary: BTreeMap::new(),
1454 },
1455 output: Output {
1456 cmx: Some(ExtractedNoteCommitment::from(note.commitment()).to_bytes()),
1457 ephemeral_key: OrchardDomain::epk_bytes(encryptor.epk()).0,
1458 enc_ciphertext: EncCiphertext::Encrypted(enc_ciphertext),
1459 out_ciphertext: Vec::new(),
1460 recipient: Some(recipient.to_raw_address_bytes()),
1461 value: Some(value.inner()),
1462 rseed: Some(*note.rseed().as_bytes()),
1463 ock: None,
1464 zip32_derivation: None,
1465 user_address: None,
1466 proprietary: BTreeMap::new(),
1467 },
1468 rcv: None,
1469 }
1470 }
1471
1472 #[cfg(feature = "orchard")]
1477 #[test]
1478 fn resolvable_field_compaction_retains_randomized_ciphertext() {
1479 let mut action = fabricated_same_address_action();
1480 let original_enc_ciphertext = action.output.enc_ciphertext.clone();
1481
1482 action.compact_resolvable_fields(NoteVersion::V2);
1483
1484 assert_eq!(action.output.enc_ciphertext, original_enc_ciphertext);
1485 assert_eq!(action.output.cmx, None);
1486 }
1487
1488 #[cfg(feature = "orchard")]
1492 #[test]
1493 fn resolve_fields_preserves_randomized_ciphertext() {
1494 let action = fabricated_same_address_action();
1495 let original_enc_ciphertext = action.output.enc_ciphertext.clone();
1496 let mut bundle = LogicalBundle {
1497 actions: vec![action],
1498 flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
1499 value_sum: (0, false),
1500 anchor: None,
1501 note_version: NoteVersion::V2,
1502 zkproof: None,
1503 bsk: None,
1504 };
1505 bundle.actions[0].output.cmx = None;
1506
1507 bundle.resolve_fields().unwrap();
1508
1509 assert_eq!(
1510 bundle.actions[0].output.enc_ciphertext,
1511 original_enc_ciphertext
1512 );
1513 assert!(bundle.actions[0].output.cmx.is_some());
1514 }
1515
1516 #[cfg(feature = "orchard")]
1517 #[test]
1518 fn v2_round_trips_memo_plaintext_ciphertext_data() {
1519 const HELLO_MEMO_PAYLOAD_SIZE_REDUCTION: usize = 575;
1520 const HELLO_MEMO_SERIALIZED_SIZE_REDUCTION: usize =
1523 HELLO_MEMO_PAYLOAD_SIZE_REDUCTION + 1;
1524
1525 let mut memo = [0; MEMO_SIZE];
1526 memo[..5].copy_from_slice(b"hello");
1527
1528 let mut pczt = Creator::new(
1529 BranchId::Nu6.into(),
1530 10_000_000,
1531 133,
1532 Some([0; 32]),
1533 Some([0; 32]),
1534 )
1535 .unwrap()
1536 .build()
1537 .unwrap();
1538 pczt.orchard
1539 .actions
1540 .push(decryptable_action_with_memo(memo));
1541
1542 let encrypted_size = crate::v2::Pczt::try_from(pczt.clone())
1547 .unwrap()
1548 .serialize()
1549 .len();
1550 let redacted = Redactor::new(pczt)
1551 .redact_orchard_with(|mut orchard| {
1552 orchard.redact_actions(|mut action| {
1553 action
1554 .replace_enc_ciphertext_with_decrypted_memo_plaintext(NoteVersion::V2);
1555 });
1556 })
1557 .finish();
1558 let redacted_size = crate::v2::Pczt::try_from(redacted.clone())
1559 .unwrap()
1560 .serialize()
1561 .len();
1562
1563 assert_eq!(
1564 redacted.orchard.actions[0].output.enc_ciphertext,
1565 EncCiphertext::MemoPlaintext(MemoPlaintext::from_memo(memo))
1566 );
1567 assert_eq!(
1568 encrypted_size - redacted_size,
1569 HELLO_MEMO_SERIALIZED_SIZE_REDUCTION
1570 );
1571
1572 let decoded = crate::parse(&redacted.serialize().unwrap()).unwrap();
1573
1574 assert_eq!(
1575 decoded.orchard.actions[0].output.enc_ciphertext,
1576 EncCiphertext::MemoPlaintext(MemoPlaintext::from_memo(memo))
1577 );
1578 }
1579
1580 #[cfg(feature = "orchard")]
1581 #[test]
1582 fn resolve_fields_recomputes_cmx() {
1583 let action = decryptable_action_with_memo([0; MEMO_SIZE]);
1584 let expected_cmx = action.output.cmx;
1585 let mut bundle = LogicalBundle {
1586 actions: vec![action],
1587 flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
1588 value_sum: (0, false),
1589 anchor: None,
1590 note_version: NoteVersion::V2,
1591 zkproof: None,
1592 bsk: None,
1593 };
1594 bundle.actions[0].output.cmx = None;
1595
1596 bundle.resolve_fields().unwrap();
1597
1598 assert_eq!(bundle.actions[0].output.cmx, expected_cmx);
1599 }
1600
1601 #[cfg(feature = "orchard")]
1602 #[test]
1603 fn decrypted_memo_plaintext_compaction_skips_decryption_failure() {
1604 let mut action = decryptable_action_with_memo([0; MEMO_SIZE]);
1605 let original_enc_ciphertext = match &mut action.output.enc_ciphertext {
1606 EncCiphertext::Encrypted(enc_ciphertext) => {
1607 enc_ciphertext[0] ^= 1;
1608 enc_ciphertext.clone()
1609 }
1610 EncCiphertext::MemoPlaintext(_) => unreachable!("helper encrypts memo plaintext"),
1611 };
1612
1613 action.replace_enc_ciphertext_with_decrypted_memo_plaintext(NoteVersion::V2);
1614
1615 assert_eq!(
1616 action.output.enc_ciphertext,
1617 EncCiphertext::Encrypted(original_enc_ciphertext)
1618 );
1619 }
1620
1621 #[cfg(feature = "orchard")]
1622 #[test]
1623 fn resolvable_field_compaction_checks_derived_values() {
1624 let mut memo = [0; MEMO_SIZE];
1625 memo[..5].copy_from_slice(b"hello");
1626 let mut action = decryptable_action_with_memo(memo);
1627 action.spend.value = Some(200_000);
1628 action.rcv = Some([3; 32]);
1629 action.cv_net = Some(super::super::testing::value_commitment(100_000, [3; 32]));
1630
1631 action.compact_resolvable_fields(NoteVersion::V2);
1632
1633 assert_eq!(action.cv_net, None);
1634 assert_eq!(action.output.cmx, None);
1635 assert_eq!(
1636 action.output.enc_ciphertext,
1637 EncCiphertext::MemoPlaintext(MemoPlaintext::from_memo(memo))
1638 );
1639 }
1640
1641 #[cfg(feature = "orchard")]
1642 #[test]
1643 fn resolvable_field_compaction_retains_unverifiable_values() {
1644 let mut memo = [0; MEMO_SIZE];
1645 memo[..5].copy_from_slice(b"hello");
1646 let mut action = decryptable_action_with_memo(memo);
1647 let original_cv_net = action.cv_net;
1648 let original_cmx = action.output.cmx;
1649 let original_enc_ciphertext = action.output.enc_ciphertext.clone();
1650 action.output.recipient = None;
1651
1652 action.compact_resolvable_fields(NoteVersion::V2);
1653
1654 assert_eq!(action.cv_net, original_cv_net);
1655 assert_eq!(action.output.cmx, original_cmx);
1656 assert_eq!(action.output.enc_ciphertext, original_enc_ciphertext);
1657 }
1658
1659 #[cfg(feature = "orchard")]
1660 #[test]
1661 fn resolvable_field_compaction_retains_mismatched_values() {
1662 let mut memo = [0; MEMO_SIZE];
1663 memo[..5].copy_from_slice(b"hello");
1664 let mut action = decryptable_action_with_memo(memo);
1665 action.spend.value = Some(200_000);
1666 action.rcv = Some([3; 32]);
1667 let mut cv_net = super::super::testing::value_commitment(100_000, [3; 32]);
1668 cv_net[0] ^= 1;
1669 action.cv_net = Some(cv_net);
1670 let mut cmx = action.output.cmx.unwrap();
1671 cmx[0] ^= 1;
1672 action.output.cmx = Some(cmx);
1673 let original_enc_ciphertext = action.output.enc_ciphertext.clone();
1674
1675 action.compact_resolvable_fields(NoteVersion::V2);
1676
1677 assert_eq!(action.cv_net, Some(cv_net));
1678 assert_eq!(action.output.cmx, Some(cmx));
1679 assert_eq!(action.output.enc_ciphertext, original_enc_ciphertext);
1680 }
1681
1682 #[test]
1683 fn v1_rejects_memo_plaintext_ciphertext_data() {
1684 let mut bundle = logical_bundle(Some([5; 32]), Some([6; 32]));
1685 bundle.actions[0].output.enc_ciphertext =
1686 EncCiphertext::MemoPlaintext(MemoPlaintext::from_memo([0; MEMO_SIZE]));
1687
1688 assert!(matches!(
1689 crate::orchard::v1::Bundle::try_from(bundle),
1690 Err(crate::EncodingError::RequiresV2)
1691 ));
1692 }
1693
1694 #[test]
1695 fn v1_rejects_missing_anchor_and_cv_net() {
1696 assert!(matches!(
1697 crate::orchard::v1::Bundle::try_from(logical_bundle(None, Some([6; 32]))),
1698 Err(crate::EncodingError::RequiresV2)
1699 ));
1700
1701 assert!(matches!(
1702 crate::orchard::v1::Bundle::try_from(logical_bundle(Some([5; 32]), None)),
1703 Err(crate::EncodingError::RequiresV2)
1704 ));
1705
1706 assert!(matches!(
1707 crate::orchard::v1::Bundle::try_from(logical_bundle_with_cmx(
1708 Some([5; 32]),
1709 Some([6; 32]),
1710 None
1711 )),
1712 Err(crate::EncodingError::RequiresV2)
1713 ));
1714 }
1715
1716 #[test]
1717 fn v1_empty_bundle_anchor_falls_back_to_default() {
1718 let bundle = LogicalBundle {
1721 actions: Vec::new(),
1722 flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
1723 value_sum: (0, false),
1724 anchor: None,
1725 note_version: NoteVersion::V2,
1726 zkproof: None,
1727 bsk: None,
1728 };
1729
1730 let encoded = crate::orchard::v1::Bundle::try_from(bundle)
1731 .expect("an empty bundle's anchor falls back to DEFAULT_ANCHOR");
1732
1733 let decoded = super::super::Bundle::from(encoded);
1736 assert_eq!(decoded, EMPTY_ORCHARD);
1737 }
1738 }
1739}
1740
1741impl Bundle {
1742 pub fn sole_action(&self) -> Option<&Action> {
1744 match self.actions.as_slice() {
1745 [action] => Some(action),
1746 _ => None,
1747 }
1748 }
1749
1750 pub fn value_carrying_outputs_all_pay(&self, recipient: &[u8; 43]) -> Option<bool> {
1761 for action in &self.actions {
1762 let output = &action.output;
1763 if output.value? == 0 {
1764 continue;
1765 }
1766 if &output.recipient? != recipient {
1767 return Some(false);
1768 }
1769 }
1770 Some(true)
1771 }
1772
1773 pub(crate) fn merge(
1777 mut self,
1778 other: Self,
1779 self_global: &Global,
1780 other_global: &Global,
1781 ) -> Option<Self> {
1782 let Self {
1784 mut actions,
1785 flags,
1786 value_sum,
1787 anchor,
1788 note_version,
1789 zkproof,
1790 bsk,
1791 } = other;
1792
1793 if self.flags != flags || self.note_version != note_version {
1794 return None;
1795 }
1796
1797 match (self.bsk.as_mut(), bsk) {
1800 (Some(lhs), Some(rhs)) if lhs != &rhs => return None,
1801 (Some(_), _) | (_, Some(_))
1802 if self.actions.len() != actions.len() || self.value_sum != value_sum =>
1803 {
1804 return None;
1805 }
1806 (Some(_), _) | (_, Some(_)) => (),
1808 (None, None) => match (
1810 self_global.shielded_modifiable(),
1811 other_global.shielded_modifiable(),
1812 self.actions.len().cmp(&actions.len()),
1813 ) {
1814 (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None,
1816 (true, _, Ordering::Less) => {
1819 self.actions.extend(actions.drain(self.actions.len()..));
1820
1821 self.value_sum = value_sum;
1824 }
1825 (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (),
1827 },
1828 }
1829
1830 if !merge_optional(&mut self.anchor, anchor) {
1831 return None;
1832 }
1833
1834 if !merge_optional(&mut self.zkproof, zkproof) {
1835 return None;
1836 }
1837
1838 for (lhs, rhs) in self.actions.iter_mut().zip(actions) {
1841 let Action {
1843 cv_net,
1844 spend:
1845 Spend {
1846 nullifier,
1847 rk,
1848 spend_auth_sig,
1849 recipient,
1850 value,
1851 rho,
1852 rseed,
1853 fvk,
1854 witness,
1855 alpha,
1856 zip32_derivation: spend_zip32_derivation,
1857 dummy_sk,
1858 proprietary: spend_proprietary,
1859 },
1860 output:
1861 Output {
1862 cmx,
1863 ephemeral_key,
1864 enc_ciphertext,
1865 out_ciphertext,
1866 recipient: output_recipient,
1867 value: output_value,
1868 rseed: output_rseed,
1869 ock,
1870 zip32_derivation: output_zip32_derivation,
1871 user_address,
1872 proprietary: output_proprietary,
1873 },
1874 rcv,
1875 } = rhs;
1876
1877 if lhs.spend.nullifier != nullifier
1878 || lhs.spend.rk != rk
1879 || lhs.output.ephemeral_key != ephemeral_key
1880 || lhs.output.enc_ciphertext != enc_ciphertext
1881 || lhs.output.out_ciphertext != out_ciphertext
1882 {
1883 return None;
1884 }
1885
1886 if !(merge_optional(&mut lhs.cv_net, cv_net)
1887 && merge_optional(&mut lhs.spend.spend_auth_sig, spend_auth_sig)
1888 && merge_optional(&mut lhs.spend.recipient, recipient)
1889 && merge_optional(&mut lhs.spend.value, value)
1890 && merge_optional(&mut lhs.spend.rho, rho)
1891 && merge_optional(&mut lhs.spend.rseed, rseed)
1892 && merge_optional(&mut lhs.spend.fvk, fvk)
1893 && merge_optional(&mut lhs.spend.witness, witness)
1894 && merge_optional(&mut lhs.spend.alpha, alpha)
1895 && merge_optional(&mut lhs.spend.zip32_derivation, spend_zip32_derivation)
1896 && merge_optional(&mut lhs.spend.dummy_sk, dummy_sk)
1897 && merge_map(&mut lhs.spend.proprietary, spend_proprietary)
1898 && merge_optional(&mut lhs.output.cmx, cmx)
1899 && merge_optional(&mut lhs.output.recipient, output_recipient)
1900 && merge_optional(&mut lhs.output.value, output_value)
1901 && merge_optional(&mut lhs.output.rseed, output_rseed)
1902 && merge_optional(&mut lhs.output.ock, ock)
1903 && merge_optional(&mut lhs.output.zip32_derivation, output_zip32_derivation)
1904 && merge_optional(&mut lhs.output.user_address, user_address)
1905 && merge_map(&mut lhs.output.proprietary, output_proprietary)
1906 && merge_optional(&mut lhs.rcv, rcv))
1907 {
1908 return None;
1909 }
1910 }
1911
1912 Some(self)
1913 }
1914}
1915
1916#[cfg(feature = "orchard")]
1920pub(crate) fn bundle_version_for_revision(
1921 revision: zcash_protocol::consensus::OrchardProtocolRevision,
1922 pool: orchard::ValuePool,
1923) -> Option<BundleVersion> {
1924 match pool {
1925 ValuePool::Orchard => Some(match revision {
1926 OrchardProtocolRevision::InsecureV1 => BundleVersion::orchard_insecure_v1(),
1927 OrchardProtocolRevision::V2 => BundleVersion::orchard_v2(),
1928 OrchardProtocolRevision::V3 => BundleVersion::orchard_v3(),
1929 }),
1930 ValuePool::Ironwood => match revision {
1931 OrchardProtocolRevision::InsecureV1 | OrchardProtocolRevision::V2 => None,
1932 OrchardProtocolRevision::V3 => Some(BundleVersion::ironwood_v3()),
1933 },
1934 }
1935}
1936
1937#[cfg(feature = "orchard")]
1941pub(crate) fn orchard_bundle_version(global: &crate::common::Global) -> Option<BundleVersion> {
1942 BranchId::try_from(global.consensus_branch_id)
1943 .ok()?
1944 .orchard_protocol_revision()
1945 .and_then(|revision| bundle_version_for_revision(revision, orchard::ValuePool::Orchard))
1946}
1947
1948#[cfg(feature = "orchard")]
1951#[derive(Debug)]
1952#[non_exhaustive]
1953pub enum ParseError {
1954 MissingAnchor,
1959 Bundle(orchard::pczt::ParseError),
1961}
1962
1963#[cfg(feature = "orchard")]
1964impl From<orchard::pczt::ParseError> for ParseError {
1965 fn from(e: orchard::pczt::ParseError) -> Self {
1966 ParseError::Bundle(e)
1967 }
1968}
1969
1970#[cfg(all(feature = "orchard", feature = "prover"))]
1973#[derive(Debug)]
1974#[non_exhaustive]
1975pub enum AnchorConsistencyError {
1976 IncompleteSpendData,
1979 WitnessDoesNotRootToAnchor,
1981}
1982
1983#[cfg(all(feature = "orchard", feature = "prover"))]
1991pub(crate) fn verify_witnesses_root_to_anchor(
1992 bundle: &orchard::pczt::Bundle,
1993 anchor: orchard::Anchor,
1994) -> Result<(), AnchorConsistencyError> {
1995 for action in bundle.actions() {
1996 let spend = action.spend();
1997
1998 let Some(witness) = spend.witness() else {
1999 continue;
2000 };
2001 let Some(value) = spend.value() else {
2002 continue;
2003 };
2004 if value.inner() == 0 {
2005 continue;
2006 }
2007
2008 let recipient = spend
2009 .recipient()
2010 .ok_or(AnchorConsistencyError::IncompleteSpendData)?;
2011 let rho = spend
2012 .rho()
2013 .ok_or(AnchorConsistencyError::IncompleteSpendData)?;
2014 let rseed = spend
2015 .rseed()
2016 .ok_or(AnchorConsistencyError::IncompleteSpendData)?;
2017
2018 let note = orchard::Note::from_parts(recipient, *value, rho, rseed, *spend.note_version())
2019 .into_option()
2020 .ok_or(AnchorConsistencyError::IncompleteSpendData)?;
2021 let cmx = orchard::note::ExtractedNoteCommitment::from(note.commitment());
2022 let computed_anchor = witness.root(cmx);
2023
2024 if computed_anchor != anchor {
2025 return Err(AnchorConsistencyError::WitnessDoesNotRootToAnchor);
2026 }
2027 }
2028
2029 Ok(())
2030}
2031
2032#[cfg(feature = "orchard")]
2033impl Output {
2034 fn resolve_cmx(
2036 &mut self,
2037 note_version: NoteVersion,
2038 spend_nullifier: [u8; 32],
2039 ) -> Result<(), orchard::pczt::ParseError> {
2040 if self.cmx.is_some() {
2041 return Ok(());
2042 }
2043
2044 let recipient = Address::from_raw_address_bytes(
2045 self.recipient
2046 .as_ref()
2047 .ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?,
2048 )
2049 .into_option()
2050 .ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?;
2051 let rho = Rho::from_bytes(&spend_nullifier)
2052 .into_option()
2053 .ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?;
2054 let rseed = RandomSeed::from_bytes(
2055 *self
2056 .rseed
2057 .as_ref()
2058 .ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?,
2059 &rho,
2060 )
2061 .into_option()
2062 .ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?;
2063 let note = Note::from_parts(
2064 recipient,
2065 NoteValue::from_raw(
2066 self.value
2067 .ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?,
2068 ),
2069 rho,
2070 rseed,
2071 note_version,
2072 )
2073 .into_option()
2074 .ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?;
2075
2076 self.cmx = Some(ExtractedNoteCommitment::from(note.commitment()).to_bytes());
2077 Ok(())
2078 }
2079
2080 fn encrypt_ciphertext_from_memo(
2090 &mut self,
2091 note_version: NoteVersion,
2092 spend_nullifier: [u8; 32],
2093 ) -> Result<(), orchard::pczt::ParseError> {
2094 let memo: [u8; 512] = match &self.enc_ciphertext {
2095 EncCiphertext::Encrypted(_) => return Ok(()),
2096 EncCiphertext::MemoPlaintext(memo) => memo.to_memo(),
2097 };
2098
2099 let recipient = Address::from_raw_address_bytes(
2100 self.recipient
2101 .as_ref()
2102 .ok_or(orchard::pczt::ParseError::InvalidRecipient)?,
2103 )
2104 .into_option()
2105 .ok_or(orchard::pczt::ParseError::InvalidRecipient)?;
2106 let rho = Rho::from_bytes(&spend_nullifier)
2107 .into_option()
2108 .ok_or(orchard::pczt::ParseError::InvalidNullifier)?;
2109 let rseed = RandomSeed::from_bytes(
2110 *self
2111 .rseed
2112 .as_ref()
2113 .ok_or(orchard::pczt::ParseError::InvalidRandomSeed)?,
2114 &rho,
2115 )
2116 .into_option()
2117 .ok_or(orchard::pczt::ParseError::InvalidRandomSeed)?;
2118 let note = Note::from_parts(
2119 recipient,
2120 NoteValue::from_raw(
2121 self.value
2122 .ok_or(orchard::pczt::ParseError::InvalidEncCiphertext)?,
2123 ),
2124 rho,
2125 rseed,
2126 note_version,
2127 )
2128 .into_option()
2129 .ok_or(orchard::pczt::ParseError::InvalidEncCiphertext)?;
2130 let encryptor = OrchardNoteEncryption::new(None, note, memo);
2131 let ephemeral_key = OrchardDomain::epk_bytes(encryptor.epk()).0;
2132 let enc_ciphertext = encryptor.encrypt_note_plaintext().0.to_vec();
2133
2134 if ephemeral_key != self.ephemeral_key {
2135 return Err(orchard::pczt::ParseError::InvalidEncCiphertext);
2136 }
2137
2138 self.enc_ciphertext = EncCiphertext::Encrypted(enc_ciphertext);
2139 Ok(())
2140 }
2141}
2142
2143#[cfg(feature = "orchard")]
2144impl Action {
2145 fn resolve_cv_net(&mut self) -> Result<(), orchard::pczt::ParseError> {
2147 if self.cv_net.is_some() {
2148 return Ok(());
2149 }
2150
2151 let spend_value: NoteValue = NoteValue::from_raw(
2152 self.spend
2153 .value
2154 .ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?,
2155 );
2156 let output_value = NoteValue::from_raw(
2157 self.output
2158 .value
2159 .ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?,
2160 );
2161 let rcv = ValueCommitTrapdoor::from_bytes(
2162 self.rcv
2163 .ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?,
2164 )
2165 .into_option()
2166 .ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?;
2167
2168 self.cv_net = Some(ValueCommitment::derive(spend_value - output_value, rcv).to_bytes());
2169 Ok(())
2170 }
2171}
2172
2173#[cfg(feature = "orchard")]
2174impl Bundle {
2175 pub fn resolve_fields(&mut self) -> Result<(), orchard::pczt::ParseError> {
2187 for action in &mut self.actions {
2188 action.resolve_cv_net()?;
2189 action
2190 .output
2191 .resolve_cmx(self.note_version, action.spend.nullifier)?;
2192 action
2193 .output
2194 .encrypt_ciphertext_from_memo(self.note_version, action.spend.nullifier)?;
2195 }
2196
2197 Ok(())
2198 }
2199
2200 pub(crate) fn into_ironwood_parsed(
2203 self,
2204 anchor_requirement: crate::common::AnchorRequirement,
2205 ) -> Result<Parsed, ParseError> {
2206 self.into_parsed_with_version(BundleVersion::ironwood_v3(), anchor_requirement)
2207 }
2208
2209 pub(crate) fn into_ironwood_parsed_preverified_for_signing(
2214 self,
2215 anchor_requirement: crate::common::AnchorRequirement,
2216 ) -> Result<Parsed, ParseError> {
2217 self.into_parsed_with_version_preverified_for_signing(
2218 BundleVersion::ironwood_v3(),
2219 anchor_requirement,
2220 )
2221 }
2222
2223 #[allow(dead_code)]
2230 pub(crate) fn into_parsed_with_version(
2231 self,
2232 bundle_version: BundleVersion,
2233 anchor_requirement: crate::common::AnchorRequirement,
2234 ) -> Result<Parsed, ParseError> {
2235 self.into_parsed_inner(bundle_version, anchor_requirement, false)
2236 }
2237
2238 #[allow(dead_code)]
2252 pub(crate) fn into_parsed_with_version_preverified_for_signing(
2253 self,
2254 bundle_version: BundleVersion,
2255 anchor_requirement: crate::common::AnchorRequirement,
2256 ) -> Result<Parsed, ParseError> {
2257 self.into_parsed_inner(bundle_version, anchor_requirement, true)
2258 }
2259
2260 fn into_parsed_inner(
2264 mut self,
2265 bundle_version: BundleVersion,
2266 anchor_requirement: crate::common::AnchorRequirement,
2267 preverified: bool,
2268 ) -> Result<Parsed, ParseError> {
2269 self.resolve_fields()?;
2270 let wire_anchor = self.anchor;
2271 let anchor = anchor_requirement
2272 .resolve(wire_anchor, self.actions.is_empty())
2273 .ok_or(ParseError::MissingAnchor)?;
2274
2275 #[inline(never)]
2281 fn parse_action_inner(
2282 mut action: Action,
2283 note_version: NoteVersion,
2284 preverified: bool,
2285 ) -> Result<orchard::pczt::Action, orchard::pczt::ParseError> {
2286 let spend_zip32_derivation = action
2287 .spend
2288 .zip32_derivation
2289 .map(|z| {
2290 orchard::pczt::Zip32Derivation::parse(z.seed_fingerprint, z.derivation_path)
2291 })
2292 .transpose()?;
2293 let spend_nullifier = action.spend.nullifier;
2294
2295 let spend = if preverified {
2296 orchard::pczt::Spend::parse_preverified_for_signing(
2297 action.spend.nullifier,
2298 action.spend.rk,
2299 action.spend.spend_auth_sig,
2300 action.spend.recipient,
2301 action.spend.value,
2302 action.spend.rho,
2303 action.spend.rseed,
2304 action.spend.fvk,
2305 action.spend.witness,
2306 action.spend.alpha,
2307 spend_zip32_derivation,
2308 action.spend.dummy_sk.as_ref().map(|sk| *sk.expose_secret()),
2309 note_version,
2310 action.spend.proprietary,
2311 )
2312 } else {
2313 orchard::pczt::Spend::parse(
2314 action.spend.nullifier,
2315 action.spend.rk,
2316 action.spend.spend_auth_sig,
2317 action.spend.recipient,
2318 action.spend.value,
2319 action.spend.rho,
2320 action.spend.rseed,
2321 action.spend.fvk,
2322 action.spend.witness,
2323 action.spend.alpha,
2324 spend_zip32_derivation,
2325 action.spend.dummy_sk.as_ref().map(|sk| *sk.expose_secret()),
2326 note_version,
2327 action.spend.proprietary,
2328 )
2329 }?;
2330
2331 action
2332 .output
2333 .encrypt_ciphertext_from_memo(note_version, spend_nullifier)?;
2334
2335 let enc_ciphertext = action
2336 .output
2337 .enc_ciphertext
2338 .into_encrypted()
2339 .ok_or(orchard::pczt::ParseError::InvalidEncCiphertext)?;
2340 let cv_net = action
2341 .cv_net
2342 .ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?;
2343
2344 let output = orchard::pczt::Output::parse(
2345 *spend.nullifier(),
2346 action
2347 .output
2348 .cmx
2349 .ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?,
2350 action.output.ephemeral_key,
2351 enc_ciphertext,
2352 action.output.out_ciphertext,
2353 action.output.recipient,
2354 action.output.value,
2355 action.output.rseed,
2356 action.output.ock,
2357 action
2358 .output
2359 .zip32_derivation
2360 .map(|z| {
2361 orchard::pczt::Zip32Derivation::parse(z.seed_fingerprint, z.derivation_path)
2362 })
2363 .transpose()?,
2364 action.output.user_address,
2365 note_version,
2366 action.output.proprietary,
2367 )?;
2368
2369 orchard::pczt::Action::parse(cv_net, spend, output, action.rcv)
2370 }
2371
2372 let note_version = self.note_version;
2373 let mut actions = Vec::with_capacity(self.actions.len());
2374 for action in self.actions {
2375 actions.push(parse_action_inner(action, note_version, preverified)?);
2376 }
2377
2378 let bundle = orchard::pczt::Bundle::parse(
2379 actions,
2380 self.flags,
2381 bundle_version,
2382 self.value_sum,
2383 anchor,
2384 self.zkproof,
2385 self.bsk.as_ref().map(|bsk| *bsk.expose_secret()),
2386 )?;
2387
2388 Ok(Parsed {
2389 bundle,
2390 wire_anchor,
2391 })
2392 }
2393
2394 #[allow(dead_code)]
2395 pub(crate) fn serialize_from(bundle: orchard::pczt::Bundle) -> Self {
2396 let note_version = bundle.bundle_version().note_version();
2397
2398 assert!(
2399 bundle.actions().iter().all(|action| {
2400 action.spend().note_version() == ¬e_version
2401 && action.output().note_version() == ¬e_version
2402 }),
2403 "Orchard PCZT bundle must have a single note version"
2404 );
2405
2406 let actions = bundle
2407 .actions()
2408 .iter()
2409 .map(|action| {
2410 let spend = action.spend();
2411 let output = action.output();
2412
2413 Action {
2414 cv_net: Some(action.cv_net().to_bytes()),
2415 spend: Spend {
2416 nullifier: spend.nullifier().to_bytes(),
2417 rk: spend.rk().into(),
2418 spend_auth_sig: spend.spend_auth_sig().as_ref().map(|s| s.into()),
2419 recipient: action
2420 .spend()
2421 .recipient()
2422 .map(|recipient| recipient.to_raw_address_bytes()),
2423 value: spend.value().map(|value| value.inner()),
2424 rho: spend.rho().map(|rho| rho.to_bytes()),
2425 rseed: spend.rseed().map(|rseed| *rseed.as_bytes()),
2426 fvk: spend.fvk().as_ref().map(|fvk| fvk.to_bytes()),
2427 witness: spend.witness().as_ref().map(|witness| {
2428 (
2429 u32::try_from(u64::from(witness.position()))
2430 .expect("Sapling positions fit in u32"),
2431 witness
2432 .auth_path()
2433 .iter()
2434 .map(|node| node.to_bytes())
2435 .collect::<Vec<_>>()[..]
2436 .try_into()
2437 .expect("path is length 32"),
2438 )
2439 }),
2440 alpha: spend.alpha().map(|alpha| alpha.to_repr()),
2441 zip32_derivation: spend.zip32_derivation().as_ref().map(|z| {
2442 Zip32Derivation {
2443 seed_fingerprint: *z.seed_fingerprint(),
2444 derivation_path: z
2445 .derivation_path()
2446 .iter()
2447 .map(|i| i.index())
2448 .collect(),
2449 }
2450 }),
2451 dummy_sk: action
2452 .spend()
2453 .dummy_sk()
2454 .as_ref()
2455 .map(|dummy_sk| SecretKeyBytes::new(*dummy_sk.to_bytes())),
2456 proprietary: spend.proprietary().clone(),
2457 },
2458 output: Output {
2459 cmx: Some(output.cmx().to_bytes()),
2460 ephemeral_key: output.encrypted_note().epk_bytes,
2461 enc_ciphertext: EncCiphertext::Encrypted(
2462 output.encrypted_note().enc_ciphertext.0.to_vec(),
2463 ),
2464 out_ciphertext: output.encrypted_note().out_ciphertext.to_vec(),
2465 recipient: action
2466 .output()
2467 .recipient()
2468 .map(|recipient| recipient.to_raw_address_bytes()),
2469 value: output.value().map(|value| value.inner()),
2470 rseed: output.rseed().map(|rseed| *rseed.as_bytes()),
2471 ock: output.ock().as_ref().map(|ock| ock.0),
2472 zip32_derivation: output.zip32_derivation().as_ref().map(|z| {
2473 Zip32Derivation {
2474 seed_fingerprint: *z.seed_fingerprint(),
2475 derivation_path: z
2476 .derivation_path()
2477 .iter()
2478 .map(|i| i.index())
2479 .collect(),
2480 }
2481 }),
2482 user_address: output.user_address().clone(),
2483 proprietary: output.proprietary().clone(),
2484 },
2485 rcv: action.rcv().as_ref().map(|rcv| rcv.to_bytes()),
2486 }
2487 })
2488 .collect();
2489
2490 let value_sum = {
2491 let (magnitude, sign) = bundle.value_sum().magnitude_sign();
2492 (magnitude, matches!(sign, orchard::value::Sign::Negative))
2493 };
2494 let anchor = (!bundle.anchor_deferred()).then(|| bundle.anchor().to_bytes());
2498
2499 Self {
2500 actions,
2501 flags: bundle.flag_byte(),
2502 value_sum,
2503 anchor,
2504 note_version,
2505 zkproof: bundle
2506 .zkproof()
2507 .as_ref()
2508 .map(|zkproof| zkproof.as_ref().to_vec()),
2509 bsk: bundle
2510 .bsk()
2511 .as_ref()
2512 .map(|bsk| SecretKeyBytes::new(bsk.to_bytes())),
2513 }
2514 }
2515}
2516
2517#[cfg(test)]
2518mod shape_tests {
2519 use alloc::collections::BTreeMap;
2520 use alloc::vec::Vec;
2521
2522 use proptest::prelude::*;
2523 use proptest::sample::Index;
2524
2525 use super::{
2526 Action, Bundle, EncCiphertext, NoteVersion, ORCHARD_SPENDS_AND_OUTPUTS_ENABLED, Output,
2527 Spend,
2528 };
2529
2530 const OURS: [u8; 43] = [1; 43];
2533 const THEIRS: [u8; 43] = [2; 43];
2534
2535 fn output(value: Option<u64>, recipient: Option<[u8; 43]>) -> Output {
2536 Output {
2537 cmx: None,
2538 ephemeral_key: [0; 32],
2539 enc_ciphertext: EncCiphertext::Encrypted(Vec::new()),
2540 out_ciphertext: Vec::new(),
2541 recipient,
2542 value,
2543 rseed: None,
2544 ock: None,
2545 zip32_derivation: None,
2546 user_address: None,
2547 proprietary: BTreeMap::new(),
2548 }
2549 }
2550
2551 fn spend() -> Spend {
2552 Spend {
2553 nullifier: [0; 32],
2554 rk: [0; 32],
2555 spend_auth_sig: None,
2556 recipient: None,
2557 value: None,
2558 rho: None,
2559 rseed: None,
2560 fvk: None,
2561 witness: None,
2562 alpha: None,
2563 zip32_derivation: None,
2564 dummy_sk: None,
2565 proprietary: BTreeMap::new(),
2566 }
2567 }
2568
2569 fn action(value: Option<u64>, recipient: Option<[u8; 43]>) -> Action {
2572 Action {
2573 cv_net: None,
2574 spend: spend(),
2575 output: output(value, recipient),
2576 rcv: None,
2577 }
2578 }
2579
2580 fn bundle(actions: Vec<Action>) -> Bundle {
2581 Bundle {
2582 actions,
2583 flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
2584 value_sum: (0, false),
2585 anchor: None,
2586 note_version: NoteVersion::V2,
2587 zkproof: None,
2588 bsk: None,
2589 }
2590 }
2591
2592 fn arb_recipient() -> impl Strategy<Value = [u8; 43]> {
2593 prop_oneof![Just(OURS), Just(THEIRS)]
2594 }
2595
2596 fn arb_value() -> impl Strategy<Value = u64> {
2599 prop_oneof![Just(0u64), 1u64..1_000]
2600 }
2601
2602 fn arb_action() -> impl Strategy<Value = Action> {
2605 (
2606 prop_oneof![Just(None), arb_value().prop_map(Some)],
2607 prop_oneof![Just(None), arb_recipient().prop_map(Some)],
2608 )
2609 .prop_map(|(value, recipient)| action(value, recipient))
2610 }
2611
2612 fn arb_bundle() -> impl Strategy<Value = Bundle> {
2613 prop::collection::vec(arb_action(), 0..6).prop_map(bundle)
2614 }
2615
2616 fn arb_specified_bundle() -> impl Strategy<Value = Bundle> {
2618 prop::collection::vec((arb_value(), arb_recipient()), 0..6).prop_map(|outputs| {
2619 bundle(
2620 outputs
2621 .into_iter()
2622 .map(|(value, recipient)| action(Some(value), Some(recipient)))
2623 .collect(),
2624 )
2625 })
2626 }
2627
2628 fn arb_send_to_self_bundle() -> impl Strategy<Value = Bundle> {
2632 prop::collection::vec((arb_value(), arb_recipient()), 1..6).prop_map(|outputs| {
2633 bundle(
2634 outputs
2635 .into_iter()
2636 .map(|(value, dummy_recipient)| {
2637 let recipient = if value == 0 { dummy_recipient } else { OURS };
2638 action(Some(value), Some(recipient))
2639 })
2640 .collect(),
2641 )
2642 })
2643 }
2644
2645 proptest! {
2646 #[test]
2648 fn sole_action_answers_exactly_when_there_is_one_action(bundle in arb_bundle()) {
2649 match bundle.sole_action() {
2650 Some(action) => {
2651 prop_assert_eq!(bundle.actions.len(), 1);
2652 prop_assert_eq!(action, &bundle.actions[0]);
2653 }
2654 None => prop_assert_ne!(bundle.actions.len(), 1),
2655 }
2656 }
2657
2658 #[test]
2661 fn all_pay_reads_the_value_carrying_outputs(bundle in arb_specified_bundle()) {
2662 let expected = bundle
2663 .actions
2664 .iter()
2665 .all(|a| a.output.value == Some(0) || a.output.recipient == Some(OURS));
2666
2667 prop_assert_eq!(bundle.value_carrying_outputs_all_pay(&OURS), Some(expected));
2668 }
2669
2670 #[test]
2674 fn zero_valued_dummies_do_not_change_the_answer(
2675 bundle in arb_bundle(),
2676 dummy_recipient in prop_oneof![Just(None), arb_recipient().prop_map(Some)],
2677 at in any::<Index>(),
2678 ) {
2679 let expected = bundle.value_carrying_outputs_all_pay(&OURS);
2680
2681 let mut padded = bundle.clone();
2682 let at = at.index(padded.actions.len() + 1);
2683 padded.actions.insert(at, action(Some(0), dummy_recipient));
2684
2685 prop_assert_eq!(padded.value_carrying_outputs_all_pay(&OURS), expected);
2686 }
2687
2688 #[test]
2691 fn a_redacted_value_is_unanswerable(bundle in arb_send_to_self_bundle(), at in any::<Index>()) {
2692 prop_assert_eq!(bundle.value_carrying_outputs_all_pay(&OURS), Some(true));
2693
2694 let mut redacted = bundle.clone();
2695 let at = at.index(redacted.actions.len());
2696 redacted.actions[at].output.value = None;
2697
2698 prop_assert_eq!(redacted.value_carrying_outputs_all_pay(&OURS), None);
2699 }
2700
2701 #[test]
2704 fn a_redacted_recipient_is_unanswerable_only_where_it_is_judged(
2705 bundle in arb_send_to_self_bundle(),
2706 at in any::<Index>(),
2707 ) {
2708 let mut redacted = bundle.clone();
2709 let at = at.index(redacted.actions.len());
2710 let carries_value = redacted.actions[at].output.value != Some(0);
2711 redacted.actions[at].output.recipient = None;
2712
2713 let expected = if carries_value { None } else { Some(true) };
2714 prop_assert_eq!(redacted.value_carrying_outputs_all_pay(&OURS), expected);
2715 }
2716
2717 #[test]
2720 fn all_pay_is_asked_about_a_specific_recipient(bundle in arb_send_to_self_bundle()) {
2721 let pays_nobody = bundle
2722 .actions
2723 .iter()
2724 .all(|a| a.output.value == Some(0));
2725
2726 prop_assert_eq!(bundle.value_carrying_outputs_all_pay(&THEIRS), Some(pays_nobody));
2727 }
2728 }
2729}