Skip to main content

pepper_sync/
keys.rs

1//! Copied and modified from LRZ due to thread safety limitations and missing OVK
2
3use std::collections::HashMap;
4
5use incrementalmerkletree::Position;
6use orchard::{
7    keys::{FullViewingKey, IncomingViewingKey},
8    note_encryption::{IronwoodDomain, OrchardDomain},
9};
10use sapling_crypto::{
11    self as sapling, NullifierDerivingKey, SaplingIvk, note_encryption::SaplingDomain,
12};
13use zcash_address::{ZcashAddress, unified::ParseError};
14use zcash_keys::{address::UnifiedAddress, keys::UnifiedFullViewingKey};
15use zcash_note_encryption::Domain;
16use zcash_protocol::consensus;
17use zip32::Scope;
18
19use crate::wallet::KeyIdInterface;
20
21pub mod transparent;
22
23/// Child index for the `address_index` path level in the BIP44 hierarchy.
24pub type AddressIndex = u32;
25
26/// Unique ID for shielded keys.
27#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
28pub struct KeyId {
29    /// Account ID
30    pub account_id: zip32::AccountId,
31    /// Scope
32    pub scope: Scope,
33}
34
35impl KeyId {
36    pub(crate) fn from_parts(account_id: zip32::AccountId, scope: Scope) -> Self {
37        Self { account_id, scope }
38    }
39}
40
41impl KeyIdInterface for KeyId {
42    fn account_id(&self) -> zip32::AccountId {
43        self.account_id
44    }
45}
46
47impl memuse::DynamicUsage for KeyId {
48    fn dynamic_usage(&self) -> usize {
49        self.scope.dynamic_usage()
50    }
51
52    fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
53        self.scope.dynamic_usage_bounds()
54    }
55}
56
57/// A key that can be used to perform trial decryption and nullifier
58/// computation for a `CompactSaplingOutput` or `CompactOrchardAction`.
59pub trait ScanningKeyOps<D: Domain, Nf> {
60    /// Prepare the key for use in batch trial decryption.
61    fn prepare(&self) -> D::IncomingViewingKey;
62
63    /// Returns the account identifier for this key. An account identifier corresponds
64    /// to at most a single unified spending key's worth of spend authority, such that
65    /// both received notes and change spendable by that spending authority will be
66    /// interpreted as belonging to that account.
67    fn account_id(&self) -> &zip32::AccountId;
68
69    /// Returns the [`zip32::Scope`] for which this key was derived, if known.
70    fn key_scope(&self) -> Option<Scope>;
71
72    /// Produces the nullifier for the specified note and witness, if possible.
73    ///
74    /// IVK-based implementations of this trait cannot successfully derive
75    /// nullifiers, in which this function will always return `None`.
76    fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf>;
77}
78impl<D: Domain, Nf, K: ScanningKeyOps<D, Nf>> ScanningKeyOps<D, Nf> for &K {
79    fn prepare(&self) -> D::IncomingViewingKey {
80        (*self).prepare()
81    }
82
83    fn account_id(&self) -> &zip32::AccountId {
84        (*self).account_id()
85    }
86
87    fn key_scope(&self) -> Option<Scope> {
88        (*self).key_scope()
89    }
90
91    fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
92        (*self).nf(note, note_position)
93    }
94}
95
96pub(crate) struct ScanningKey<Ivk, Nk> {
97    key_id: KeyId,
98    ivk: Ivk,
99    nk: Option<Nk>,
100}
101
102impl ScanningKeyOps<SaplingDomain, sapling::Nullifier>
103    for ScanningKey<SaplingIvk, NullifierDerivingKey>
104{
105    fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey {
106        sapling_crypto::note_encryption::PreparedIncomingViewingKey::new(&self.ivk)
107    }
108
109    fn nf(&self, note: &sapling::Note, position: Position) -> Option<sapling::Nullifier> {
110        self.nk.as_ref().map(|key| note.nf(key, position.into()))
111    }
112
113    fn account_id(&self) -> &zip32::AccountId {
114        &self.key_id.account_id
115    }
116
117    fn key_scope(&self) -> Option<Scope> {
118        Some(self.key_id.scope)
119    }
120}
121
122impl ScanningKeyOps<OrchardDomain, orchard::note::Nullifier>
123    for ScanningKey<IncomingViewingKey, FullViewingKey>
124{
125    fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey {
126        orchard::keys::PreparedIncomingViewingKey::new(&self.ivk)
127    }
128
129    fn nf(
130        &self,
131        note: &orchard::note::Note,
132        _position: Position,
133    ) -> Option<orchard::note::Nullifier> {
134        self.nk.as_ref().map(|key| note.nullifier(key))
135    }
136
137    fn account_id(&self) -> &zip32::AccountId {
138        &self.key_id.account_id
139    }
140
141    fn key_scope(&self) -> Option<Scope> {
142        Some(self.key_id.scope)
143    }
144}
145
146impl ScanningKeyOps<IronwoodDomain, orchard::note::Nullifier>
147    for ScanningKey<IncomingViewingKey, FullViewingKey>
148{
149    fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey {
150        orchard::keys::PreparedIncomingViewingKey::new(&self.ivk)
151    }
152
153    fn nf(
154        &self,
155        note: &orchard::note::Note,
156        _position: Position,
157    ) -> Option<orchard::note::Nullifier> {
158        self.nk.as_ref().map(|key| note.nullifier(key))
159    }
160
161    fn account_id(&self) -> &zip32::AccountId {
162        &self.key_id.account_id
163    }
164
165    fn key_scope(&self) -> Option<Scope> {
166        Some(self.key_id.scope)
167    }
168}
169
170/// A set of keys to be used in scanning for decryptable transaction outputs.
171pub(crate) struct ScanningKeys {
172    pub(crate) sapling: HashMap<KeyId, ScanningKey<SaplingIvk, NullifierDerivingKey>>,
173    pub(crate) orchard: HashMap<KeyId, ScanningKey<IncomingViewingKey, FullViewingKey>>,
174    /// Ironwood scans with the same key material as Orchard, held separately
175    /// so the ironwood decryption batch runner has its own key set.
176    pub(crate) ironwood: HashMap<KeyId, ScanningKey<IncomingViewingKey, FullViewingKey>>,
177}
178
179impl ScanningKeys {
180    /// Constructs a [`ScanningKeys`] from an iterator of [`zcash_keys::keys::UnifiedFullViewingKey`]s,
181    /// along with the account identifiers corresponding to those UFVKs.
182    pub(crate) fn from_account_ufvks(
183        ufvks: impl IntoIterator<Item = (zip32::AccountId, UnifiedFullViewingKey)>,
184    ) -> Self {
185        #![allow(clippy::type_complexity)]
186
187        let mut sapling: HashMap<KeyId, ScanningKey<SaplingIvk, NullifierDerivingKey>> =
188            HashMap::new();
189        let mut orchard: HashMap<KeyId, ScanningKey<IncomingViewingKey, FullViewingKey>> =
190            HashMap::new();
191        let mut ironwood: HashMap<KeyId, ScanningKey<IncomingViewingKey, FullViewingKey>> =
192            HashMap::new();
193
194        for (account_id, ufvk) in ufvks {
195            if let Some(dfvk) = ufvk.sapling() {
196                for scope in [Scope::External, Scope::Internal] {
197                    let key_id = KeyId::from_parts(account_id, scope);
198                    sapling.insert(
199                        key_id,
200                        ScanningKey {
201                            key_id,
202                            ivk: dfvk.to_ivk(scope),
203                            nk: Some(dfvk.to_nk(scope)),
204                        },
205                    );
206                }
207            }
208
209            if let Some(fvk) = ufvk.orchard() {
210                for scope in [Scope::External, Scope::Internal] {
211                    let key_id = KeyId::from_parts(account_id, scope);
212                    orchard.insert(
213                        key_id,
214                        ScanningKey {
215                            key_id,
216                            ivk: fvk.to_ivk(scope),
217                            nk: Some(fvk.clone()),
218                        },
219                    );
220                    ironwood.insert(
221                        key_id,
222                        ScanningKey {
223                            key_id,
224                            ivk: fvk.to_ivk(scope),
225                            nk: Some(fvk.clone()),
226                        },
227                    );
228                }
229            }
230        }
231
232        Self {
233            sapling,
234            orchard,
235            ironwood,
236        }
237    }
238}
239
240pub(crate) fn encode_orchard_receiver(
241    parameters: &impl consensus::Parameters,
242    orchard_address: &orchard::Address,
243) -> Result<String, ParseError> {
244    Ok(zcash_address::unified::Encoding::encode(
245        &<zcash_address::unified::Address as zcash_address::unified::Encoding>::try_from_items(
246            zcash_address::unified::Revision::R0,
247            vec![zcash_address::unified::Uitem::Data(
248                zcash_address::unified::Receiver::Orchard(orchard_address.to_raw_address_bytes()),
249            )],
250        )?,
251        &parameters.network_type(),
252    ))
253}
254
255/// Decode string to unified address.
256// TODO: return custom error type
257pub fn decode_unified_address(
258    consensus_parameters: &impl consensus::Parameters,
259    encoded_address: &str,
260) -> std::io::Result<UnifiedAddress> {
261    if let zcash_keys::address::Address::Unified(unified_address) =
262        decode_address(consensus_parameters, encoded_address)?
263    {
264        Ok(*unified_address)
265    } else {
266        Err(std::io::Error::new(
267            std::io::ErrorKind::InvalidData,
268            "failed to decode unified address. incorrect address type.".to_string(),
269        ))
270    }
271}
272
273/// Decode string to [`zcash_keys::address::Address`] enum.
274pub fn decode_address(
275    consensus_parameters: &impl consensus::Parameters,
276    encoded_address: &str,
277) -> std::io::Result<zcash_keys::address::Address> {
278    ZcashAddress::try_from_encoded(encoded_address)
279        .map_err(|e| {
280            std::io::Error::new(
281                std::io::ErrorKind::InvalidData,
282                format!("failed to decode address. {e}"),
283            )
284        })?
285        .convert_if_network::<zcash_keys::address::Address>(consensus_parameters.network_type())
286        .map_err(|e| {
287            std::io::Error::new(
288                std::io::ErrorKind::InvalidData,
289                format!("failed to decode address. {e}"),
290            )
291        })
292}