zcash_client_backend/
decrypt.rs

1use std::collections::HashMap;
2
3use sapling::note_encryption::{PreparedIncomingViewingKey, SaplingDomain};
4use zcash_keys::keys::UnifiedFullViewingKey;
5use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk};
6use zcash_primitives::{
7    transaction::components::sapling::zip212_enforcement, transaction::Transaction,
8};
9use zcash_protocol::{
10    consensus::{self, BlockHeight, NetworkUpgrade},
11    memo::MemoBytes,
12    value::Zatoshis,
13};
14use zip32::Scope;
15
16use crate::data_api::DecryptedTransaction;
17
18#[cfg(feature = "orchard")]
19use orchard::note_encryption::OrchardDomain;
20
21/// An enumeration of the possible relationships a TXO can have to the wallet.
22#[derive(Debug, Copy, Clone, PartialEq, Eq)]
23pub enum TransferType {
24    /// The output was received on one of the wallet's external addresses via decryption using the
25    /// associated incoming viewing key, or at one of the wallet's transparent addresses.
26    Incoming,
27    /// The output was received on one of the wallet's internal-only shielded addresses via trial
28    /// decryption using one of the wallet's internal incoming viewing keys.
29    WalletInternal,
30    /// The output was decrypted using one of the wallet's outgoing viewing keys, or was created
31    /// in a transaction constructed by this wallet.
32    Outgoing,
33}
34
35/// A decrypted shielded output.
36pub struct DecryptedOutput<Note, AccountId> {
37    index: usize,
38    note: Note,
39    account: AccountId,
40    memo: MemoBytes,
41    transfer_type: TransferType,
42}
43
44impl<Note, AccountId> DecryptedOutput<Note, AccountId> {
45    pub fn new(
46        index: usize,
47        note: Note,
48        account: AccountId,
49        memo: MemoBytes,
50        transfer_type: TransferType,
51    ) -> Self {
52        Self {
53            index,
54            note,
55            account,
56            memo,
57            transfer_type,
58        }
59    }
60
61    /// The index of the output within the shielded outputs of the Sapling bundle or the actions of
62    /// the Orchard bundle, depending upon the type of [`Self::note`].
63    pub fn index(&self) -> usize {
64        self.index
65    }
66
67    /// The note within the output.
68    pub fn note(&self) -> &Note {
69        &self.note
70    }
71
72    /// The account that decrypted the note.
73    pub fn account(&self) -> &AccountId {
74        &self.account
75    }
76
77    /// The memo bytes included with the note.
78    pub fn memo(&self) -> &MemoBytes {
79        &self.memo
80    }
81
82    /// Returns a [`TransferType`] value that is determined based upon what type of key was used to
83    /// decrypt the transaction.
84    pub fn transfer_type(&self) -> TransferType {
85        self.transfer_type
86    }
87}
88
89impl<A> DecryptedOutput<sapling::Note, A> {
90    pub fn note_value(&self) -> Zatoshis {
91        Zatoshis::from_u64(self.note.value().inner())
92            .expect("Sapling note value is expected to have been validated by consensus.")
93    }
94}
95
96#[cfg(feature = "orchard")]
97impl<A> DecryptedOutput<orchard::note::Note, A> {
98    pub fn note_value(&self) -> Zatoshis {
99        Zatoshis::from_u64(self.note.value().inner())
100            .expect("Orchard note value is expected to have been validated by consensus.")
101    }
102}
103
104/// Scans a [`Transaction`] for any information that can be decrypted by the set of
105/// [`UnifiedFullViewingKey`]s.
106///
107/// # Parameters
108/// - `params`: The network parameters corresponding to the network the transaction
109///   was created for.
110/// - `mined_height`: The height at which the transaction was mined, or `None` for
111///   unmined transactions.
112/// - `chain_tip_height`: The current chain tip height, if known. This parameter
113///   will be unused if `mined_height.is_some()`.
114/// - `tx`: The transaction to decrypt.
115/// - `ufvks`: The [`UnifiedFullViewingKey`]s to use in trial decryption, keyed
116///   by the identifiers for the wallet accounts they correspond to.
117pub fn decrypt_transaction<'a, P: consensus::Parameters, AccountId: Copy>(
118    params: &P,
119    mined_height: Option<BlockHeight>,
120    chain_tip_height: Option<BlockHeight>,
121    tx: &'a Transaction,
122    ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
123) -> DecryptedTransaction<'a, AccountId> {
124    let zip212_enforcement = zip212_enforcement(
125        params,
126        // Height is block height for mined transactions, and the "mempool height" (chain height + 1)
127        // for mempool transactions. We fall back to Sapling activation if we have no other
128        // information.
129        mined_height.unwrap_or_else(|| {
130            chain_tip_height
131                .map(|max_height| max_height + 1) // "mempool height"
132                .or_else(|| params.activation_height(NetworkUpgrade::Sapling))
133                .expect("Sapling activation height must be known.")
134        }),
135    );
136    let sapling_bundle = tx.sapling_bundle();
137    let sapling_outputs = sapling_bundle
138        .iter()
139        .flat_map(|bundle| {
140            ufvks
141                .iter()
142                .flat_map(|(account, ufvk)| ufvk.sapling().into_iter().map(|dfvk| (*account, dfvk)))
143                .flat_map(|(account, dfvk)| {
144                    let sapling_domain = SaplingDomain::new(zip212_enforcement);
145                    let ivk_external =
146                        PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::External));
147                    let ivk_internal =
148                        PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
149                    let ovk = dfvk.fvk().ovk;
150
151                    bundle
152                        .shielded_outputs()
153                        .iter()
154                        .enumerate()
155                        .flat_map(move |(index, output)| {
156                            try_note_decryption(&sapling_domain, &ivk_external, output)
157                                .map(|ret| (ret, TransferType::Incoming))
158                                .or_else(|| {
159                                    try_note_decryption(&sapling_domain, &ivk_internal, output)
160                                        .map(|ret| (ret, TransferType::WalletInternal))
161                                })
162                                .or_else(|| {
163                                    try_output_recovery_with_ovk(
164                                        &sapling_domain,
165                                        &ovk,
166                                        output,
167                                        output.cv(),
168                                        output.out_ciphertext(),
169                                    )
170                                    .map(|ret| (ret, TransferType::Outgoing))
171                                })
172                                .into_iter()
173                                .map(move |((note, _, memo), transfer_type)| {
174                                    DecryptedOutput::new(
175                                        index,
176                                        note,
177                                        account,
178                                        MemoBytes::from_bytes(&memo).expect("correct length"),
179                                        transfer_type,
180                                    )
181                                })
182                        })
183                })
184        })
185        .collect();
186
187    #[cfg(feature = "orchard")]
188    let orchard_bundle = tx.orchard_bundle();
189    #[cfg(feature = "orchard")]
190    let orchard_outputs = orchard_bundle
191        .iter()
192        .flat_map(|bundle| {
193            ufvks
194                .iter()
195                .flat_map(|(account, ufvk)| ufvk.orchard().into_iter().map(|fvk| (*account, fvk)))
196                .flat_map(|(account, fvk)| {
197                    let ivk_external = orchard::keys::PreparedIncomingViewingKey::new(
198                        &fvk.to_ivk(Scope::External),
199                    );
200                    let ivk_internal = orchard::keys::PreparedIncomingViewingKey::new(
201                        &fvk.to_ivk(Scope::Internal),
202                    );
203                    let ovk = fvk.to_ovk(Scope::External);
204
205                    bundle
206                        .actions()
207                        .iter()
208                        .enumerate()
209                        .flat_map(move |(index, action)| {
210                            let domain = OrchardDomain::for_action(action);
211                            try_note_decryption(&domain, &ivk_external, action)
212                                .map(|ret| (ret, TransferType::Incoming))
213                                .or_else(|| {
214                                    try_note_decryption(&domain, &ivk_internal, action)
215                                        .map(|ret| (ret, TransferType::WalletInternal))
216                                })
217                                .or_else(|| {
218                                    try_output_recovery_with_ovk(
219                                        &domain,
220                                        &ovk,
221                                        action,
222                                        action.cv_net(),
223                                        &action.encrypted_note().out_ciphertext,
224                                    )
225                                    .map(|ret| (ret, TransferType::Outgoing))
226                                })
227                                .into_iter()
228                                .map(move |((note, _, memo), transfer_type)| {
229                                    DecryptedOutput::new(
230                                        index,
231                                        note,
232                                        account,
233                                        MemoBytes::from_bytes(&memo).expect("correct length"),
234                                        transfer_type,
235                                    )
236                                })
237                        })
238                })
239        })
240        .collect();
241
242    DecryptedTransaction::new(
243        mined_height,
244        tx,
245        sapling_outputs,
246        #[cfg(feature = "orchard")]
247        orchard_outputs,
248    )
249}