diff --git a/zingolib/src/blaze/trial_decryptions.rs b/zingolib/src/blaze/trial_decryptions.rs index a1ba2bf469..cd2f5d4a51 100644 --- a/zingolib/src/blaze/trial_decryptions.rs +++ b/zingolib/src/blaze/trial_decryptions.rs @@ -5,6 +5,7 @@ use crate::error::ZingoLibResult; +use crate::wallet::keys::unified::{External, Fvk as _, Ivk}; use crate::wallet::notes::ShieldedNoteInterface; use crate::wallet::{ data::PoolNullifier, @@ -17,9 +18,8 @@ use crate::wallet::{ use futures::{stream::FuturesUnordered, StreamExt}; use incrementalmerkletree::{Position, Retention}; use log::debug; -use orchard::{keys::IncomingViewingKey as OrchardIvk, note_encryption::OrchardDomain}; +use orchard::note_encryption::OrchardDomain; use sapling_crypto::note_encryption::SaplingDomain; -use sapling_crypto::SaplingIvk; use std::sync::Arc; use tokio::{ sync::{ @@ -89,8 +89,12 @@ impl TrialDecryptions { let mut workers = FuturesUnordered::new(); let mut cbs = vec![]; - let sapling_ivk = SaplingIvk::try_from(&*wc).ok(); - let orchard_ivk = orchard::keys::IncomingViewingKey::try_from(&*wc).ok(); + let sapling_ivk = sapling_crypto::zip32::DiversifiableFullViewingKey::try_from(&*wc) + .ok() + .map(|key| key.derive_ivk()); + let orchard_ivk = orchard::keys::FullViewingKey::try_from(&*wc) + .ok() + .map(|key| key.derive_ivk()); while let Some(cb) = receiver.recv().await { cbs.push(cb); @@ -128,8 +132,8 @@ impl TrialDecryptions { compact_blocks: Vec, wc: Arc, bsync_data: Arc>, - sapling_ivk: Option, - orchard_ivk: Option, + sapling_ivk: Option>, + orchard_ivk: Option>, transaction_metadata_set: Arc>, transaction_size_filter: Option, detected_transaction_id_sender: UnboundedSender<( @@ -179,7 +183,7 @@ impl TrialDecryptions { compact_transaction, transaction_num, &compact_block, - sapling_crypto::note_encryption::PreparedIncomingViewingKey::new(ivk), + ivk.ivk.clone(), height, &config, &wc, @@ -199,7 +203,7 @@ impl TrialDecryptions { compact_transaction, transaction_num, &compact_block, - orchard::keys::PreparedIncomingViewingKey::new(ivk), + ivk.ivk.clone(), height, &config, &wc, diff --git a/zingolib/src/wallet.rs b/zingolib/src/wallet.rs index bb85ac2417..4f6cba1451 100644 --- a/zingolib/src/wallet.rs +++ b/zingolib/src/wallet.rs @@ -15,7 +15,7 @@ use rand::rngs::OsRng; use rand::Rng; use sapling_crypto::note_encryption::SaplingDomain; -use sapling_crypto::SaplingIvk; +use sapling_crypto::zip32::DiversifiableFullViewingKey; use shardtree::error::ShardTreeError; use shardtree::store::memory::MemoryShardStore; use shardtree::ShardTree; @@ -40,6 +40,7 @@ use zcash_primitives::{consensus::BlockHeight, memo::Memo, transaction::componen use zingo_status::confirmation_status::ConfirmationStatus; use self::data::{WitnessTrees, COMMITMENT_TREE_LEVELS, MAX_SHARD_LEVEL}; +use self::keys::unified::Fvk as _; use self::keys::unified::{Capability, WalletCapability}; use self::traits::Recipient; use self::traits::{DomainWalletExt, SpendableNote}; @@ -309,9 +310,10 @@ impl LightWallet { ///TODO: Make this work for orchard too pub async fn decrypt_message(&self, enc: Vec) -> Result { - let sapling_ivk = SaplingIvk::try_from(&*self.wallet_capability())?; + let sapling_ivk = DiversifiableFullViewingKey::try_from(&*self.wallet_capability())? + .derive_ivk::(); - if let Ok(msg) = Message::decrypt(&enc, &sapling_ivk) { + if let Ok(msg) = Message::decrypt(&enc, &sapling_ivk.ivk) { // If decryption succeeded for this IVK, return the decrypted memo and the matched address return Ok(msg); } diff --git a/zingolib/src/wallet/keys/unified.rs b/zingolib/src/wallet/keys/unified.rs index 328c76f3b5..24f87417ca 100644 --- a/zingolib/src/wallet/keys/unified.rs +++ b/zingolib/src/wallet/keys/unified.rs @@ -1,3 +1,4 @@ +use std::marker::PhantomData; use std::sync::atomic; use std::{ collections::{HashMap, HashSet}, @@ -7,12 +8,13 @@ use std::{ use append_only_vec::AppendOnlyVec; use byteorder::{ReadBytesExt, WriteBytesExt}; -use orchard::keys::Scope; +use orchard::note_encryption::OrchardDomain; +use sapling_crypto::note_encryption::SaplingDomain; use zcash_primitives::consensus::{NetworkConstants, Parameters}; use zcash_primitives::zip339::Mnemonic; use secp256k1::SecretKey; -use zcash_address::unified::{Container, Encoding, Fvk, Ufvk}; +use zcash_address::unified::{Container, Encoding, Ufvk}; use zcash_client_backend::address::UnifiedAddress; use zcash_client_backend::keys::{Era, UnifiedSpendingKey}; use zcash_encoding::Vector; @@ -20,7 +22,7 @@ use zcash_primitives::zip32::AccountId; use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex}; use zingoconfig::ZingoConfig; -use crate::wallet::traits::ReadableWriteable; +use crate::wallet::traits::{DomainWalletExt, ReadableWriteable, Recipient}; use super::{ extended_transparent::{ExtendedPrivKey, ExtendedPubKey, KeyIndex}, @@ -164,8 +166,10 @@ impl WalletCapability { } pub fn ufvk(&self) -> Result { - let o_fvk = Fvk::Orchard(orchard::keys::FullViewingKey::try_from(self)?.to_bytes()); - let s_fvk = Fvk::Sapling( + use zcash_address::unified::Fvk as UfvkComponent; + let o_fvk = + UfvkComponent::Orchard(orchard::keys::FullViewingKey::try_from(self)?.to_bytes()); + let s_fvk = UfvkComponent::Sapling( sapling_crypto::zip32::DiversifiableFullViewingKey::try_from(self)?.to_bytes(), ); let mut t_fvk_bytes = [0u8; 65]; @@ -173,7 +177,7 @@ impl WalletCapability { if let Ok(t_ext_pk) = possible_transparent_key { t_fvk_bytes[0..32].copy_from_slice(&t_ext_pk.chain_code[..]); t_fvk_bytes[32..65].copy_from_slice(&t_ext_pk.public_key.serialize()[..]); - let t_fvk = Fvk::P2pkh(t_fvk_bytes); + let t_fvk = UfvkComponent::P2pkh(t_fvk_bytes); Ufvk::try_from_items(vec![o_fvk, s_fvk, t_fvk]).map_err(|e| e.to_string()) } else { Ufvk::try_from_items(vec![o_fvk, s_fvk]).map_err(|e| e.to_string()) @@ -206,7 +210,7 @@ impl WalletCapability { return Err(e); } }; - Some(fvk.address_at(self.addresses.len(), Scope::External)) + Some(fvk.address_at(self.addresses.len(), orchard::keys::Scope::External)) } else { None }; @@ -395,14 +399,15 @@ impl WalletCapability { // Initialize an instance with no capabilities. let mut wc = WalletCapability::default(); for fvk in ufvk.items() { + use zcash_address::unified::Fvk as UfvkComponent; match fvk { - Fvk::Orchard(key_bytes) => { + UfvkComponent::Orchard(key_bytes) => { wc.orchard = Capability::View( orchard::keys::FullViewingKey::from_bytes(&key_bytes) .ok_or("Orchard FVK deserialization failed")?, ); } - Fvk::Sapling(key_bytes) => { + UfvkComponent::Sapling(key_bytes) => { wc.sapling = Capability::View( sapling_crypto::zip32::DiversifiableFullViewingKey::read( &key_bytes[..], @@ -411,14 +416,14 @@ impl WalletCapability { .map_err(|e| e.to_string())?, ); } - Fvk::P2pkh(key_bytes) => { + UfvkComponent::P2pkh(key_bytes) => { wc.transparent = Capability::View(ExtendedPubKey { chain_code: key_bytes[0..32].to_vec(), public_key: secp256k1::PublicKey::from_slice(&key_bytes[32..65]) .map_err(|e| e.to_string())?, }); } - Fvk::Unknown { typecode, data: _ } => { + UfvkComponent::Unknown { typecode, data: _ } => { log::info!( "Unknown receiver of type {} found in Unified Viewing Key", typecode @@ -591,6 +596,49 @@ impl ReadableWriteable<()> for WalletCapability { } } +/// The external, default scope for deriving an fvk's component viewing keys +pub struct External; + +/// The internal scope, used for change only +pub struct Internal; + +mod scope { + use super::*; + use zcash_primitives::zip32::Scope as ScopeEnum; + pub trait Scope { + fn scope() -> ScopeEnum; + } + + impl Scope for External { + fn scope() -> ScopeEnum { + ScopeEnum::External + } + } + impl Scope for Internal { + fn scope() -> ScopeEnum { + ScopeEnum::Internal + } + } +} +pub struct Ivk +where + D: zcash_note_encryption::Domain, +{ + pub ivk: D::IncomingViewingKey, + __scope: PhantomData, +} + +/// This is of questionable utility, but internally-scoped ovks +/// exist, and so we represent them at the type level despite +/// having no current use for them +pub struct Ovk +where + D: zcash_note_encryption::Domain, +{ + pub ovk: D::OutgoingViewingKey, + __scope: PhantomData, +} + impl TryFrom<&WalletCapability> for super::extended_transparent::ExtendedPrivKey { type Error = String; fn try_from(wc: &WalletCapability) -> Result { @@ -663,36 +711,44 @@ impl TryFrom<&WalletCapability> for sapling_crypto::zip32::DiversifiableFullView } } -impl TryFrom<&WalletCapability> for sapling_crypto::note_encryption::PreparedIncomingViewingKey { - type Error = String; +pub trait Fvk +where + ::Note: PartialEq + Clone, + ::Recipient: Recipient, +{ + fn derive_ivk(&self) -> Ivk; + fn derive_ovk(&self) -> Ovk; +} - fn try_from(value: &WalletCapability) -> Result { - sapling_crypto::SaplingIvk::try_from(value) - .map(|k| sapling_crypto::note_encryption::PreparedIncomingViewingKey::new(&k)) +impl Fvk for orchard::keys::FullViewingKey { + fn derive_ivk(&self) -> Ivk { + Ivk { + ivk: orchard::keys::PreparedIncomingViewingKey::new(&self.to_ivk(S::scope())), + __scope: PhantomData, + } } -} -impl TryFrom<&WalletCapability> for orchard::keys::IncomingViewingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - let fvk: orchard::keys::FullViewingKey = wc.try_into()?; - Ok(fvk.to_ivk(Scope::External)) + fn derive_ovk(&self) -> Ovk { + Ovk { + ovk: self.to_ovk(S::scope()), + __scope: PhantomData, + } } } -impl TryFrom<&WalletCapability> for orchard::keys::PreparedIncomingViewingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - orchard::keys::IncomingViewingKey::try_from(wc) - .map(|k| orchard::keys::PreparedIncomingViewingKey::new(&k)) +impl Fvk for sapling_crypto::zip32::DiversifiableFullViewingKey { + fn derive_ivk(&self) -> Ivk { + Ivk { + ivk: sapling_crypto::keys::PreparedIncomingViewingKey::new(&self.to_ivk(S::scope())), + __scope: PhantomData, + } } -} -impl TryFrom<&WalletCapability> for sapling_crypto::SaplingIvk { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - let fvk: sapling_crypto::zip32::DiversifiableFullViewingKey = wc.try_into()?; - Ok(fvk.fvk().vk.ivk()) + fn derive_ovk(&self) -> Ovk { + Ovk { + ovk: self.to_ovk(S::scope()), + __scope: PhantomData, + } } } @@ -700,7 +756,7 @@ impl TryFrom<&WalletCapability> for orchard::keys::OutgoingViewingKey { type Error = String; fn try_from(wc: &WalletCapability) -> Result { let fvk: orchard::keys::FullViewingKey = wc.try_into()?; - Ok(fvk.to_ovk(Scope::External)) + Ok(fvk.to_ovk(orchard::keys::Scope::External)) } } diff --git a/zingolib/src/wallet/message.rs b/zingolib/src/wallet/message.rs index 80eb07a612..e5e07db31b 100644 --- a/zingolib/src/wallet/message.rs +++ b/zingolib/src/wallet/message.rs @@ -7,7 +7,7 @@ use sapling_crypto::{ note::ExtractedNoteCommitment, note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey, SaplingDomain}, value::NoteValue, - PaymentAddress, Rseed, SaplingIvk, + PaymentAddress, Rseed, }; use std::io::{self, ErrorKind, Read}; use zcash_note_encryption::{ @@ -104,7 +104,7 @@ impl Message { Ok(data) } - pub fn decrypt(data: &[u8], ivk: &SaplingIvk) -> io::Result { + pub fn decrypt(data: &[u8], ivk: &PreparedIncomingViewingKey) -> io::Result { if data.len() != 1 + Message::magic_word().len() + 32 + 32 + ENC_CIPHERTEXT_SIZE { return Err(io::Error::new( ErrorKind::InvalidData, @@ -182,7 +182,7 @@ impl Message { // really apply, since this note is not spendable anyway, so the rseed and the note itself // are not usable. match try_sapling_note_decryption( - &PreparedIncomingViewingKey::new(ivk), + ivk, &Unspendable { cmu_bytes, epk_bytes, @@ -215,7 +215,11 @@ pub mod tests { use super::*; - fn get_random_zaddr() -> (ExtendedSpendingKey, SaplingIvk, PaymentAddress) { + fn get_random_zaddr() -> ( + ExtendedSpendingKey, + PreparedIncomingViewingKey, + PaymentAddress, + ) { let mut rng = OsRng; let mut seed = [0u8; 32]; rng.fill(&mut seed); @@ -225,7 +229,11 @@ pub mod tests { let fvk = dfvk; let (_, addr) = fvk.default_address(); - (extsk, fvk.fvk().vk.ivk(), addr) + ( + extsk, + PreparedIncomingViewingKey::new(&fvk.fvk().vk.ivk()), + addr, + ) } #[test] diff --git a/zingolib/src/wallet/traits.rs b/zingolib/src/wallet/traits.rs index e051c13997..b8f49d307e 100644 --- a/zingolib/src/wallet/traits.rs +++ b/zingolib/src/wallet/traits.rs @@ -429,7 +429,11 @@ where const NU: NetworkUpgrade; const NAME: &'static str; - type Fvk: Clone + Send + Diversifiable; + type Fvk: Clone + + Send + + Diversifiable + + for<'a> TryFrom<&'a WalletCapability> + + super::keys::unified::Fvk; type SpendingKey: for<'a> TryFrom<&'a WalletCapability> + Clone; type CompactOutput: CompactOutput; @@ -484,8 +488,6 @@ where receiver: &Self::Recipient, ) -> Option<&'a UnifiedAddress>; fn wc_to_fvk(wc: &WalletCapability) -> Result; - fn wc_to_ivk(wc: &WalletCapability) -> Result; - fn wc_to_ovk(wc: &WalletCapability) -> Result; fn wc_to_sk(wc: &WalletCapability) -> Result; } @@ -554,12 +556,6 @@ impl DomainWalletExt for SaplingDomain { fn wc_to_fvk(wc: &WalletCapability) -> Result { Self::Fvk::try_from(wc) } - fn wc_to_ivk(wc: &WalletCapability) -> Result { - Self::IncomingViewingKey::try_from(wc) - } - fn wc_to_ovk(wc: &WalletCapability) -> Result { - Self::OutgoingViewingKey::try_from(wc) - } fn wc_to_sk(wc: &WalletCapability) -> Result { Self::SpendingKey::try_from(wc) @@ -631,12 +627,6 @@ impl DomainWalletExt for OrchardDomain { fn wc_to_fvk(wc: &WalletCapability) -> Result { Self::Fvk::try_from(wc) } - fn wc_to_ivk(wc: &WalletCapability) -> Result { - Self::IncomingViewingKey::try_from(wc) - } - fn wc_to_ovk(wc: &WalletCapability) -> Result { - Self::OutgoingViewingKey::try_from(wc) - } fn wc_to_sk(wc: &WalletCapability) -> Result { Self::SpendingKey::try_from(wc) diff --git a/zingolib/src/wallet/transaction_context.rs b/zingolib/src/wallet/transaction_context.rs index b57db4debe..0720216bca 100644 --- a/zingolib/src/wallet/transaction_context.rs +++ b/zingolib/src/wallet/transaction_context.rs @@ -40,7 +40,10 @@ pub mod decrypt_transaction { error::{ZingoLibError, ZingoLibResult}, wallet::{ data::OutgoingTxData, - keys::address_from_pubkeyhash, + keys::{ + address_from_pubkeyhash, + unified::{External, Fvk}, + }, notes::ShieldedNoteInterface, traits::{ self as zingo_traits, Bundle as _, DomainWalletExt, Recipient as _, @@ -390,6 +393,7 @@ pub mod decrypt_transaction { D::OutgoingViewingKey: std::fmt::Debug, D::Recipient: zingo_traits::Recipient, D::Memo: zingo_traits::ToBytes<512>, + D::IncomingViewingKey: Clone, { type FnGenBundle = ::Bundle; // Check if any of the nullifiers generated in this transaction are ours. We only need this for unconfirmed transactions, @@ -442,15 +446,18 @@ pub mod decrypt_transaction { }) .collect::>(); - let (Ok(ivk), Ok(ovk)) = (D::wc_to_ivk(&self.key), D::wc_to_ovk(&self.key)) else { + let Ok(fvk) = D::wc_to_fvk(&self.key) else { // skip scanning if wallet has not viewing capability return; }; + let (ivk, ovk) = (fvk.derive_ivk::(), fvk.derive_ovk::()); - let decrypt_attempts = - zcash_note_encryption::batch::try_note_decryption(&[ivk], &domain_tagged_outputs) - .into_iter() - .enumerate(); + let decrypt_attempts = zcash_note_encryption::batch::try_note_decryption( + &[ivk.ivk], + &domain_tagged_outputs, + ) + .into_iter() + .enumerate(); for (output_index, decrypt_attempt) in decrypt_attempts { let ((note, to, memo_bytes), _ivk_num) = match decrypt_attempt { Some(plaintext) => plaintext, @@ -498,7 +505,7 @@ pub mod decrypt_transaction { as zingo_traits::Bundle>::Output, >( &output.domain(status.get_height(), self.config.chain), - &ovk, + &ovk.ovk, &output, &output.value_commitment(), &output.out_ciphertext(),