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