1use std::collections::HashMap;
2
3use sapling::note_encryption::{PreparedIncomingViewingKey, SaplingDomain};
4use zcash_keys::keys::{OutgoingViewingKey, UnifiedFullViewingKey};
5use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk};
6use zcash_primitives::{
7 transaction::Transaction, transaction::components::sapling::zip212_enforcement,
8};
9use zcash_protocol::{
10 ShieldedPool,
11 consensus::{self, BlockHeight, NetworkUpgrade},
12 memo::MemoBytes,
13 value::Zatoshis,
14};
15use zip32::Scope;
16
17use crate::data_api::DecryptedTransaction;
18
19#[cfg(feature = "orchard")]
20use orchard::note_encryption::{
21 DomainVersion, IronwoodVersion, NoteEncryptionDomain, OrchardVersion,
22};
23
24#[derive(Debug, Copy, Clone, PartialEq, Eq)]
26pub enum TransferType {
27 Incoming,
30 AccountInternal,
34 WalletInternal,
39 Outgoing,
42}
43
44pub struct DecryptedOutput<Note, AccountId> {
46 index: usize,
47 note: Note,
48 value_pool: ShieldedPool,
49 account: AccountId,
50 memo: MemoBytes,
51 transfer_type: TransferType,
52}
53
54impl<Note, AccountId> DecryptedOutput<Note, AccountId> {
55 pub fn new(
56 index: usize,
57 note: Note,
58 value_pool: ShieldedPool,
59 account: AccountId,
60 memo: MemoBytes,
61 transfer_type: TransferType,
62 ) -> Self {
63 Self {
64 index,
65 note,
66 value_pool,
67 account,
68 memo,
69 transfer_type,
70 }
71 }
72
73 pub fn index(&self) -> usize {
76 self.index
77 }
78
79 pub fn note(&self) -> &Note {
81 &self.note
82 }
83 pub fn value_pool(&self) -> ShieldedPool {
85 self.value_pool
86 }
87 pub fn account(&self) -> &AccountId {
89 &self.account
90 }
91
92 pub fn memo(&self) -> &MemoBytes {
94 &self.memo
95 }
96
97 pub fn transfer_type(&self) -> TransferType {
100 self.transfer_type
101 }
102}
103
104impl<A> DecryptedOutput<sapling::Note, A> {
105 pub fn note_value(&self) -> Zatoshis {
106 Zatoshis::from_u64(self.note.value().inner())
107 .expect("Sapling note value is expected to have been validated by consensus.")
108 }
109}
110
111#[cfg(feature = "orchard")]
112impl<A> DecryptedOutput<orchard::note::Note, A> {
113 pub fn note_value(&self) -> Zatoshis {
114 Zatoshis::from_u64(self.note.value().inner())
115 .expect("Orchard note value is expected to have been validated by consensus.")
116 }
117}
118
119fn candidate_ovks(ufvk: &UnifiedFullViewingKey) -> Vec<OutgoingViewingKey> {
131 let mut ovks = Vec::with_capacity(6);
135
136 #[cfg(feature = "orchard")]
137 if let Some(fvk) = ufvk.orchard() {
138 ovks.push(fvk.to_ovk(Scope::External).into());
139 ovks.push(fvk.to_ovk(Scope::Internal).into());
140 }
141
142 if let Some(dfvk) = ufvk.sapling() {
143 ovks.push(dfvk.to_ovk(Scope::External).into());
144 ovks.push(dfvk.to_ovk(Scope::Internal).into());
145 }
146
147 #[cfg(feature = "transparent-inputs")]
148 if let Some(pubkey) = ufvk.p2pkh() {
149 let shielding_ovks = pubkey.ovks_for_shielding();
150 ovks.push(shielding_ovks.external().as_bytes().into());
151 ovks.push(shielding_ovks.internal().as_bytes().into());
152 }
153
154 ovks
155}
156
157pub fn decrypt_transaction<'a, P: consensus::Parameters, AccountId: Copy>(
171 params: &P,
172 mined_height: Option<BlockHeight>,
173 chain_tip_height: Option<BlockHeight>,
174 tx: &'a Transaction,
175 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
176) -> DecryptedTransaction<'a, Transaction, AccountId> {
177 let zip212_enforcement = zip212_enforcement(
178 params,
179 mined_height.unwrap_or_else(|| {
183 chain_tip_height
184 .map(|max_height| max_height + 1) .or_else(|| params.activation_height(NetworkUpgrade::Sapling))
186 .unwrap_or_else(|| BlockHeight::from(0))
188 }),
189 );
190 let sapling_bundle = tx.sapling_bundle();
191 let sapling_outputs = sapling_bundle
192 .iter()
193 .flat_map(|bundle| {
194 ufvks.iter().flat_map(|(account, ufvk)| {
195 let account = *account;
196 let sapling_domain = SaplingDomain::new(zip212_enforcement);
197 let ivks = ufvk.sapling().map(|dfvk| {
201 (
202 PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::External)),
203 PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)),
204 )
205 });
206 let ovks = candidate_ovks(ufvk)
207 .into_iter()
208 .map(::sapling::keys::OutgoingViewingKey::from)
209 .collect::<Vec<_>>();
210
211 bundle
212 .shielded_outputs()
213 .iter()
214 .enumerate()
215 .flat_map(move |(index, output)| {
216 ivks.as_ref()
217 .and_then(|(ivk_external, ivk_internal)| {
218 try_note_decryption(&sapling_domain, ivk_external, output)
219 .map(|ret| (ret, TransferType::Incoming))
220 .or_else(|| {
221 try_note_decryption(&sapling_domain, ivk_internal, output)
222 .map(|ret| (ret, TransferType::AccountInternal))
223 })
224 })
225 .or_else(|| {
226 ovks.iter()
232 .find_map(|ovk| {
233 try_output_recovery_with_ovk(
234 &sapling_domain,
235 ovk,
236 output,
237 output.cv(),
238 output.out_ciphertext(),
239 )
240 })
241 .map(|ret| (ret, TransferType::Outgoing))
242 })
243 .into_iter()
244 .map(move |((note, _, memo), transfer_type)| {
245 DecryptedOutput::new(
246 index,
247 note,
248 ShieldedPool::Sapling,
249 account,
250 MemoBytes::from_bytes(&memo).expect("correct length"),
251 transfer_type,
252 )
253 })
254 })
255 })
256 })
257 .collect();
258
259 #[cfg(feature = "orchard")]
266 fn decrypt_orchard_protocol_bundle<V: DomainVersion, AccountId: Copy>(
267 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
268 bundle: &orchard::bundle::Bundle<
269 orchard::bundle::Authorized,
270 zcash_protocol::value::ZatBalance,
271 >,
272 pool: orchard::ValuePool,
273 ) -> impl Iterator<Item = DecryptedOutput<(orchard::Note, orchard::ValuePool), AccountId>> {
274 let shielded_pool = crate::wallet::shielded_pool_for_value_pool(pool);
275 ufvks.iter().flat_map(move |(account, ufvk)| {
276 let account = *account;
277 let ivks = ufvk.orchard().map(|fvk| {
281 (
282 orchard::keys::PreparedIncomingViewingKey::new(&fvk.to_ivk(Scope::External)),
283 orchard::keys::PreparedIncomingViewingKey::new(&fvk.to_ivk(Scope::Internal)),
284 )
285 });
286 let ovks = candidate_ovks(ufvk)
287 .into_iter()
288 .map(orchard::keys::OutgoingViewingKey::from)
289 .collect::<Vec<_>>();
290
291 bundle
292 .actions()
293 .iter()
294 .enumerate()
295 .flat_map(move |(index, action)| {
296 let domain = NoteEncryptionDomain::<V>::for_action(action);
297 ivks.as_ref()
298 .and_then(|(ivk_external, ivk_internal)| {
299 try_note_decryption(&domain, ivk_external, action)
300 .map(|ret| (ret, TransferType::Incoming))
301 .or_else(|| {
302 try_note_decryption(&domain, ivk_internal, action)
303 .map(|ret| (ret, TransferType::AccountInternal))
304 })
305 })
306 .or_else(|| {
307 ovks.iter()
313 .find_map(|ovk| {
314 try_output_recovery_with_ovk(
315 &domain,
316 ovk,
317 action,
318 action.cv_net(),
319 &action.encrypted_note().out_ciphertext,
320 )
321 })
322 .map(|ret| (ret, TransferType::Outgoing))
323 })
324 .into_iter()
325 .map(move |((note, _, memo), transfer_type)| {
326 DecryptedOutput::new(
327 index,
328 (note, pool),
329 shielded_pool,
330 account,
331 MemoBytes::from_bytes(&memo).expect("correct length"),
332 transfer_type,
333 )
334 })
335 })
336 })
337 }
338
339 #[cfg(feature = "orchard")]
340 let orchard_outputs = tx
341 .orchard_bundle()
342 .iter()
343 .flat_map(|bundle| {
344 decrypt_orchard_protocol_bundle::<OrchardVersion, _>(
345 ufvks,
346 bundle,
347 orchard::ValuePool::Orchard,
348 )
349 })
350 .collect();
351
352 #[cfg(feature = "orchard")]
353 let ironwood_outputs = tx
354 .ironwood_bundle()
355 .iter()
356 .flat_map(|bundle| {
357 decrypt_orchard_protocol_bundle::<IronwoodVersion, _>(
358 ufvks,
359 bundle,
360 orchard::ValuePool::Ironwood,
361 )
362 })
363 .collect();
364
365 DecryptedTransaction::new(
366 mined_height,
367 tx,
368 sapling_outputs,
369 #[cfg(feature = "orchard")]
370 orchard_outputs,
371 #[cfg(feature = "orchard")]
372 ironwood_outputs,
373 )
374}
375
376#[cfg(test)]
377mod tests {
378 use std::collections::HashMap;
379
380 use rand_chacha::ChaChaRng;
381 use rand_core::{CryptoRng, SeedableRng};
382 use sapling::{
383 note_encryption::sapling_note_encryption,
384 util::generate_random_rseed,
385 value::{NoteValue, ValueCommitTrapdoor, ValueCommitment},
386 };
387 use zcash_keys::keys::{OutgoingViewingKey, UnifiedFullViewingKey, UnifiedSpendingKey};
388 use zcash_note_encryption::Domain;
389 use zcash_primitives::transaction::{
390 Authorized, Transaction, TransactionData, TxVersion,
391 components::sapling::zip212_enforcement,
392 };
393 use zcash_protocol::{
394 consensus::{BlockHeight, BranchId, Network},
395 memo::MemoBytes,
396 value::{ZatBalance, Zatoshis},
397 };
398 use zip32::{AccountId, Scope};
399
400 use super::{SaplingDomain, TransferType, decrypt_transaction};
401
402 const MINED_HEIGHT: u32 = 2_000_000;
405
406 const VALUE: Zatoshis = Zatoshis::const_from_u64(100_000);
407
408 fn wallet_ufvk() -> UnifiedFullViewingKey {
410 UnifiedSpendingKey::from_seed(&Network::TestNetwork, &[0xab; 32], AccountId::ZERO)
411 .expect("seed produces a valid unified spending key")
412 .to_unified_full_viewing_key()
413 }
414
415 fn counterparty_ufvk() -> UnifiedFullViewingKey {
418 UnifiedSpendingKey::from_seed(&Network::TestNetwork, &[0xcd; 32], AccountId::ZERO)
419 .expect("seed produces a valid unified spending key")
420 .to_unified_full_viewing_key()
421 }
422
423 fn zip316_sender_ovks(ufvk: &UnifiedFullViewingKey) -> Vec<(&'static str, OutgoingViewingKey)> {
428 let mut ovks: Vec<(&'static str, OutgoingViewingKey)> = Vec::new();
429
430 #[cfg(feature = "orchard")]
431 {
432 let fvk = ufvk.orchard().expect("UFVK has an Orchard item");
433 ovks.push(("Orchard external", fvk.to_ovk(Scope::External).into()));
434 ovks.push(("Orchard internal", fvk.to_ovk(Scope::Internal).into()));
435 }
436
437 let dfvk = ufvk.sapling().expect("UFVK has a Sapling item");
438 ovks.push(("Sapling external", dfvk.to_ovk(Scope::External).into()));
439 ovks.push(("Sapling internal", dfvk.to_ovk(Scope::Internal).into()));
440
441 #[cfg(feature = "transparent-inputs")]
442 {
443 let tfvk = ufvk.p2pkh().expect("UFVK has a transparent item");
444 let shielding_ovks = tfvk.ovks_for_shielding();
445 assert_eq!(
449 shielding_ovks.external().as_bytes(),
450 tfvk.external_ovk().as_bytes()
451 );
452 assert_eq!(
453 shielding_ovks.internal().as_bytes(),
454 tfvk.internal_ovk().as_bytes()
455 );
456 ovks.push((
457 "transparent external",
458 shielding_ovks.external().as_bytes().into(),
459 ));
460 ovks.push((
461 "transparent internal",
462 shielding_ovks.internal().as_bytes().into(),
463 ));
464 }
465
466 ovks
467 }
468
469 fn foreign_ovk() -> OutgoingViewingKey {
471 OutgoingViewingKey::from([0x5a; 32])
472 }
473
474 fn sapling_tx_paying(
480 recipient: ::sapling::PaymentAddress,
481 ovk: OutgoingViewingKey,
482 rng: &mut impl CryptoRng,
483 ) -> Transaction {
484 let enforcement =
485 zip212_enforcement(&Network::TestNetwork, BlockHeight::from_u32(MINED_HEIGHT));
486 let value = NoteValue::from_raw(VALUE.into_u64());
487 let rseed = generate_random_rseed(enforcement, rng);
488 let note = ::sapling::Note::from_parts(recipient, value, rseed);
489 let cv = ValueCommitment::derive(value, ValueCommitTrapdoor::random(&mut *rng));
490 let cmu = note.cmu();
491
492 let encryptor =
493 sapling_note_encryption(Some(ovk.into()), note, MemoBytes::empty().into_bytes(), rng);
494 let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk());
495 let enc_ciphertext = encryptor.encrypt_note_plaintext();
496 let out_ciphertext = encryptor.encrypt_outgoing_plaintext(&cv, &cmu, rng);
497
498 let output = ::sapling::bundle::OutputDescription::from_parts(
499 cv,
500 cmu,
501 ephemeral_key,
502 enc_ciphertext.0,
503 out_ciphertext,
504 [0u8; core::mem::size_of::<::sapling::bundle::GrothProofBytes>()],
505 );
506
507 let bundle = ::sapling::Bundle::from_parts(
508 vec![],
509 vec![output],
510 ZatBalance::const_from_i64(-(VALUE.into_u64() as i64)),
511 ::sapling::bundle::Authorized {
512 binding_sig: redjubjub::Signature::from([0u8; 64]),
513 },
514 )
515 .expect("the bundle has an output");
516
517 TransactionData::<Authorized>::from_parts(
518 TxVersion::V5,
519 BranchId::Nu5,
520 0,
521 BlockHeight::from_u32(MINED_HEIGHT + 100),
522 None,
523 None,
524 Some(bundle),
525 None,
526 )
527 .freeze()
528 .expect("the transaction data is well-formed")
529 }
530
531 fn decrypt_sapling(tx: &Transaction) -> Vec<(TransferType, Zatoshis)> {
534 let mut ufvks = HashMap::new();
535 ufvks.insert(0u32, wallet_ufvk());
536
537 decrypt_transaction(
538 &Network::TestNetwork,
539 Some(BlockHeight::from_u32(MINED_HEIGHT)),
540 None,
541 tx,
542 &ufvks,
543 )
544 .sapling_outputs()
545 .iter()
546 .map(|output| (output.transfer_type(), output.note_value()))
547 .collect()
548 }
549
550 #[test]
553 fn sapling_output_recovered_with_every_zip316_ovk() {
554 let mut rng = ChaChaRng::seed_from_u64(0);
555 let recipient = counterparty_ufvk()
556 .sapling()
557 .expect("UFVK has a Sapling item")
558 .default_address()
559 .1;
560
561 for (label, ovk) in zip316_sender_ovks(&wallet_ufvk()) {
562 let tx = sapling_tx_paying(recipient, ovk, &mut rng);
563 assert_eq!(
564 decrypt_sapling(&tx),
565 vec![(TransferType::Outgoing, VALUE)],
566 "output encrypted under the {label} OVK was not recovered"
567 );
568 }
569 }
570
571 #[test]
573 fn sapling_output_not_recovered_with_foreign_ovk() {
574 let mut rng = ChaChaRng::seed_from_u64(1);
575 let recipient = counterparty_ufvk()
576 .sapling()
577 .expect("UFVK has a Sapling item")
578 .default_address()
579 .1;
580
581 let tx = sapling_tx_paying(recipient, foreign_ovk(), &mut rng);
582 assert_eq!(decrypt_sapling(&tx), vec![]);
583 }
584
585 #[cfg(feature = "orchard")]
592 fn orchard_tx_paying(
593 recipient: ::orchard::Address,
594 ovk: OutgoingViewingKey,
595 rng: &mut impl CryptoRng,
596 ) -> Transaction {
597 use ::orchard::{
598 Anchor, Proof,
599 bundle::{Authorized as OrchardAuthorized, BundleVersion, Flags},
600 note::{
601 ExtractedNoteCommitment, Nullifier, RandomSeed, Rho, TransmittedNoteCiphertext,
602 },
603 note_encryption::OrchardNoteEncryption,
604 primitives::redpallas::{SigningKey, SpendAuth, VerificationKey},
605 value::{NoteValue as OrchardNoteValue, ValueCommitTrapdoor, ValueCommitment},
606 };
607 use nonempty::NonEmpty;
608
609 let mut nf_bytes = [0u8; 32];
610 rng.fill_bytes(&mut nf_bytes);
611 let nf_old = loop {
612 let nf = Nullifier::from_bytes(&nf_bytes);
613 if nf.is_some().into() {
614 break nf.unwrap();
615 }
616 rng.fill_bytes(&mut nf_bytes);
617 };
618 let rho = Rho::from_bytes(&nf_old.to_bytes()).expect("a nullifier is a canonical Rho");
622 let rseed = loop {
623 let mut bytes = [0u8; 32];
624 rng.fill_bytes(&mut bytes);
625 let rseed = RandomSeed::from_bytes(bytes, &rho);
626 if rseed.is_some().into() {
627 break rseed.unwrap();
628 }
629 };
630
631 let value = OrchardNoteValue::from_raw(VALUE.into_u64());
632 let note =
633 ::orchard::Note::from_parts(recipient, value, rho, rseed, ::orchard::NoteVersion::V2)
634 .expect("the note parts are consistent");
635 let cmx = ExtractedNoteCommitment::from(note.commitment());
636 let cv_net = ValueCommitment::derive(
637 OrchardNoteValue::from_raw(0) - value,
638 ValueCommitTrapdoor::from_bytes([0u8; 32]).expect("zero is a canonical scalar"),
639 );
640
641 let encryptor = OrchardNoteEncryption::new(Some(ovk.into()), note, [0u8; 512]);
642 let encrypted_note = TransmittedNoteCiphertext {
643 epk_bytes: ::orchard::note_encryption::OrchardDomain::epk_bytes(encryptor.epk()).0,
644 enc_ciphertext: encryptor.encrypt_note_plaintext(),
645 out_ciphertext: encryptor.encrypt_outgoing_plaintext(&cv_net, &cmx, rng),
646 };
647
648 let rk = loop {
649 let mut rk_bytes = [0u8; 32];
650 rng.fill_bytes(&mut rk_bytes);
651 if let Ok(sk) = SigningKey::<SpendAuth>::try_from(rk_bytes) {
652 break VerificationKey::<SpendAuth>::from(&sk);
653 }
654 };
655
656 let action = ::orchard::Action::from_parts(
657 nf_old,
658 rk,
659 cmx,
660 encrypted_note,
661 cv_net,
662 ::orchard::primitives::redpallas::Signature::from([0u8; 64]),
663 )
664 .expect("the action parts are valid");
665
666 let bundle = ::orchard::Bundle::try_from_parts(
667 NonEmpty::new(action),
668 Flags::ENABLED,
669 ZatBalance::const_from_i64(-(VALUE.into_u64() as i64)),
670 Anchor::empty_tree(),
671 OrchardAuthorized::from_parts(
672 Proof::new(vec![0u8; Proof::expected_proof_size(1)]),
673 ::orchard::primitives::redpallas::Signature::from([0u8; 64]),
674 ),
675 BundleVersion::orchard_v2(),
676 )
677 .expect("the bundle parts are valid");
678
679 TransactionData::<Authorized>::from_parts(
680 TxVersion::V5,
681 BranchId::Nu5,
682 0,
683 BlockHeight::from_u32(MINED_HEIGHT + 100),
684 None,
685 None,
686 None,
687 Some(bundle),
688 )
689 .freeze()
690 .expect("the transaction data is well-formed")
691 }
692
693 #[cfg(feature = "orchard")]
696 fn decrypt_orchard_with(
697 ufvk: UnifiedFullViewingKey,
698 tx: &Transaction,
699 ) -> Vec<(TransferType, Zatoshis)> {
700 let mut ufvks = HashMap::new();
701 ufvks.insert(0u32, ufvk);
702
703 decrypt_transaction(
704 &Network::TestNetwork,
705 Some(BlockHeight::from_u32(MINED_HEIGHT)),
706 None,
707 tx,
708 &ufvks,
709 )
710 .orchard_outputs()
711 .iter()
712 .map(|output| {
713 let (note, _) = output.note();
714 (
715 output.transfer_type(),
716 Zatoshis::from_u64(note.value().inner()).expect("note value is in range"),
717 )
718 })
719 .collect()
720 }
721
722 #[cfg(feature = "orchard")]
723 fn decrypt_orchard(tx: &Transaction) -> Vec<(TransferType, Zatoshis)> {
724 decrypt_orchard_with(wallet_ufvk(), tx)
725 }
726
727 #[cfg(feature = "orchard")]
733 #[test]
734 fn orchard_action_recovered_with_every_zip316_ovk() {
735 let mut rng = ChaChaRng::seed_from_u64(2);
736 let recipient = counterparty_ufvk()
737 .orchard()
738 .expect("UFVK has an Orchard item")
739 .address_at(0u32, Scope::External);
740
741 for (label, ovk) in zip316_sender_ovks(&wallet_ufvk()) {
742 let tx = orchard_tx_paying(recipient, ovk, &mut rng);
743 assert_eq!(
744 decrypt_orchard(&tx),
745 vec![(TransferType::Outgoing, VALUE)],
746 "action encrypted under the {label} OVK was not recovered"
747 );
748 }
749 }
750
751 #[cfg(feature = "orchard")]
753 #[test]
754 fn orchard_action_not_recovered_with_foreign_ovk() {
755 let mut rng = ChaChaRng::seed_from_u64(3);
756 let recipient = counterparty_ufvk()
757 .orchard()
758 .expect("UFVK has an Orchard item")
759 .address_at(0u32, Scope::External);
760
761 let tx = orchard_tx_paying(recipient, foreign_ovk(), &mut rng);
762 assert_eq!(decrypt_orchard(&tx), vec![]);
763 }
764
765 #[cfg(feature = "orchard")]
770 #[test]
771 fn orchard_action_recovered_by_ufvk_without_orchard_item() {
772 let mut rng = ChaChaRng::seed_from_u64(4);
773 let recipient = counterparty_ufvk()
774 .orchard()
775 .expect("UFVK has an Orchard item")
776 .address_at(0u32, Scope::External);
777
778 let wallet = wallet_ufvk();
779 let dfvk = wallet.sapling().expect("UFVK has a Sapling item").clone();
780 let sapling_only = UnifiedFullViewingKey::new(
781 #[cfg(feature = "transparent-inputs")]
782 None,
783 Some(dfvk.clone()),
784 #[cfg(feature = "orchard")]
785 None,
786 )
787 .expect("a Sapling-only UFVK is valid");
788
789 let tx = orchard_tx_paying(recipient, dfvk.to_ovk(Scope::External).into(), &mut rng);
790 assert_eq!(
791 decrypt_orchard_with(sapling_only, &tx),
792 vec![(TransferType::Outgoing, VALUE)]
793 );
794 }
795}