diff --git a/Cargo.lock b/Cargo.lock index dd45e78eb..7bdbb548e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4315,7 +4315,9 @@ dependencies = [ "base58", "base64 0.13.1", "bip0039", + "bip32", "bls12_381", + "bs58", "build_utils", "byteorder", "bytes 0.4.12", diff --git a/libtonode-tests/tests/concrete.rs b/libtonode-tests/tests/concrete.rs index 9b0eed16e..4aa7b7102 100644 --- a/libtonode-tests/tests/concrete.rs +++ b/libtonode-tests/tests/concrete.rs @@ -15,6 +15,7 @@ use zingolib::testutils::lightclient::from_inputs; use zingolib::testutils::{build_fvk_client, increase_height_and_wait_for_client, scenarios}; use zingolib::utils::conversion::address_from_str; use zingolib::wallet::data::summaries::TransactionSummaryInterface; +use zingolib::wallet::keys::unified::UnifiedKeyStore; use zingolib::wallet::propose::ProposeSendError; use zingolib::{check_client_balances, get_base_address_macro, get_otd, validate_otds}; @@ -61,23 +62,26 @@ fn check_view_capability_bounds( balance: &PoolBalances, watch_wc: &WalletCapability, fvks: &[&Fvk], - ovk: &Fvk, - svk: &Fvk, - tvk: &Fvk, + orchard_fvk: &Fvk, + sapling_fvk: &Fvk, + transparent_fvk: &Fvk, sent_o_value: Option, sent_s_value: Option, sent_t_value: Option, notes: &JsonValue, ) { + let UnifiedKeyStore::View(ufvk) = watch_wc.unified_key_store() else { + panic!("should be viewing key!") + }; //Orchard - if !fvks.contains(&ovk) { - assert!(!watch_wc.orchard.can_view()); + if !fvks.contains(&orchard_fvk) { + assert!(ufvk.orchard().is_none()); assert_eq!(balance.orchard_balance, None); assert_eq!(balance.verified_orchard_balance, None); assert_eq!(balance.unverified_orchard_balance, None); assert_eq!(notes["unspent_orchard_notes"].members().count(), 0); } else { - assert!(watch_wc.orchard.can_view()); + assert!(ufvk.orchard().is_some()); assert_eq!(balance.orchard_balance, sent_o_value); assert_eq!(balance.verified_orchard_balance, sent_o_value); assert_eq!(balance.unverified_orchard_balance, Some(0)); @@ -86,25 +90,25 @@ fn check_view_capability_bounds( assert!((1..=2).contains(&orchard_notes_count)); } //Sapling - if !fvks.contains(&svk) { - assert!(!watch_wc.sapling.can_view()); + if !fvks.contains(&sapling_fvk) { + assert!(ufvk.sapling().is_none()); assert_eq!(balance.sapling_balance, None); assert_eq!(balance.verified_sapling_balance, None); assert_eq!(balance.unverified_sapling_balance, None); assert_eq!(notes["unspent_sapling_notes"].members().count(), 0); } else { - assert!(watch_wc.sapling.can_view()); + assert!(ufvk.sapling().is_some()); assert_eq!(balance.sapling_balance, sent_s_value); assert_eq!(balance.verified_sapling_balance, sent_s_value); assert_eq!(balance.unverified_sapling_balance, Some(0)); assert_eq!(notes["unspent_sapling_notes"].members().count(), 1); } - if !fvks.contains(&tvk) { - assert!(!watch_wc.transparent.can_view()); + if !fvks.contains(&transparent_fvk) { + assert!(ufvk.transparent().is_none()); assert_eq!(balance.transparent_balance, None); assert_eq!(notes["utxos"].members().count(), 0); } else { - assert!(watch_wc.transparent.can_view()); + assert!(ufvk.transparent().is_some()); assert_eq!(balance.transparent_balance, sent_t_value); assert_eq!(notes["utxos"].members().count(), 1); } diff --git a/zingolib/Cargo.toml b/zingolib/Cargo.toml index 2fc13a305..4af492244 100644 --- a/zingolib/Cargo.toml +++ b/zingolib/Cargo.toml @@ -36,6 +36,8 @@ zcash_proofs = { workspace = true } zip32.workspace = true bip0039.workspace = true +bip32 = { version = "0.5", default-features = false, features = ["secp256k1-ffi"] } +bs58 = { version = "0.5", features = ["check"] } append-only-vec = { workspace = true } base58 = { workspace = true } diff --git a/zingolib/src/blaze/trial_decryptions.rs b/zingolib/src/blaze/trial_decryptions.rs index c66675426..13b3b61f0 100644 --- a/zingolib/src/blaze/trial_decryptions.rs +++ b/zingolib/src/blaze/trial_decryptions.rs @@ -89,10 +89,12 @@ impl TrialDecryptions { let mut workers = FuturesUnordered::new(); let mut cbs = vec![]; - 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) + let sapling_ivk = sapling_crypto::zip32::DiversifiableFullViewingKey::try_from( + wc.unified_key_store(), + ) + .ok() + .map(|key| key.derive_ivk()); + let orchard_ivk = orchard::keys::FullViewingKey::try_from(wc.unified_key_store()) .ok() .map(|key| key.derive_ivk()); @@ -315,7 +317,7 @@ impl TrialDecryptions { let config = config.clone(); workers.push(tokio::spawn(async move { - let Ok(fvk) = D::wc_to_fvk(&wc) else { + let Ok(fvk) = D::unified_key_store_to_fvk(wc.unified_key_store()) else { // skip any scanning if the wallet doesn't have viewing capability return Ok::<_, String>(()); }; @@ -450,7 +452,7 @@ where transaction_id, Some(output_index), position + i as u64, - &D::wc_to_fvk(wc).unwrap(), + &D::unified_key_store_to_fvk(wc.unified_key_store()).unwrap(), )?; } nodes_retention.push((node, retention)); diff --git a/zingolib/src/commands.rs b/zingolib/src/commands.rs index 6f0998a33..6f87db22f 100644 --- a/zingolib/src/commands.rs +++ b/zingolib/src/commands.rs @@ -2,6 +2,7 @@ //! upgrade-or-replace use crate::data::proposal; +use crate::wallet::keys::unified::UnifiedKeyStore; use crate::wallet::MemoDownloadOption; use crate::{lightclient::LightClient, wallet}; use indoc::indoc; @@ -13,7 +14,7 @@ use std::str::FromStr; use tokio::runtime::Runtime; use zcash_address::unified::{Container, Encoding, Ufvk}; use zcash_keys::address::Address; -use zcash_primitives::consensus::Parameters; +use zcash_keys::keys::UnifiedFullViewingKey; use zcash_primitives::transaction::components::amount::NonNegativeAmount; use zcash_primitives::transaction::fees::zip317::MINIMUM_FEE; @@ -134,16 +135,36 @@ impl Command for WalletKindCommand { fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { RT.block_on(async move { if lightclient.do_seed_phrase().await.is_ok() { - object! {"kind" => "Seeded"}.pretty(4) - } else { - let capability = lightclient.wallet.wallet_capability(); - object! { - "kind" => "Loaded from key", - "transparent" => capability.transparent.kind_str(), - "sapling" => capability.sapling.kind_str(), - "orchard" => capability.orchard.kind_str(), + object! {"kind" => "Loaded from seed phrase", + "transparent" => true, + "sapling" => true, + "orchard" => true, } .pretty(4) + } else { + match lightclient.wallet.wallet_capability().unified_key_store() { + UnifiedKeyStore::Spend(_) => object! { + "kind" => "Loaded from unified spending key", + "transparent" => true, + "sapling" => true, + "orchard" => true, + } + .pretty(4), + UnifiedKeyStore::View(ufvk) => object! { + "kind" => "Loaded from unified full viewing key", + "transparent" => ufvk.transparent().is_some(), + "sapling" => ufvk.sapling().is_some(), + "orchard" => ufvk.orchard().is_some(), + } + .pretty(4), + UnifiedKeyStore::Empty => object! { + "kind" => "No keys found", + "transparent" => false, + "sapling" => false, + "orchard" => false, + } + .pretty(4), + } } }) } @@ -690,18 +711,20 @@ impl Command for ExportUfvkCommand { } fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - let ufvk_res = lightclient.wallet.transaction_context.key.ufvk(); - match ufvk_res { - Ok(ufvk) => { - use zcash_address::unified::Encoding as _; - object! { - "ufvk" => ufvk.encode(&lightclient.config().chain.network_type()), - "birthday" => RT.block_on(lightclient.wallet.get_birthday()) - } - .pretty(2) - } - Err(e) => format!("Error: {e}"), + let ufvk: UnifiedFullViewingKey = match lightclient + .wallet + .wallet_capability() + .unified_key_store() + .try_into() + { + Ok(ufvk) => ufvk, + Err(e) => return e.to_string(), + }; + object! { + "ufvk" => ufvk.encode(&lightclient.config().chain), + "birthday" => RT.block_on(lightclient.wallet.get_birthday()) } + .pretty(2) } } diff --git a/zingolib/src/lightclient.rs b/zingolib/src/lightclient.rs index a5bee2e76..28efa5aeb 100644 --- a/zingolib/src/lightclient.rs +++ b/zingolib/src/lightclient.rs @@ -474,7 +474,7 @@ impl LightClient { let new_address = self .wallet .wallet_capability() - .new_address(desired_receivers)?; + .new_address(desired_receivers, false)?; // self.save_internal_rust().await?; diff --git a/zingolib/src/testutils.rs b/zingolib/src/testutils.rs index 354052d20..11bb2ab15 100644 --- a/zingolib/src/testutils.rs +++ b/zingolib/src/testutils.rs @@ -21,7 +21,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::time::Duration; use tokio::task::JoinHandle; -use zcash_address::unified::{Fvk, Ufvk}; +use zcash_address::unified::Fvk; use crate::config::ZingoConfig; use crate::lightclient::LightClient; @@ -46,31 +46,27 @@ pub mod regtest; /// TODO: Add Doc Comment Here! pub fn build_fvks_from_wallet_capability(wallet_capability: &WalletCapability) -> [Fvk; 3] { - let o_fvk = Fvk::Orchard( - orchard::keys::FullViewingKey::try_from(wallet_capability) - .unwrap() - .to_bytes(), - ); - let s_fvk = Fvk::Sapling( - zcash_client_backend::keys::sapling::DiversifiableFullViewingKey::try_from( - wallet_capability, - ) - .unwrap() - .to_bytes(), - ); - let mut t_fvk_bytes = [0u8; 65]; - let t_ext_pk: crate::wallet::keys::extended_transparent::ExtendedPubKey = - (wallet_capability).try_into().unwrap(); - 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); - [o_fvk, s_fvk, t_fvk] + let orchard_vk: orchard::keys::FullViewingKey = + wallet_capability.unified_key_store().try_into().unwrap(); + let sapling_vk: sapling_crypto::zip32::DiversifiableFullViewingKey = + wallet_capability.unified_key_store().try_into().unwrap(); + let transparent_vk: zcash_primitives::legacy::keys::AccountPubKey = + wallet_capability.unified_key_store().try_into().unwrap(); + + let mut transparent_vk_bytes = [0u8; 65]; + transparent_vk_bytes.copy_from_slice(&transparent_vk.serialize()); + + [ + Fvk::Orchard(orchard_vk.to_bytes()), + Fvk::Sapling(sapling_vk.to_bytes()), + Fvk::P2pkh(transparent_vk_bytes), + ] } /// TODO: Add Doc Comment Here! pub async fn build_fvk_client(fvks: &[&Fvk], zingoconfig: &ZingoConfig) -> LightClient { let ufvk = zcash_address::unified::Encoding::encode( - &::try_from_items( + &::try_from_items( fvks.iter().copied().cloned().collect(), ) .unwrap(), @@ -80,14 +76,6 @@ pub async fn build_fvk_client(fvks: &[&Fvk], zingoconfig: &ZingoConfig) -> Light .await .unwrap() } - -/// Converts a Lightclient with spending capability to a Lightclient with only viewing capability -pub async fn sk_client_to_fvk_client(client: &LightClient) -> LightClient { - let [o_fvk, s_fvk, t_fvk] = - build_fvks_from_wallet_capability(&client.wallet.wallet_capability().clone()); - build_fvk_client(&[&o_fvk, &s_fvk, &t_fvk], client.config()).await -} - async fn get_synced_wallet_height(client: &LightClient) -> Result { client.do_sync(true).await?; Ok(client diff --git a/zingolib/src/wallet.rs b/zingolib/src/wallet.rs index 351e1a940..ac1c0e776 100644 --- a/zingolib/src/wallet.rs +++ b/zingolib/src/wallet.rs @@ -3,7 +3,9 @@ //! TODO: Add Mod Description Here use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use error::KeyError; use getset::{Getters, MutGetters}; +use zcash_keys::keys::UnifiedFullViewingKey; #[cfg(feature = "sync")] use zcash_primitives::consensus::BlockHeight; use zcash_primitives::memo::Memo; @@ -12,8 +14,6 @@ use log::{info, warn}; use rand::rngs::OsRng; use rand::Rng; -use sapling_crypto::zip32::DiversifiableFullViewingKey; - #[cfg(feature = "sync")] use zingo_sync::{ primitives::{NullifierMap, SyncState, WalletBlock}, @@ -35,7 +35,6 @@ use crate::config::ZingoConfig; use zcash_client_backend::proto::service::TreeState; use zcash_encoding::Optional; -use self::keys::unified::Fvk as _; use self::keys::unified::WalletCapability; use self::{ @@ -268,10 +267,18 @@ impl LightWallet { ///TODO: Make this work for orchard too pub async fn decrypt_message(&self, enc: Vec) -> Result { - let sapling_ivk = DiversifiableFullViewingKey::try_from(&*self.wallet_capability())? - .derive_ivk::(); + let ufvk: UnifiedFullViewingKey = + match self.wallet_capability().unified_key_store().try_into() { + Ok(ufvk) => ufvk, + Err(e) => return Err(e.to_string()), + }; + let sapling_ivk = if let Some(ivk) = ufvk.sapling() { + ivk.to_external_ivk().prepare() + } else { + return Err(KeyError::NoViewCapability.to_string()); + }; - if let Ok(msg) = Message::decrypt(&enc, &sapling_ivk.ivk) { + if let Ok(msg) = Message::decrypt(&enc, &sapling_ivk) { // If decryption succeeded for this IVK, return the decrypted memo and the matched address return Ok(msg); } @@ -367,13 +374,13 @@ impl LightWallet { } }; - if let Err(e) = wc.new_address(wc.can_view()) { + if let Err(e) = wc.new_address(wc.can_view(), false) { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("could not create initial address: {e}"), )); }; - let transaction_metadata_set = if wc.can_spend_from_all_pools() { + let transaction_metadata_set = if wc.unified_key_store().is_spending_key() { Arc::new(RwLock::new(TxMap::new_with_witness_trees( wc.transparent_child_addresses().clone(), ))) diff --git a/zingolib/src/wallet/describe.rs b/zingolib/src/wallet/describe.rs index 2ff054a8b..3fc669f88 100644 --- a/zingolib/src/wallet/describe.rs +++ b/zingolib/src/wallet/describe.rs @@ -22,7 +22,7 @@ use crate::wallet::notes::ShieldedNoteInterface; use crate::wallet::traits::Diversifiable as _; use crate::wallet::error::BalanceError; -use crate::wallet::keys::unified::{Capability, WalletCapability}; +use crate::wallet::keys::unified::WalletCapability; use crate::wallet::notes::TransparentOutput; use crate::wallet::traits::DomainWalletExt; use crate::wallet::traits::Recipient; @@ -30,6 +30,8 @@ use crate::wallet::traits::Recipient; use crate::wallet::LightWallet; use crate::wallet::{data::BlockData, tx_map::TxMap}; +use super::keys::unified::UnifiedKeyStore; + impl LightWallet { /// returns Some seed phrase for the wallet. /// if wallet does not have a seed phrase, returns None @@ -53,17 +55,17 @@ impl LightWallet { ::Recipient: Recipient, { // For the moment we encode lack of view capability as None - match D::SHIELDED_PROTOCOL { - ShieldedProtocol::Sapling => { - if !self.wallet_capability().sapling.can_view() { - return None; + match self.wallet_capability().unified_key_store() { + UnifiedKeyStore::Spend(_) => (), + UnifiedKeyStore::View(ufvk) => match D::SHIELDED_PROTOCOL { + ShieldedProtocol::Sapling => { + ufvk.sapling()?; } - } - ShieldedProtocol::Orchard => { - if !self.wallet_capability().orchard.can_view() { - return None; + ShieldedProtocol::Orchard => { + ufvk.orchard()?; } - } + }, + UnifiedKeyStore::Empty => return None, } Some( self.transaction_context @@ -98,7 +100,7 @@ impl LightWallet { ::Recipient: Recipient, ::Note: PartialEq + Clone, { - if let Capability::Spend(_) = self.wallet_capability().orchard { + if let UnifiedKeyStore::Spend(_) = self.wallet_capability().unified_key_store() { self.confirmed_balance::().await } else { None @@ -106,18 +108,21 @@ impl LightWallet { } /// Sums the transparent balance (unspent) pub async fn get_transparent_balance(&self) -> Option { - if self.wallet_capability().transparent.can_view() { - Some( - self.get_utxos() - .await - .iter() - .filter(|transparent_output| transparent_output.is_unspent()) - .map(|utxo| utxo.value) - .sum::(), - ) - } else { - None + match self.wallet_capability().unified_key_store() { + UnifiedKeyStore::Spend(_) => (), + UnifiedKeyStore::View(ufvk) => { + ufvk.transparent()?; + } + UnifiedKeyStore::Empty => return None, } + Some( + self.get_utxos() + .await + .iter() + .filter(|transparent_output| transparent_output.is_unspent()) + .map(|utxo| utxo.value) + .sum::(), + ) } /// On chain balance @@ -190,7 +195,7 @@ impl LightWallet { ::Recipient: Recipient, ::Note: PartialEq + Clone, { - D::wc_to_fvk(wallet_capability).expect("to get fvk from wc") + D::unified_key_store_to_fvk(wallet_capability.unified_key_store()).expect("to get fvk from the unified key store") .diversified_address(*note.diversifier()) .and_then(|address| { D::ua_from_contained_receiver(wallet_capability, &address) diff --git a/zingolib/src/wallet/disk.rs b/zingolib/src/wallet/disk.rs index b10bb8f0f..edd2c5469 100644 --- a/zingolib/src/wallet/disk.rs +++ b/zingolib/src/wallet/disk.rs @@ -2,6 +2,8 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use log::{error, info}; +use zcash_keys::keys::UnifiedSpendingKey; +use zip32::AccountId; use std::{ io::{self, Error, ErrorKind, Read, Write}, @@ -19,13 +21,13 @@ use zcash_encoding::{Optional, Vector}; use zcash_primitives::consensus::BlockHeight; -use crate::config::ZingoConfig; +use crate::{config::ZingoConfig, wallet::keys::unified::UnifiedKeyStore}; use crate::wallet::traits::ReadableWriteable; use crate::wallet::WalletOptions; use crate::wallet::{utils, SendProgress}; -use super::keys::unified::{Capability, WalletCapability}; +use super::keys::unified::WalletCapability; use super::LightWallet; use super::{ @@ -35,15 +37,12 @@ use super::{ }; impl LightWallet { - /// Changes in version 27: - /// - The wallet does not have to have a mnemonic. - /// Absence of mnemonic is represented by an empty byte vector in v27. - /// v26 serialized wallet is always loaded with `Some(mnemonic)`. - /// - The wallet capabilities can be restricted from spending to view-only or none. - /// We introduce `Capability` type represent different capability types in v27. - /// v26 serialized wallet is always loaded with `Capability::Spend(sk)`. + /// Changes in version 29: + /// - Replaced `Capability` with `UnifiedKeyStore` + /// - Implemented read/write for `UnifiedSpendingKey` + /// - Implemented read/write for `UnifiedFullViewingKey` pub const fn serialized_version() -> u64 { - 28 + 29 } /// TODO: Add Doc Comment Here! @@ -52,7 +51,9 @@ impl LightWallet { writer.write_u64::(Self::serialized_version())?; // Write all the keys - self.transaction_context.key.write(&mut writer)?; + self.transaction_context + .key + .write(&mut writer, self.transaction_context.config.chain)?; Vector::write(&mut writer, &self.blocks.read().await, |w, b| b.write(w))?; @@ -123,23 +124,7 @@ impl LightWallet { } info!("Reading wallet version {}", external_version); - let wallet_capability = WalletCapability::read(&mut reader, ())?; - info!("Keys in this wallet:"); - match &wallet_capability.orchard { - Capability::None => (), - Capability::View(_) => info!(" - Orchard Full Viewing Key"), - Capability::Spend(_) => info!(" - Orchard Spending Key"), - }; - match &wallet_capability.sapling { - Capability::None => (), - Capability::View(_) => info!(" - Sapling Extended Full Viewing Key"), - Capability::Spend(_) => info!(" - Sapling Extended Spending Key"), - }; - match &wallet_capability.transparent { - Capability::None => (), - Capability::View(_) => info!(" - transparent extended public key"), - Capability::Spend(_) => info!(" - transparent extended private key"), - }; + let mut wallet_capability = WalletCapability::read(&mut reader, config.chain)?; let mut blocks = Vector::read(&mut reader, |r| BlockData::read(r))?; if external_version <= 14 { @@ -200,20 +185,6 @@ impl LightWallet { WalletZecPriceInfo::read(&mut reader)? }; - // this initialization combines two types of data - let transaction_context = TransactionContext::new( - // Config data could be used differently based on the circumstances - // hardcoded? - // entered at init by user? - // stored on disk in a separate location and connected by a descendant library (such as zingo-mobile)? - config, - // Saveable Arc data - // - Arcs allow access between threads. - // - This data is loaded from the wallet file and but needs multithreaded access during sync. - Arc::new(wallet_capability), - Arc::new(RwLock::new(transactions)), - ); - let _orchard_anchor_height_pairs = if external_version == 25 { Vector::read(&mut reader, |r| { let mut anchor_bytes = [0; 32]; @@ -245,6 +216,72 @@ impl LightWallet { None }; + // Derive unified spending key from seed and overide temporary USK if wallet is pre v29. + // + // UnifiedSpendingKey is initially incomplete for old wallet versions. + // This is due to the legacy transparent extended private key (ExtendedPrivKey) not containing all information required for BIP0032. + // There is also the issue that the legacy transparent private key is derived an extra level to the external scope. + if external_version < 29 { + if let Some(mnemonic) = mnemonic.as_ref() { + wallet_capability.set_unified_key_store(UnifiedKeyStore::Spend(Box::new( + UnifiedSpendingKey::from_seed( + &config.chain, + &mnemonic.0.to_seed(""), + AccountId::ZERO, + ) + .map_err(|e| { + Error::new( + ErrorKind::InvalidData, + format!( + "Failed to derive unified spending key from stored seed bytes. {}", + e + ), + ) + })?, + ))); + } else if let UnifiedKeyStore::Spend(_) = wallet_capability.unified_key_store() { + return Err(io::Error::new( + ErrorKind::Other, + "loading from legacy spending keys with no seed phrase to recover", + )); + } + } + + info!("Keys in this wallet:"); + match wallet_capability.unified_key_store() { + UnifiedKeyStore::Spend(_) => { + info!(" - orchard spending key"); + info!(" - sapling extended spending key"); + info!(" - transparent extended private key"); + } + UnifiedKeyStore::View(ufvk) => { + if ufvk.orchard().is_some() { + info!(" - orchard full viewing key"); + } + if ufvk.sapling().is_some() { + info!(" - sapling diversifiable full viewing key"); + } + if ufvk.transparent().is_some() { + info!(" - transparent extended public key"); + } + } + UnifiedKeyStore::Empty => info!(" - no keys found"), + } + + // this initialization combines two types of data + let transaction_context = TransactionContext::new( + // Config data could be used differently based on the circumstances + // hardcoded? + // entered at init by user? + // stored on disk in a separate location and connected by a descendant library (such as zingo-mobile)? + config, + // Saveable Arc data + // - Arcs allow access between threads. + // - This data is loaded from the wallet file and but needs multithreaded access during sync. + Arc::new(wallet_capability), + Arc::new(RwLock::new(transactions)), + ); + let lw = Self { blocks: Arc::new(RwLock::new(blocks)), mnemonic, diff --git a/zingolib/src/wallet/disk/testing.rs b/zingolib/src/wallet/disk/testing.rs index a78effc9d..536c63522 100644 --- a/zingolib/src/wallet/disk/testing.rs +++ b/zingolib/src/wallet/disk/testing.rs @@ -2,6 +2,9 @@ //! do not compile test-elevation feature for production. use bip0039::Mnemonic; +use zcash_keys::keys::{Era, UnifiedSpendingKey}; + +use crate::wallet::keys::unified::UnifiedKeyStore; use super::LightWallet; @@ -77,37 +80,15 @@ pub async fn assert_wallet_capability_matches_seed( .unwrap(); let wc = wallet.wallet_capability(); - // We don't want the WalletCapability to impl. `Eq` (because it stores secret keys) - // so we have to compare each component instead - - // Compare Orchard - let crate::wallet::keys::unified::Capability::Spend(orchard_sk) = &wc.orchard else { - panic!("Expected Orchard Spending Key"); - }; - assert_eq!( - orchard_sk.to_bytes(), - orchard::keys::SpendingKey::try_from(&expected_wc) - .unwrap() - .to_bytes() - ); - - // Compare Sapling - let crate::wallet::keys::unified::Capability::Spend(sapling_sk) = &wc.sapling else { - panic!("Expected Sapling Spending Key"); - }; - assert_eq!( - sapling_sk, - &zcash_client_backend::keys::sapling::ExtendedSpendingKey::try_from(&expected_wc).unwrap() - ); - - // Compare transparent - let crate::wallet::keys::unified::Capability::Spend(transparent_sk) = &wc.transparent else { - panic!("Expected transparent extended private key"); + // Compare USK + let UnifiedKeyStore::Spend(usk) = &wc.unified_key_store() else { + panic!("Expected Unified Spending Key"); }; assert_eq!( - transparent_sk, - &crate::wallet::keys::extended_transparent::ExtendedPrivKey::try_from(&expected_wc) + usk.to_bytes(Era::Orchard), + UnifiedSpendingKey::try_from(expected_wc.unified_key_store()) .unwrap() + .to_bytes(Era::Orchard) ); } diff --git a/zingolib/src/wallet/disk/testing/tests.rs b/zingolib/src/wallet/disk/testing/tests.rs index 43b402030..a1ca35ded 100644 --- a/zingolib/src/wallet/disk/testing/tests.rs +++ b/zingolib/src/wallet/disk/testing/tests.rs @@ -1,9 +1,11 @@ use bip0039::Mnemonic; -use zcash_address::unified::Encoding; + use zcash_client_backend::PoolType; use zcash_client_backend::ShieldedProtocol; +use zcash_keys::keys::Era; use crate::lightclient::LightClient; +use crate::wallet::keys::unified::UnifiedKeyStore; use super::super::LightWallet; use super::assert_wallet_capability_matches_seed; @@ -36,7 +38,7 @@ impl ExampleWalletNetwork { async fn load_example_wallet_with_verification(&self) -> LightWallet { let wallet = self.load_example_wallet().await; assert_wallet_capability_matches_seed(&wallet, self.example_wallet_base()).await; - for pool in vec![ + for pool in [ PoolType::Transparent, PoolType::Shielded(ShieldedProtocol::Sapling), PoolType::Shielded(ShieldedProtocol::Orchard), @@ -225,11 +227,7 @@ async fn loaded_wallet_assert( // todo: proptest enum #[tokio::test] async fn reload_wallet_from_buffer() { - use zcash_primitives::consensus::Parameters; - use crate::testvectors::seeds::CHIMNEY_BETTER_SEED; - use crate::wallet::disk::Capability; - use crate::wallet::keys::extended_transparent::ExtendedPrivKey; use crate::wallet::WalletBase; use crate::wallet::WalletCapability; @@ -263,30 +261,22 @@ async fn reload_wallet_from_buffer() { .unwrap(); let wc = wallet.wallet_capability(); - let Capability::Spend(orchard_sk) = &wc.orchard else { - panic!("Expected Orchard Spending Key"); + let UnifiedKeyStore::Spend(usk) = wc.unified_key_store() else { + panic!("should be spending key!") }; - assert_eq!( - orchard_sk.to_bytes(), - orchard::keys::SpendingKey::try_from(&expected_wc) - .unwrap() - .to_bytes() - ); - - let Capability::Spend(sapling_sk) = &wc.sapling else { - panic!("Expected Sapling Spending Key"); + let UnifiedKeyStore::Spend(expected_usk) = expected_wc.unified_key_store() else { + panic!("should be spending key!") }; + assert_eq!( - sapling_sk, - &zcash_client_backend::keys::sapling::ExtendedSpendingKey::try_from(&expected_wc).unwrap() + usk.to_bytes(Era::Orchard), + expected_usk.to_bytes(Era::Orchard) ); - - let Capability::Spend(transparent_sk) = &wc.transparent else { - panic!("Expected transparent extended private key"); - }; + assert_eq!(usk.orchard().to_bytes(), expected_usk.orchard().to_bytes()); + assert_eq!(usk.sapling().to_bytes(), expected_usk.sapling().to_bytes()); assert_eq!( - transparent_sk, - &ExtendedPrivKey::try_from(&expected_wc).unwrap() + usk.transparent().to_bytes(), + expected_usk.transparent().to_bytes() ); assert_eq!(wc.addresses().len(), 3); @@ -296,8 +286,8 @@ async fn reload_wallet_from_buffer() { assert!(addr.transparent().is_some()); } - let ufvk = wc.ufvk().unwrap(); - let ufvk_string = ufvk.encode(&wallet.transaction_context.config.chain.network_type()); + let ufvk = usk.to_unified_full_viewing_key(); + let ufvk_string = ufvk.encode(&wallet.transaction_context.config.chain); let ufvk_base = WalletBase::Ufvk(ufvk_string.clone()); let view_wallet = LightWallet::new( wallet.transaction_context.config.clone(), @@ -306,9 +296,11 @@ async fn reload_wallet_from_buffer() { ) .unwrap(); let v_wc = view_wallet.wallet_capability(); - let vv = v_wc.ufvk().unwrap(); - let vv_string = vv.encode(&wallet.transaction_context.config.chain.network_type()); - assert_eq!(ufvk_string, vv_string); + let UnifiedKeyStore::View(v_ufvk) = v_wc.unified_key_store() else { + panic!("should be viewing key!"); + }; + let v_ufvk_string = v_ufvk.encode(&wallet.transaction_context.config.chain); + assert_eq!(ufvk_string, v_ufvk_string); let client = LightClient::create_from_wallet_async(wallet).await.unwrap(); let balance = client.do_balance().await; diff --git a/zingolib/src/wallet/error.rs b/zingolib/src/wallet/error.rs index 921763745..8611c28c7 100644 --- a/zingolib/src/wallet/error.rs +++ b/zingolib/src/wallet/error.rs @@ -41,3 +41,40 @@ pub enum BalanceError { #[error("conversion failed. {0}")] ConversionFailed(#[from] crate::utils::error::ConversionError), } + +/// Errors associated with balance key derivation +#[derive(Debug, Error)] +pub enum KeyError { + /// Error asociated with standard IO + #[error("{0}")] + IoError(#[from] std::io::Error), + /// Invalid account ID + #[error("Account ID should be at most 31 bits")] + InvalidAccountId(#[from] zip32::TryFromIntError), + /// Key derivation failed + // TODO: add std::Error to zcash_keys::keys::DerivationError in LRZ fork and add thiserror #[from] macro + #[error("Key derivation failed")] + KeyDerivationError, + /// Key decoding failed + // TODO: add std::Error to zcash_keys::keys::DecodingError in LRZ fork and add thiserror #[from] macro + #[error("Key decoding failed")] + KeyDecodingError, + /// Key parsing failed + #[error("Key parsing failed. {0}")] + KeyParseError(#[from] zcash_address::unified::ParseError), + /// No spend capability + #[error("No spend capability")] + NoSpendCapability, + /// No view capability + #[error("No view capability")] + NoViewCapability, + /// Invalid non-hardened child indexes + #[error("Outside range of non-hardened child indexes")] + InvalidNonHardenedChildIndex, + /// Network mismatch + #[error("Decoded unified full viewing key does not match current network")] + NetworkMismatch, + /// Invalid format + #[error("Viewing keys must be imported in the unified format")] + InvalidFormat, +} diff --git a/zingolib/src/wallet/keys.rs b/zingolib/src/wallet/keys.rs index dd53cfac6..66667ec35 100644 --- a/zingolib/src/wallet/keys.rs +++ b/zingolib/src/wallet/keys.rs @@ -13,7 +13,7 @@ use zcash_primitives::{ consensus::NetworkConstants, legacy::TransparentAddress, zip32::ChildIndex, }; -pub mod extended_transparent; +pub mod legacy; pub mod unified; /// Sha256(Sha256(value)) diff --git a/zingolib/src/wallet/keys/legacy.rs b/zingolib/src/wallet/keys/legacy.rs new file mode 100644 index 000000000..662cce64d --- /dev/null +++ b/zingolib/src/wallet/keys/legacy.rs @@ -0,0 +1,198 @@ +//! Module for legacy code associated with wallet keys required for backward-compatility with old wallet versions + +use std::io::{self, Read, Write}; + +use bip32::ExtendedPublicKey; +use byteorder::{ReadBytesExt, WriteBytesExt}; +use bytes::LittleEndian; +use zcash_address::unified::Typecode; +use zcash_encoding::CompactSize; +use zcash_keys::keys::{Era, UnifiedFullViewingKey, UnifiedSpendingKey}; +use zcash_primitives::legacy::{ + keys::{AccountPubKey, NonHardenedChildIndex}, + TransparentAddress, +}; + +use crate::wallet::{error::KeyError, traits::ReadableWriteable}; + +use super::unified::{KEY_TYPE_EMPTY, KEY_TYPE_SPEND, KEY_TYPE_VIEW}; + +pub mod extended_transparent; + +/// TODO: Add Doc Comment Here! +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Capability { + /// TODO: Add Doc Comment Here! + None, + /// TODO: Add Doc Comment Here! + View(ViewingKeyType), + /// TODO: Add Doc Comment Here! + Spend(SpendKeyType), +} + +impl ReadableWriteable<(), ()> for Capability +where + V: ReadableWriteable<(), ()>, + S: ReadableWriteable<(), ()>, +{ + const VERSION: u8 = 1; + fn read(mut reader: R, _input: ()) -> io::Result { + let _version = Self::get_version(&mut reader)?; + let capability_type = reader.read_u8()?; + Ok(match capability_type { + KEY_TYPE_EMPTY => Capability::None, + KEY_TYPE_VIEW => Capability::View(V::read(&mut reader, ())?), + KEY_TYPE_SPEND => Capability::Spend(S::read(&mut reader, ())?), + x => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Unknown wallet Capability type: {}", x), + )) + } + }) + } + + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { + writer.write_u8(Self::VERSION)?; + match self { + Capability::None => writer.write_u8(KEY_TYPE_EMPTY), + Capability::View(vk) => { + writer.write_u8(KEY_TYPE_VIEW)?; + vk.write(&mut writer, ()) + } + Capability::Spend(sk) => { + writer.write_u8(KEY_TYPE_SPEND)?; + sk.write(&mut writer, ()) + } + } + } +} + +pub(crate) fn legacy_fvks_to_ufvk( + orchard_fvk: Option<&orchard::keys::FullViewingKey>, + sapling_fvk: Option<&sapling_crypto::zip32::DiversifiableFullViewingKey>, + transparent_fvk: Option<&extended_transparent::ExtendedPubKey>, + parameters: &P, +) -> Result { + use zcash_address::unified::Encoding; + + let mut fvks = Vec::new(); + if let Some(fvk) = orchard_fvk { + fvks.push(zcash_address::unified::Fvk::Orchard(fvk.to_bytes())); + } + if let Some(fvk) = sapling_fvk { + fvks.push(zcash_address::unified::Fvk::Sapling(fvk.to_bytes())); + } + if let Some(fvk) = transparent_fvk { + let mut fvk_bytes = [0u8; 65]; + fvk_bytes[0..32].copy_from_slice(&fvk.chain_code[..]); + fvk_bytes[32..65].copy_from_slice(&fvk.public_key.serialize()[..]); + fvks.push(zcash_address::unified::Fvk::P2pkh(fvk_bytes)); + } + + let ufvk = zcash_address::unified::Ufvk::try_from_items(fvks)?; + + UnifiedFullViewingKey::decode(parameters, &ufvk.encode(¶meters.network_type())) + .map_err(|_| KeyError::KeyDecodingError) +} + +pub(crate) fn legacy_sks_to_usk( + orchard_key: &orchard::keys::SpendingKey, + sapling_key: &sapling_crypto::zip32::ExtendedSpendingKey, + transparent_key: &extended_transparent::ExtendedPrivKey, +) -> Result { + let mut usk_bytes = vec![]; + + // hard-coded Orchard Era ID due to `id()` being a private fn + usk_bytes.write_u32::(0xc2d6_d0b4)?; + + CompactSize::write( + &mut usk_bytes, + usize::try_from(Typecode::Orchard).expect("typecode to usize should not fail"), + )?; + let orchard_key_bytes = orchard_key.to_bytes(); + CompactSize::write(&mut usk_bytes, orchard_key_bytes.len())?; + usk_bytes.write_all(orchard_key_bytes)?; + + CompactSize::write( + &mut usk_bytes, + usize::try_from(Typecode::Sapling).expect("typecode to usize should not fail"), + )?; + let sapling_key_bytes = sapling_key.to_bytes(); + CompactSize::write(&mut usk_bytes, sapling_key_bytes.len())?; + usk_bytes.write_all(&sapling_key_bytes)?; + + // the following code performs the same operations for calling `to_bytes()` on an AccountPrivKey in LRZ + let prefix = bip32::Prefix::XPRV; + let mut chain_code = [0u8; 32]; + chain_code.copy_from_slice(&transparent_key.chain_code); + let attrs = bip32::ExtendedKeyAttrs { + depth: 4, + parent_fingerprint: [0xff, 0xff, 0xff, 0xff], + child_number: bip32::ChildNumber::new(0, true).expect("correct"), + chain_code, + }; + // Add leading `0` byte + let mut key_bytes = [0u8; 33]; + key_bytes[1..].copy_from_slice(transparent_key.private_key.as_ref()); + + let extended_key = bip32::ExtendedKey { + prefix, + attrs, + key_bytes, + }; + + let xprv_encoded = extended_key.to_string(); + let account_tkey_bytes = bs58::decode(xprv_encoded) + .with_check(None) + .into_vec() + .expect("correct") + .split_off(bip32::Prefix::LENGTH); + + CompactSize::write( + &mut usk_bytes, + usize::try_from(Typecode::P2pkh).expect("typecode to usize should not fail"), + )?; + CompactSize::write(&mut usk_bytes, account_tkey_bytes.len())?; + usk_bytes.write_all(&account_tkey_bytes)?; + + UnifiedSpendingKey::from_bytes(Era::Orchard, &usk_bytes).map_err(|_| KeyError::KeyDecodingError) +} + +/// Generates a transparent address from legacy key +/// +/// Legacy key is a key used ONLY during wallet load for wallet versions <29 +/// This legacy key is already derived to the external scope so should only derive a child at the `address_index` +/// and use this child to derive the transparent address +#[allow(deprecated)] +pub(crate) fn generate_transparent_address_from_legacy_key( + external_pubkey: &AccountPubKey, + address_index: NonHardenedChildIndex, +) -> Result { + let external_pubkey_bytes = external_pubkey.serialize(); + + let mut chain_code = [0u8; 32]; + chain_code.copy_from_slice(&external_pubkey_bytes[..32]); + let public_key = secp256k1::PublicKey::from_slice(&external_pubkey_bytes[32..]) + .map_err(|e| e.to_string())?; + + let extended_pubkey = ExtendedPublicKey::new( + public_key, + bip32::ExtendedKeyAttrs { + depth: 4, + parent_fingerprint: [0xff, 0xff, 0xff, 0xff], + child_number: bip32::ChildNumber::new(0, true) + .expect("hard-coded index of 0 is not larger than the hardened bit"), + chain_code, + }, + ); + + // address generation copied from IncomingViewingKey::derive_address in LRZ + let child_key = extended_pubkey + .derive_child(address_index.into()) + .map_err(|e| e.to_string())?; + Ok(zcash_primitives::legacy::keys::pubkey_to_address( + child_key.public_key(), + )) +} diff --git a/zingolib/src/wallet/keys/extended_transparent.rs b/zingolib/src/wallet/keys/legacy/extended_transparent.rs similarity index 92% rename from zingolib/src/wallet/keys/extended_transparent.rs rename to zingolib/src/wallet/keys/legacy/extended_transparent.rs index 16e378ca6..8e6c0f863 100644 --- a/zingolib/src/wallet/keys/extended_transparent.rs +++ b/zingolib/src/wallet/keys/legacy/extended_transparent.rs @@ -152,7 +152,7 @@ impl ExtendedPrivKey { } } -impl ReadableWriteable<()> for SecretKey { +impl ReadableWriteable for SecretKey { const VERSION: u8 = 0; // not applicable fn read(mut reader: R, _: ()) -> std::io::Result { let mut secret_key_bytes = [0; 32]; @@ -161,12 +161,12 @@ impl ReadableWriteable<()> for SecretKey { .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) } - fn write(&self, mut writer: W) -> std::io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> std::io::Result<()> { writer.write(&self.secret_bytes()).map(|_| ()) } } -impl ReadableWriteable<()> for ExtendedPrivKey { +impl ReadableWriteable for ExtendedPrivKey { const VERSION: u8 = 1; fn read(mut reader: R, _: ()) -> std::io::Result { @@ -179,9 +179,9 @@ impl ReadableWriteable<()> for ExtendedPrivKey { }) } - fn write(&self, mut writer: W) -> std::io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> std::io::Result<()> { writer.write_u8(Self::VERSION)?; - self.private_key.write(&mut writer)?; + self.private_key.write(&mut writer, ())?; Vector::write(&mut writer, &self.chain_code, |w, byte| w.write_u8(*byte))?; Ok(()) } @@ -225,7 +225,7 @@ impl ExtendedPubKey { } } -impl ReadableWriteable<()> for PublicKey { +impl ReadableWriteable for PublicKey { const VERSION: u8 = 0; // not applicable fn read(mut reader: R, _: ()) -> std::io::Result { let mut public_key_bytes = [0; 33]; @@ -234,15 +234,15 @@ impl ReadableWriteable<()> for PublicKey { .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) } - fn write(&self, mut writer: W) -> std::io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> std::io::Result<()> { writer.write(&self.serialize()).map(|_| ()) } } -impl ReadableWriteable<()> for ExtendedPubKey { +impl ReadableWriteable for ExtendedPubKey { const VERSION: u8 = 1; - fn read(mut reader: R, _: ()) -> std::io::Result { + fn read(mut reader: R, _input: ()) -> std::io::Result { Self::get_version(&mut reader)?; let public_key = PublicKey::read(&mut reader, ())?; let chain_code = Vector::read(&mut reader, |r| r.read_u8())?; @@ -252,9 +252,9 @@ impl ReadableWriteable<()> for ExtendedPubKey { }) } - fn write(&self, mut writer: W) -> std::io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> std::io::Result<()> { writer.write_u8(Self::VERSION)?; - self.public_key.write(&mut writer)?; + self.public_key.write(&mut writer, ())?; Vector::write(&mut writer, &self.chain_code, |w, byte| w.write_u8(*byte))?; Ok(()) } diff --git a/zingolib/src/wallet/keys/unified.rs b/zingolib/src/wallet/keys/unified.rs index 586d0e570..c2ef3daad 100644 --- a/zingolib/src/wallet/keys/unified.rs +++ b/zingolib/src/wallet/keys/unified.rs @@ -1,4 +1,5 @@ //! TODO: Add Mod Description Here! + use std::sync::atomic; use std::{ collections::{HashMap, HashSet}, @@ -9,63 +10,204 @@ use std::{marker::PhantomData, sync::Arc}; use append_only_vec::AppendOnlyVec; use bip0039::Mnemonic; -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use byteorder::{ReadBytesExt, WriteBytesExt}; +use getset::{Getters, Setters}; + use orchard::note_encryption::OrchardDomain; use sapling_crypto::note_encryption::SaplingDomain; -use zcash_primitives::consensus::{BranchId, NetworkConstants, Parameters}; - -use crate::config::{ChainType, ZingoConfig}; -use secp256k1::SecretKey; -use zcash_address::unified::{Container, Encoding, Typecode, Ufvk}; +use zcash_address::unified::{Encoding as _, Ufvk}; use zcash_client_backend::address::UnifiedAddress; use zcash_client_backend::keys::{Era, UnifiedSpendingKey}; +use zcash_client_backend::wallet::TransparentAddressMetadata; use zcash_encoding::{CompactSize, Vector}; -use zcash_primitives::zip32::AccountId; -use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_primitives::consensus::{NetworkConstants, Parameters}; +use zcash_primitives::legacy::{ + keys::{AccountPubKey, IncomingViewingKey, NonHardenedChildIndex, TransparentKeyScope}, + TransparentAddress, +}; +use zcash_primitives::zip32::{AccountId, DiversifierIndex}; +use crate::config::{ChainType, ZingoConfig}; +use crate::wallet::error::KeyError; use crate::wallet::traits::{DomainWalletExt, ReadableWriteable, Recipient}; -use super::{ - extended_transparent::{ExtendedPrivKey, ExtendedPubKey, KeyIndex}, - get_zaddr_from_bip39seed, ToBase58Check, -}; +use super::legacy::{generate_transparent_address_from_legacy_key, legacy_sks_to_usk, Capability}; +use super::ToBase58Check; -/// TODO: Add Doc Comment Here! -#[derive(Clone, Debug)] -#[non_exhaustive] -pub enum Capability { - /// TODO: Add Doc Comment Here! - None, - /// TODO: Add Doc Comment Here! - View(ViewingKeyType), - /// TODO: Add Doc Comment Here! - Spend(SpendKeyType), +pub(crate) const KEY_TYPE_EMPTY: u8 = 0; +pub(crate) const KEY_TYPE_VIEW: u8 = 1; +pub(crate) const KEY_TYPE_SPEND: u8 = 2; + +/// In-memory store for wallet spending or viewing keys +#[derive(Debug)] +pub enum UnifiedKeyStore { + /// Wallet with spend capability + Spend(Box), + /// Wallet with view capability + View(Box), + /// Wallet with no keys + Empty, } -impl Capability { - /// TODO: Add Doc Comment Here! - pub fn can_spend(&self) -> bool { - matches!(self, Capability::Spend(_)) +impl UnifiedKeyStore { + /// Returns true if [`UnifiedKeyStore`] is of `Spend` variant + pub fn is_spending_key(&self) -> bool { + matches!(self, UnifiedKeyStore::Spend(_)) } - /// TODO: Add Doc Comment Here! - pub fn can_view(&self) -> bool { + /// Returns true if [`UnifiedKeyStore`] is of `Spend` variant + pub fn is_empty(&self) -> bool { + matches!(self, UnifiedKeyStore::Empty) + } +} + +impl ReadableWriteable for UnifiedKeyStore { + const VERSION: u8 = 0; + fn read(mut reader: R, input: ChainType) -> io::Result { + let _version = Self::get_version(&mut reader)?; + let key_type = reader.read_u8()?; + Ok(match key_type { + KEY_TYPE_SPEND => { + UnifiedKeyStore::Spend(Box::new(UnifiedSpendingKey::read(reader, ())?)) + } + KEY_TYPE_VIEW => { + UnifiedKeyStore::View(Box::new(UnifiedFullViewingKey::read(reader, input)?)) + } + KEY_TYPE_EMPTY => UnifiedKeyStore::Empty, + x => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Unknown key type: {}", x), + )) + } + }) + } + + fn write(&self, mut writer: W, input: ChainType) -> io::Result<()> { + writer.write_u8(Self::VERSION)?; match self { - Capability::None => false, - Capability::View(_) => true, - Capability::Spend(_) => true, + UnifiedKeyStore::Spend(usk) => { + writer.write_u8(KEY_TYPE_SPEND)?; + usk.write(&mut writer, ()) + } + UnifiedKeyStore::View(ufvk) => { + writer.write_u8(KEY_TYPE_VIEW)?; + ufvk.write(&mut writer, input) + } + UnifiedKeyStore::Empty => writer.write_u8(KEY_TYPE_EMPTY), } } +} +impl ReadableWriteable for UnifiedSpendingKey { + const VERSION: u8 = 0; - /// TODO: Add Doc Comment Here! - pub fn kind_str(&self) -> &'static str { - match self { - Capability::None => "No key", - Capability::View(_) => "View only", - Capability::Spend(_) => "Spend capable", + fn read(mut reader: R, _input: ()) -> io::Result { + let len = CompactSize::read(&mut reader)?; + let mut usk = vec![0u8; len as usize]; + reader.read_exact(&mut usk)?; + + UnifiedSpendingKey::from_bytes(Era::Orchard, &usk) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "USK bytes are invalid")) + } + + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { + let usk_bytes = self.to_bytes(Era::Orchard); + CompactSize::write(&mut writer, usk_bytes.len())?; + writer.write_all(&usk_bytes)?; + Ok(()) + } +} +impl ReadableWriteable for UnifiedFullViewingKey { + const VERSION: u8 = 0; + + fn read(mut reader: R, input: ChainType) -> io::Result { + let len = CompactSize::read(&mut reader)?; + let mut ufvk = vec![0u8; len as usize]; + reader.read_exact(&mut ufvk)?; + let ufvk_encoded = std::str::from_utf8(&ufvk) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + + UnifiedFullViewingKey::decode(&input, ufvk_encoded).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("UFVK decoding error: {}", e), + ) + }) + } + + fn write(&self, mut writer: W, input: ChainType) -> io::Result<()> { + let ufvk_bytes = self.encode(&input).as_bytes().to_vec(); + CompactSize::write(&mut writer, ufvk_bytes.len())?; + writer.write_all(&ufvk_bytes)?; + Ok(()) + } +} + +impl TryFrom<&UnifiedKeyStore> for UnifiedSpendingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + match unified_key_store { + UnifiedKeyStore::Spend(usk) => Ok(*usk.clone()), + _ => Err(KeyError::NoSpendCapability), + } + } +} +impl TryFrom<&UnifiedKeyStore> for orchard::keys::SpendingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let usk = UnifiedSpendingKey::try_from(unified_key_store)?; + Ok(*usk.orchard()) + } +} +impl TryFrom<&UnifiedKeyStore> for sapling_crypto::zip32::ExtendedSpendingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let usk = UnifiedSpendingKey::try_from(unified_key_store)?; + Ok(usk.sapling().clone()) + } +} +impl TryFrom<&UnifiedKeyStore> for zcash_primitives::legacy::keys::AccountPrivKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let usk = UnifiedSpendingKey::try_from(unified_key_store)?; + Ok(usk.transparent().clone()) + } +} + +impl TryFrom<&UnifiedKeyStore> for UnifiedFullViewingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + match unified_key_store { + UnifiedKeyStore::Spend(usk) => Ok(usk.to_unified_full_viewing_key()), + UnifiedKeyStore::View(ufvk) => Ok(*ufvk.clone()), + UnifiedKeyStore::Empty => Err(KeyError::NoViewCapability), } } } +impl TryFrom<&UnifiedKeyStore> for orchard::keys::FullViewingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let ufvk = UnifiedFullViewingKey::try_from(unified_key_store)?; + ufvk.orchard().ok_or(KeyError::NoViewCapability).cloned() + } +} +impl TryFrom<&UnifiedKeyStore> for sapling_crypto::zip32::DiversifiableFullViewingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let ufvk = UnifiedFullViewingKey::try_from(unified_key_store)?; + ufvk.sapling().ok_or(KeyError::NoViewCapability).cloned() + } +} +impl TryFrom<&UnifiedKeyStore> for zcash_primitives::legacy::keys::AccountPubKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let ufvk = UnifiedFullViewingKey::try_from(unified_key_store)?; + ufvk.transparent() + .ok_or(KeyError::NoViewCapability) + .cloned() + } +} /// Interface to cryptographic capabilities that the library requires for /// various operations.
@@ -73,26 +215,19 @@ impl Capability { /// loaded from a [`zcash_keys::keys::UnifiedSpendingKey`]
/// or a [`zcash_keys::keys::UnifiedFullViewingKey`].

/// In addition to fundamental spending and viewing keys, the type caches generated addresses. -#[derive(Debug)] +#[derive(Debug, Getters, Setters)] pub struct WalletCapability { - /// TODO: Add Doc Comment Here! - pub transparent: Capability< - super::extended_transparent::ExtendedPubKey, - super::extended_transparent::ExtendedPrivKey, - >, - /// TODO: Add Doc Comment Here! - pub sapling: Capability< - sapling_crypto::zip32::DiversifiableFullViewingKey, - sapling_crypto::zip32::ExtendedSpendingKey, - >, - /// TODO: Add Doc Comment Here! - pub orchard: Capability, - + /// Unified key store + #[getset(get = "pub", set = "pub(crate)")] + unified_key_store: UnifiedKeyStore, /// Cache of transparent addresses that the user has created. /// Receipts to a single address are correlated on chain. /// TODO: Is there any reason to have this field, apart from the /// unified_addresses field? transparent_child_addresses: Arc>, + // TODO: read/write for ephmereral addresses + transparent_child_ephemeral_addresses: + Arc>, /// Cache of unified_addresses unified_addresses: append_only_vec::AppendOnlyVec, addresses_write_lock: AtomicBool, @@ -100,10 +235,9 @@ pub struct WalletCapability { impl Default for WalletCapability { fn default() -> Self { Self { - orchard: Capability::None, - sapling: Capability::None, - transparent: Capability::None, + unified_key_store: UnifiedKeyStore::Empty, transparent_child_addresses: Arc::new(AppendOnlyVec::new()), + transparent_child_ephemeral_addresses: Arc::new(AppendOnlyVec::new()), unified_addresses: AppendOnlyVec::new(), addresses_write_lock: AtomicBool::new(false), } @@ -128,10 +262,10 @@ pub struct ReceiverSelection { pub transparent: bool, } -impl ReadableWriteable<()> for ReceiverSelection { +impl ReadableWriteable for ReceiverSelection { const VERSION: u8 = 1; - fn read(mut reader: R, _: ()) -> io::Result { + fn read(mut reader: R, _input: ()) -> io::Result { let _version = Self::get_version(&mut reader)?; let receivers = reader.read_u8()?; Ok(Self { @@ -141,7 +275,7 @@ impl ReadableWriteable<()> for ReceiverSelection { }) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_u8(Self::VERSION)?; let mut receivers = 0; if self.orchard { @@ -166,7 +300,7 @@ fn read_write_receiver_selections() { { let mut receivers_selected_bytes = [0; 2]; receivers_selected - .write(receivers_selected_bytes.as_mut_slice()) + .write(receivers_selected_bytes.as_mut_slice(), ()) .unwrap(); assert_eq!(i as u8, receivers_selected_bytes[1]); } @@ -192,51 +326,59 @@ impl WalletCapability { &self.transparent_child_addresses } - /// TODO: Add Doc Comment Here! - pub fn ufvk(&self) -> Result { - 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]; - let possible_transparent_key: Result = self.try_into(); - 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 = 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()) - } + /// Generate a new ephemeral transparent address, + /// for use in a send to a TEX address. + pub fn new_ephemeral_address( + &self, + ) -> Result< + ( + zcash_primitives::legacy::TransparentAddress, + zcash_client_backend::wallet::TransparentAddressMetadata, + ), + String, + > { + let transparent_fvk: AccountPubKey = self + .unified_key_store() + .try_into() + .map_err(|e: KeyError| e.to_string())?; + + self.generate_transparent_receiver( + &transparent_fvk, + TransparentKeyScope::EPHEMERAL, + false, + )?; + + Ok(self + .transparent_child_ephemeral_addresses + .iter() + .last() + .expect("we just generated an address, this is known to be non-empty") + .clone()) } - /// TODO: Add Doc Comment Here! + /// Generates a unified address from the given desired receivers + /// + /// See [`crate::wallet::WalletCapability::generate_transparent_receiver`] for information on using `legacy_key` pub fn new_address( &self, desired_receivers: ReceiverSelection, + legacy_key: bool, ) -> Result { - if (desired_receivers.transparent & !self.transparent.can_view()) - | (desired_receivers.sapling & !self.sapling.can_view() - | (desired_receivers.orchard & !self.orchard.can_view())) - { - return Err("The wallet is not capable of producing desired receivers.".to_string()); - } if self .addresses_write_lock .swap(true, atomic::Ordering::Acquire) { return Err("addresses_write_lock collision!".to_string()); } + let previous_num_addresses = self.unified_addresses.len(); let orchard_receiver = if desired_receivers.orchard { - let fvk: orchard::keys::FullViewingKey = match self.try_into() { + let fvk: orchard::keys::FullViewingKey = match self.unified_key_store().try_into() { Ok(viewkey) => viewkey, Err(e) => { self.addresses_write_lock .swap(false, atomic::Ordering::Release); - return Err(e); + return Err(e.to_string()); } }; Some(fvk.address_at(self.unified_addresses.len(), orchard::keys::Scope::External)) @@ -245,19 +387,26 @@ impl WalletCapability { }; // produce a Sapling address to increment Sapling diversifier index - let sapling_receiver = if desired_receivers.sapling && self.sapling.can_view() { + let sapling_receiver = if desired_receivers.sapling { let mut sapling_diversifier_index = DiversifierIndex::new(); let mut address; let mut count = 0; let fvk: sapling_crypto::zip32::DiversifiableFullViewingKey = - self.try_into().expect("to create an fvk"); + match self.unified_key_store().try_into() { + Ok(viewkey) => viewkey, + Err(e) => { + self.addresses_write_lock + .swap(false, atomic::Ordering::Release); + return Err(e.to_string()); + } + }; loop { (sapling_diversifier_index, address) = fvk .find_address(sapling_diversifier_index) .expect("Diversifier index overflow"); sapling_diversifier_index .increment() - .expect("diversifier index overflow"); + .expect("Diversifier index overflow"); // Not all sapling_diversifier_indexes produce valid // sapling addresses. // Because of this self.unified_addresses.len() @@ -272,25 +421,36 @@ impl WalletCapability { None }; - let transparent_receiver = match self.generate_transparent_receiver(desired_receivers) { - Ok(Some(transparent_receiver)) => Some(transparent_receiver), - Ok(None) => None, - Err(e) => { - self.addresses_write_lock - .swap(false, atomic::Ordering::Release); - return Err(e); + let transparent_receiver = if desired_receivers.transparent { + let transparent_fvk: AccountPubKey = match self.unified_key_store().try_into() { + Ok(viewkey) => viewkey, + Err(e) => { + self.addresses_write_lock + .swap(false, atomic::Ordering::Release); + return Err(e.to_string()); + } + }; + + match self.generate_transparent_receiver( + &transparent_fvk, + TransparentKeyScope::EXTERNAL, + legacy_key, + ) { + Ok(t_addr) => Some(t_addr), + Err(e) => { + self.addresses_write_lock + .swap(false, atomic::Ordering::Release); + return Err(e.to_string()); + } } + } else { + None }; let ua = UnifiedAddress::from_receivers( orchard_receiver, sapling_receiver, - #[allow(deprecated)] - transparent_receiver - .as_ref() - // This is deprecated. Not sure what the alternative is, - // other than implementing it ourselves. - .map(zcash_primitives::legacy::keys::pubkey_to_address), + transparent_receiver, ); let ua = match ua { Some(address) => address, @@ -310,102 +470,128 @@ impl WalletCapability { Ok(ua) } - /// Generates a transparent receiver if the wallet is capable of it. - /// - /// If the wallet is not capable of generating a transparent receiver, - /// `None` is returned. + /// Generates a transparent receiver for the specified scope. pub fn generate_transparent_receiver( &self, - desired_receivers: ReceiverSelection, - ) -> Result, String> { - if !desired_receivers.transparent { - return Ok(None); - } - let child_index = KeyIndex::from_index(self.unified_addresses.len() as u32); - let child_pk = match &self.transparent { - Capability::Spend(ext_sk) => { - let secp = secp256k1::Secp256k1::new(); - Some( - match ext_sk.derive_private_key(child_index) { - Err(e) => { - self.addresses_write_lock - .swap(false, atomic::Ordering::Release); - return Err(format!("Transparent private key derivation failed: {e}")); - } - Ok(res) => res.private_key, - } - .public_key(&secp), - ) - } - Capability::View(ext_pk) => Some(match ext_pk.derive_public_key(child_index) { - Err(e) => { - self.addresses_write_lock - .swap(false, atomic::Ordering::Release); - return Err(format!("Transparent public key derivation failed: {e}")); + transparent_fvk: &AccountPubKey, + scope: TransparentKeyScope, + // this should only be `true` when generating transparent addresses while loading from legacy keys (pre wallet version 29) + // legacy transparent keys are already derived to the external scope so setting `legacy_key` to `true` will skip this scope derivation + legacy_key: bool, + ) -> Result { + let address_for_scope = |transparent_fvk: &AccountPubKey, + scope: TransparentKeyScope, + child_index: NonHardenedChildIndex| + -> Result { + match scope { + TransparentKeyScope::EXTERNAL => { + let t_addr = if legacy_key { + generate_transparent_address_from_legacy_key(transparent_fvk, child_index)? + } else { + transparent_fvk + .derive_external_ivk() + .map_err(|e| e.to_string())? + .derive_address(child_index) + .map_err(|e| e.to_string())? + }; + self.transparent_child_addresses + .push((self.unified_addresses.len(), t_addr)); + Ok(t_addr) + } + TransparentKeyScope::INTERNAL => { + //TODO: remember transparent internal change addresses + Ok(transparent_fvk + .derive_internal_ivk() + .map_err(|e| e.to_string())? + .derive_address(child_index) + .map_err(|e| e.to_string())?) } - Ok(res) => res.public_key, - }), - Capability::None => None, + TransparentKeyScope::EPHEMERAL => { + let t_addr = transparent_fvk + .derive_ephemeral_ivk() + .map_err(|e| e.to_string())? + .derive_ephemeral_address(child_index) + .map_err(|e| e.to_string())?; + self.transparent_child_ephemeral_addresses.push(( + t_addr, + TransparentAddressMetadata::new( + TransparentKeyScope::EPHEMERAL, + child_index, + ), + )); + Ok(t_addr) + } + _ => Err(bip32::Error::Bip39.to_string()), + } }; - if let Some(pk) = child_pk { - self.transparent_child_addresses.push(( - self.unified_addresses.len(), - #[allow(deprecated)] - zcash_primitives::legacy::keys::pubkey_to_address(&pk), - )); - Ok(Some(pk)) - } else { - Ok(None) - } + + let child_index = NonHardenedChildIndex::from_index( + match scope { + TransparentKeyScope::EXTERNAL => Ok(self.unified_addresses.len()), + TransparentKeyScope::INTERNAL => { + todo!("transparent change addresses") + } + TransparentKeyScope::EPHEMERAL => { + Ok(self.transparent_child_ephemeral_addresses.len()) + } + _ => Err(bip32::Error::Bip39), + } + .map_err(|e| e.to_string())? as u32, + ) + .expect("hardened bit should not be set for non-hardened child indexes"); + + let transparent_receiver = address_for_scope(transparent_fvk, scope, child_index)?; + + Ok(transparent_receiver) } /// TODO: Add Doc Comment Here! + #[deprecated(note = "not used in zingolib codebase")] pub fn get_taddr_to_secretkey_map( &self, chain: &ChainType, - ) -> Result, String> { - if let Capability::Spend(transparent_sk) = &self.transparent { + ) -> Result, KeyError> { + if let UnifiedKeyStore::Spend(usk) = self.unified_key_store() { self.transparent_child_addresses() .iter() - .map(|(i, taddr)| -> Result<_, String> { + .map(|(i, taddr)| -> Result<_, KeyError> { let hash = match taddr { TransparentAddress::PublicKeyHash(hash) => hash, TransparentAddress::ScriptHash(hash) => hash, }; Ok(( hash.to_base58check(&chain.b58_pubkey_address_prefix(), &[]), - transparent_sk - .derive_private_key(KeyIndex::Normal(*i as u32)) - .map_err(|e| e.to_string())? - .private_key, + usk.transparent() + .derive_external_secret_key( + NonHardenedChildIndex::from_index(*i as u32) + .ok_or(KeyError::InvalidNonHardenedChildIndex)?, + ) + .map_err(|_| KeyError::KeyDerivationError)?, )) }) .collect::>() } else { - Err("Wallet is no capable to spend transparent funds".to_string()) + Err(KeyError::NoSpendCapability) } } /// TODO: Add Doc Comment Here! - pub fn new_from_seed(config: &ZingoConfig, seed: &[u8; 64], position: u32) -> Self { - let (sapling_key, _, _) = get_zaddr_from_bip39seed(config, seed, position); - let transparent_parent_key = - super::extended_transparent::ExtendedPrivKey::get_ext_taddr_from_bip39seed( - config, seed, position, - ); - - let orchard_key = orchard::keys::SpendingKey::from_zip32_seed( + pub fn new_from_seed( + config: &ZingoConfig, + seed: &[u8; 64], + position: u32, + ) -> Result { + let usk = UnifiedSpendingKey::from_seed( + &config.chain, seed, - config.chain.coin_type(), - AccountId::try_from(position).unwrap(), + AccountId::try_from(position).map_err(KeyError::InvalidAccountId)?, ) - .unwrap(); - Self { - orchard: Capability::Spend(orchard_key), - sapling: Capability::Spend(sapling_key), - transparent: Capability::Spend(transparent_parent_key), + .map_err(|_| KeyError::KeyDerivationError)?; + + Ok(Self { + unified_key_store: UnifiedKeyStore::Spend(Box::new(usk)), ..Default::default() - } + }) } /// TODO: Add Doc Comment Here! @@ -413,80 +599,42 @@ impl WalletCapability { config: &ZingoConfig, seed_phrase: &Mnemonic, position: u32, - ) -> Result { + ) -> Result { // The seed bytes is the raw entropy. To pass it to HD wallet generation, // we need to get the 64 byte bip39 entropy let bip39_seed = seed_phrase.to_seed(""); - Ok(Self::new_from_seed(config, &bip39_seed, position)) + Self::new_from_seed(config, &bip39_seed, position) } /// Creates a new `WalletCapability` from a unified spending key. - pub fn new_from_usk(usk: &[u8]) -> Result { + pub fn new_from_usk(usk: &[u8]) -> Result { // Decode unified spending key let usk = UnifiedSpendingKey::from_bytes(Era::Orchard, usk) - .map_err(|_| "Error decoding unified spending key.")?; - - // Workaround https://github.com/zcash/librustzcash/issues/929 by serializing and deserializing the transparent key. - let transparent_bytes = usk.transparent().to_bytes(); - let transparent_ext_key = transparent_key_from_bytes(transparent_bytes.as_slice()) - .map_err(|e| format!("Error processing transparent key: {}", e))?; + .map_err(|_| KeyError::KeyDecodingError)?; Ok(Self { - orchard: Capability::Spend(usk.orchard().to_owned()), - sapling: Capability::Spend(usk.sapling().to_owned()), - transparent: Capability::Spend(transparent_ext_key), + unified_key_store: UnifiedKeyStore::Spend(Box::new(usk)), ..Default::default() }) } /// TODO: Add Doc Comment Here! - pub fn new_from_ufvk(config: &ZingoConfig, ufvk_encoded: String) -> Result { + pub fn new_from_ufvk(config: &ZingoConfig, ufvk_encoded: String) -> Result { // Decode UFVK if ufvk_encoded.starts_with(config.chain.hrp_sapling_extended_full_viewing_key()) { - return Err("Viewing keys must be imported in the unified format".to_string()); + return Err(KeyError::InvalidFormat); } - let (network, ufvk) = Ufvk::decode(&ufvk_encoded) - .map_err(|e| format!("Error decoding unified full viewing key: {}", e))?; + let (network, ufvk) = + Ufvk::decode(&ufvk_encoded).map_err(|_| KeyError::KeyDecodingError)?; if network != config.chain.network_type() { - return Err("Given UFVK is not valid for current chain".to_string()); + return Err(KeyError::NetworkMismatch); } + let ufvk = UnifiedFullViewingKey::parse(&ufvk).map_err(|_| KeyError::KeyDecodingError)?; - // 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 { - UfvkComponent::Orchard(key_bytes) => { - wc.orchard = Capability::View( - orchard::keys::FullViewingKey::from_bytes(&key_bytes) - .ok_or("Orchard FVK deserialization failed")?, - ); - } - UfvkComponent::Sapling(key_bytes) => { - wc.sapling = Capability::View( - sapling_crypto::zip32::DiversifiableFullViewingKey::read( - &key_bytes[..], - (), - ) - .map_err(|e| e.to_string())?, - ); - } - 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())?, - }); - } - UfvkComponent::Unknown { typecode, data: _ } => { - log::info!( - "Unknown receiver of type {} found in Unified Viewing Key", - typecode - ); - } - } - } - Ok(wc) + Ok(Self { + unified_key_store: UnifiedKeyStore::View(Box::new(ufvk)), + ..Default::default() + }) } pub(crate) fn get_all_taddrs(&self, chain: &crate::config::ChainType) -> HashSet { @@ -517,15 +665,10 @@ impl WalletCapability { *self.addresses()[0].sapling().unwrap() } - /// Returns a selection of pools where the wallet can spend funds. - pub fn can_spend_from_all_pools(&self) -> bool { - self.orchard.can_spend() && self.sapling.can_spend() && self.transparent.can_spend() - } - /// TODO: Add Doc Comment Here! //TODO: NAME?????!! pub fn get_trees_witness_trees(&self) -> Option { - if self.can_spend_from_all_pools() { + if self.unified_key_store().is_spending_key() { Some(crate::data::witness_trees::WitnessTrees::default()) } else { None @@ -534,91 +677,150 @@ impl WalletCapability { /// Returns a selection of pools where the wallet can view funds. pub fn can_view(&self) -> ReceiverSelection { - ReceiverSelection { - orchard: self.orchard.can_view(), - sapling: self.sapling.can_view(), - transparent: self.transparent.can_view(), + match self.unified_key_store() { + UnifiedKeyStore::Spend(_) => ReceiverSelection { + orchard: true, + sapling: true, + transparent: true, + }, + UnifiedKeyStore::View(ufvk) => ReceiverSelection { + orchard: ufvk.orchard().is_some(), + sapling: ufvk.sapling().is_some(), + transparent: ufvk.transparent().is_some(), + }, + UnifiedKeyStore::Empty => ReceiverSelection { + orchard: false, + sapling: false, + transparent: false, + }, } } } -/// Reads a transparent ExtendedPrivKey from a buffer that has a 32 byte private key and 32 byte chain code. -fn transparent_key_from_bytes(bytes: &[u8]) -> Result { - let mut reader = std::io::Cursor::new(bytes); +impl ReadableWriteable for WalletCapability { + const VERSION: u8 = 3; - let private_key = SecretKey::read(&mut reader, ())?; - let mut chain_code = [0; 32]; - reader.read_exact(&mut chain_code)?; + fn read(mut reader: R, input: ChainType) -> io::Result { + let version = Self::get_version(&mut reader)?; + let legacy_key: bool; + let wc = match version { + // in version 1, only spending keys are stored + 1 => { + legacy_key = true; + + // Create a temporary USK for address generation to load old wallets + // due to missing BIP0032 transparent extended private key data + // + // USK is re-derived later from seed due to missing BIP0032 transparent extended private key data + let orchard_sk = orchard::keys::SpendingKey::read(&mut reader, ())?; + let sapling_sk = sapling_crypto::zip32::ExtendedSpendingKey::read(&mut reader)?; + let transparent_sk = + super::legacy::extended_transparent::ExtendedPrivKey::read(&mut reader, ())?; + let usk = legacy_sks_to_usk(&orchard_sk, &sapling_sk, &transparent_sk) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + Self { + unified_key_store: UnifiedKeyStore::Spend(Box::new(usk)), + ..Default::default() + } + } + 2 => { + legacy_key = true; + + let orchard_capability = Capability::< + orchard::keys::FullViewingKey, + orchard::keys::SpendingKey, + >::read(&mut reader, ())?; + let sapling_capability = Capability::< + sapling_crypto::zip32::DiversifiableFullViewingKey, + sapling_crypto::zip32::ExtendedSpendingKey, + >::read(&mut reader, ())?; + let transparent_capability = Capability::< + super::legacy::extended_transparent::ExtendedPubKey, + super::legacy::extended_transparent::ExtendedPrivKey, + >::read(&mut reader, ())?; + + let orchard_fvk = match &orchard_capability { + Capability::View(fvk) => Some(fvk), + _ => None, + }; + let sapling_fvk = match &sapling_capability { + Capability::View(fvk) => Some(fvk), + _ => None, + }; + let transparent_fvk = match &transparent_capability { + Capability::View(fvk) => Some(fvk), + _ => None, + }; + + let unified_key_store = if orchard_fvk.is_some() + || sapling_fvk.is_some() + || transparent_fvk.is_some() + { + // In the case of loading from viewing keys: + // Create the UFVK from FVKs. + let ufvk = super::legacy::legacy_fvks_to_ufvk( + orchard_fvk, + sapling_fvk, + transparent_fvk, + &input, + ) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + UnifiedKeyStore::View(Box::new(ufvk)) + } else if matches!(sapling_capability.clone(), Capability::Spend(_)) { + // In the case of loading spending keys: + // Only sapling is checked for spend capability due to only supporting a full set of spend keys + // + // Create a temporary USK for address generation to load old wallets + // due to missing BIP0032 transparent extended private key data + // + // USK is re-derived later from seed due to missing BIP0032 transparent extended private key data + // this missing data is not required for UFVKs + let orchard_sk = match &orchard_capability { + Capability::Spend(sk) => sk, + _ => return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Orchard spending key not found. Wallet should have full spend capability!" + .to_string(), + )), + }; + let sapling_sk = match &sapling_capability { + Capability::Spend(sk) => sk, + _ => return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Sapling spending key not found. Wallet should have full spend capability!" + .to_string(), + )), + }; + let transparent_sk = match &transparent_capability { + Capability::Spend(sk) => sk, + _ => return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Transparent spending key not found. Wallet should have full spend capability!" + .to_string(), + )), + }; - Ok(ExtendedPrivKey { - chain_code: chain_code.to_vec(), - private_key, - }) -} + let usk = legacy_sks_to_usk(orchard_sk, sapling_sk, transparent_sk) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; -impl ReadableWriteable<()> for Capability -where - V: ReadableWriteable<()>, - S: ReadableWriteable<()>, -{ - const VERSION: u8 = 1; - fn read(mut reader: R, _input: ()) -> io::Result { - let _version = Self::get_version(&mut reader)?; - let capability_type = reader.read_u8()?; - Ok(match capability_type { - 0 => Capability::None, - 1 => Capability::View(V::read(&mut reader, ())?), - 2 => Capability::Spend(S::read(&mut reader, ())?), - x => { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Unknown wallet Capability type: {}", x), - )) - } - }) - } + UnifiedKeyStore::Spend(Box::new(usk)) + } else { + UnifiedKeyStore::Empty + }; - fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_u8(Self::VERSION)?; - match self { - Capability::None => writer.write_u8(0), - Capability::View(vk) => { - writer.write_u8(1)?; - vk.write(&mut writer) - } - Capability::Spend(sk) => { - writer.write_u8(2)?; - sk.write(&mut writer) + Self { + unified_key_store, + ..Default::default() + } } - } - } -} + 3 => { + legacy_key = false; -impl ReadableWriteable<()> for WalletCapability { - const VERSION: u8 = 2; - - fn read(mut reader: R, _input: ()) -> io::Result { - let version = Self::get_version(&mut reader)?; - let wc = match version { - // in version 1, only spending keys are stored - 1 => { - let orchard = orchard::keys::SpendingKey::read(&mut reader, ())?; - let sapling = sapling_crypto::zip32::ExtendedSpendingKey::read(&mut reader)?; - let transparent = - super::extended_transparent::ExtendedPrivKey::read(&mut reader, ())?; Self { - orchard: Capability::Spend(orchard), - sapling: Capability::Spend(sapling), - transparent: Capability::Spend(transparent), + unified_key_store: UnifiedKeyStore::read(&mut reader, input)?, ..Default::default() } } - 2 => Self { - orchard: Capability::read(&mut reader, ())?, - sapling: Capability::read(&mut reader, ())?, - transparent: Capability::read(&mut reader, ())?, - ..Default::default() - }, _ => { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -628,17 +830,15 @@ impl ReadableWriteable<()> for WalletCapability { }; let receiver_selections = Vector::read(reader, |r| ReceiverSelection::read(r, ()))?; for rs in receiver_selections { - wc.new_address(rs) + wc.new_address(rs, legacy_key) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; } Ok(wc) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, input: ChainType) -> io::Result<()> { writer.write_u8(Self::VERSION)?; - self.orchard.write(&mut writer)?; - self.sapling.write(&mut writer)?; - self.transparent.write(&mut writer)?; + self.unified_key_store().write(&mut writer, input)?; Vector::write( &mut writer, &self.unified_addresses.iter().collect::>(), @@ -648,7 +848,7 @@ impl ReadableWriteable<()> for WalletCapability { sapling: address.sapling().is_some(), transparent: address.transparent().is_some(), } - .write(w) + .write(w, ()) }, ) } @@ -701,78 +901,6 @@ where __scope: PhantomData, } -impl TryFrom<&WalletCapability> for super::extended_transparent::ExtendedPrivKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.transparent { - Capability::Spend(sk) => Ok(sk.clone()), - _ => Err("The wallet is not capable of spending transparent funds".to_string()), - } - } -} - -impl TryFrom<&WalletCapability> for sapling_crypto::zip32::ExtendedSpendingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.sapling { - Capability::Spend(sk) => Ok(sk.clone()), - _ => Err("The wallet is not capable of spending Sapling funds".to_string()), - } - } -} - -impl TryFrom<&WalletCapability> for orchard::keys::SpendingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.orchard { - Capability::Spend(sk) => Ok(*sk), - _ => Err("The wallet is not capable of spending Orchard funds".to_string()), - } - } -} - -impl TryFrom<&WalletCapability> for super::extended_transparent::ExtendedPubKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.transparent { - Capability::Spend(ext_sk) => Ok(ExtendedPubKey::from(ext_sk)), - Capability::View(ext_pk) => Ok(ext_pk.clone()), - Capability::None => { - Err("The wallet is not capable of viewing transparent funds".to_string()) - } - } - } -} - -impl TryFrom<&WalletCapability> for orchard::keys::FullViewingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.orchard { - Capability::Spend(sk) => Ok(orchard::keys::FullViewingKey::from(sk)), - Capability::View(fvk) => Ok(fvk.clone()), - Capability::None => { - Err("The wallet is not capable of viewing Orchard funds".to_string()) - } - } - } -} - -impl TryFrom<&WalletCapability> for sapling_crypto::zip32::DiversifiableFullViewingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.sapling { - Capability::Spend(sk) => { - let dfvk = sk.to_diversifiable_full_viewing_key(); - Ok(dfvk) - } - Capability::View(fvk) => Ok(fvk.clone()), - Capability::None => { - Err("The wallet is not capable of viewing Sapling funds".to_string()) - } - } - } -} - /// TODO: Add Doc Comment Here! pub trait Fvk where @@ -816,96 +944,3 @@ impl Fvk for sapling_crypto::zip32::DiversifiableFullViewingKey { } } } - -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(orchard::keys::Scope::External)) - } -} - -impl TryFrom<&WalletCapability> for sapling_crypto::keys::OutgoingViewingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - let fvk: sapling_crypto::zip32::DiversifiableFullViewingKey = wc.try_into()?; - Ok(fvk.fvk().ovk) - } -} - -impl TryFrom<&WalletCapability> for UnifiedSpendingKey { - type Error = io::Error; - - fn try_from(value: &WalletCapability) -> Result { - let transparent = &value.transparent; - let sapling = &value.sapling; - let orchard = &value.orchard; - match (transparent, sapling, orchard) { - (Capability::Spend(tkey), Capability::Spend(skey), Capability::Spend(okey)) => { - let mut key_bytes = Vec::new(); - // orchard Era usk - key_bytes.write_u32::(BranchId::Nu5.into())?; - - let okey_bytes = okey.to_bytes(); - CompactSize::write(&mut key_bytes, u32::from(Typecode::Orchard) as usize)?; - CompactSize::write(&mut key_bytes, okey_bytes.len())?; - key_bytes.write_all(okey_bytes)?; - - let skey_bytes = skey.to_bytes(); - CompactSize::write(&mut key_bytes, u32::from(Typecode::Sapling) as usize)?; - CompactSize::write(&mut key_bytes, skey_bytes.len())?; - key_bytes.write_all(&skey_bytes)?; - - let mut tkey_bytes = Vec::new(); - tkey_bytes.write_all(tkey.private_key.as_ref())?; - tkey_bytes.write_all(&tkey.chain_code)?; - - CompactSize::write(&mut key_bytes, u32::from(Typecode::P2pkh) as usize)?; - CompactSize::write(&mut key_bytes, tkey_bytes.len())?; - key_bytes.write_all(&tkey_bytes)?; - - UnifiedSpendingKey::from_bytes(Era::Orchard, &key_bytes).map_err(|e| { - io::Error::new(io::ErrorKind::InvalidInput, format!("bad usk: {e}")) - }) - } - _otherwise => Err(io::Error::new( - io::ErrorKind::InvalidData, - "don't have spend keys", - )), - } - } -} - -#[cfg(test)] -pub async fn get_transparent_secretkey_pubkey_taddr( - lightclient: &crate::lightclient::LightClient, -) -> ( - Option, - Option, - Option, -) { - use super::address_from_pubkeyhash; - - let wc = lightclient.wallet.wallet_capability(); - // 2. Get an incoming transaction to a t address - let (sk, pk) = match &wc.transparent { - Capability::None => (None, None), - Capability::View(ext_pk) => { - let child_ext_pk = ext_pk.derive_public_key(KeyIndex::Normal(0)).ok(); - (None, child_ext_pk.map(|x| x.public_key)) - } - Capability::Spend(master_sk) => { - let secp = secp256k1::Secp256k1::new(); - let extsk = master_sk - .derive_private_key(KeyIndex::Normal(wc.transparent_child_addresses[0].0 as u32)) - .unwrap(); - let pk = extsk.private_key.public_key(&secp); - #[allow(deprecated)] - (Some(extsk.private_key), Some(pk)) - } - }; - let taddr = wc.addresses()[0] - .transparent() - .map(|taddr| address_from_pubkeyhash(&lightclient.config, *taddr)); - (sk, pk, taddr) -} diff --git a/zingolib/src/wallet/notes/interface.rs b/zingolib/src/wallet/notes/interface.rs index 942518ee8..f27c6b4a3 100644 --- a/zingolib/src/wallet/notes/interface.rs +++ b/zingolib/src/wallet/notes/interface.rs @@ -101,7 +101,7 @@ pub trait ShieldedNoteInterface: OutputInterface + OutputConstructor + Sized { type Diversifier: Copy + FromBytes<11> + ToBytes<11>; /// TODO: Add Doc Comment Here! type Note: PartialEq - + for<'a> ReadableWriteable<(Self::Diversifier, &'a WalletCapability)> + + for<'a> ReadableWriteable<(Self::Diversifier, &'a WalletCapability), ()> + Clone; /// TODO: Add Doc Comment Here! type Node: Hashable + HashSer + FromCommitment + Send + Clone + PartialEq + Eq; diff --git a/zingolib/src/wallet/traits.rs b/zingolib/src/wallet/traits.rs index 83f029a44..7cf872fcb 100644 --- a/zingolib/src/wallet/traits.rs +++ b/zingolib/src/wallet/traits.rs @@ -53,6 +53,8 @@ use zcash_primitives::{ }; use zingo_status::confirmation_status::ConfirmationStatus; +use super::keys::unified::UnifiedKeyStore; + /// This provides a uniform `.to_bytes` to types that might require it in a generic context. pub trait ToBytes { /// TODO: Add Doc Comment Here! @@ -469,11 +471,11 @@ pub trait DomainWalletExt: type Fvk: Clone + Send + Diversifiable - + for<'a> TryFrom<&'a WalletCapability> + + for<'a> TryFrom<&'a UnifiedKeyStore> + super::keys::unified::Fvk; /// TODO: Add Doc Comment Here! - type SpendingKey: for<'a> TryFrom<&'a WalletCapability> + Clone; + type SpendingKey: for<'a> TryFrom<&'a UnifiedKeyStore> + Clone; /// TODO: Add Doc Comment Here! type CompactOutput: CompactOutput; /// TODO: Add Doc Comment Here! @@ -541,10 +543,7 @@ pub trait DomainWalletExt: ) -> Option<&'a UnifiedAddress>; /// TODO: Add Doc Comment Here! - fn wc_to_fvk(wc: &WalletCapability) -> Result; - - /// TODO: Add Doc Comment Here! - fn wc_to_sk(wc: &WalletCapability) -> Result; + fn unified_key_store_to_fvk(unified_key_store: &UnifiedKeyStore) -> Result; } impl DomainWalletExt for SaplingDomain { @@ -606,12 +605,8 @@ impl DomainWalletExt for SaplingDomain { .find(|ua| ua.sapling() == Some(receiver)) } - fn wc_to_fvk(wc: &WalletCapability) -> Result { - Self::Fvk::try_from(wc) - } - - fn wc_to_sk(wc: &WalletCapability) -> Result { - Self::SpendingKey::try_from(wc) + fn unified_key_store_to_fvk(unified_key_store: &UnifiedKeyStore) -> Result { + Self::Fvk::try_from(unified_key_store).map_err(|e| e.to_string()) } } @@ -674,12 +669,8 @@ impl DomainWalletExt for OrchardDomain { .find(|unified_address| unified_address.orchard() == Some(receiver)) } - fn wc_to_fvk(wc: &WalletCapability) -> Result { - Self::Fvk::try_from(wc) - } - - fn wc_to_sk(wc: &WalletCapability) -> Result { - Self::SpendingKey::try_from(wc) + fn unified_key_store_to_fvk(unified_key_store: &UnifiedKeyStore) -> Result { + Self::Fvk::try_from(unified_key_store).map_err(|e| e.to_string()) } } @@ -889,15 +880,15 @@ impl SpendableNote for SpendableOrchardNote { } /// TODO: Add Doc Comment Here! -pub trait ReadableWriteable: Sized { +pub trait ReadableWriteable: Sized { /// TODO: Add Doc Comment Here! const VERSION: u8; /// TODO: Add Doc Comment Here! - fn read(reader: R, input: Input) -> io::Result; + fn read(reader: R, input: ReadInput) -> io::Result; /// TODO: Add Doc Comment Here! - fn write(&self, writer: W) -> io::Result<()>; + fn write(&self, writer: W, input: WriteInput) -> io::Result<()>; /// TODO: Add Doc Comment Here! fn get_version(mut reader: R) -> io::Result { @@ -916,10 +907,10 @@ pub trait ReadableWriteable: Sized { } } -impl ReadableWriteable<()> for orchard::keys::SpendingKey { +impl ReadableWriteable for orchard::keys::SpendingKey { const VERSION: u8 = 0; //Not applicable - fn read(mut reader: R, _: ()) -> io::Result { + fn read(mut reader: R, _input: ()) -> io::Result { let mut data = [0u8; 32]; reader.read_exact(&mut data)?; @@ -931,27 +922,27 @@ impl ReadableWriteable<()> for orchard::keys::SpendingKey { }) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_all(self.to_bytes()) } } -impl ReadableWriteable<()> for sapling_crypto::zip32::ExtendedSpendingKey { +impl ReadableWriteable for sapling_crypto::zip32::ExtendedSpendingKey { const VERSION: u8 = 0; //Not applicable - fn read(reader: R, _: ()) -> io::Result { + fn read(reader: R, _input: ()) -> io::Result { Self::read(reader) } - fn write(&self, writer: W) -> io::Result<()> { + fn write(&self, writer: W, _input: ()) -> io::Result<()> { self.write(writer) } } -impl ReadableWriteable<()> for sapling_crypto::zip32::DiversifiableFullViewingKey { +impl ReadableWriteable for sapling_crypto::zip32::DiversifiableFullViewingKey { const VERSION: u8 = 0; //Not applicable - fn read(mut reader: R, _: ()) -> io::Result { + fn read(mut reader: R, _input: ()) -> io::Result { let mut fvk_bytes = [0u8; 128]; reader.read_exact(&mut fvk_bytes)?; sapling_crypto::zip32::DiversifiableFullViewingKey::from_bytes(&fvk_bytes).ok_or( @@ -962,19 +953,19 @@ impl ReadableWriteable<()> for sapling_crypto::zip32::DiversifiableFullViewingKe ) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_all(&self.to_bytes()) } } -impl ReadableWriteable<()> for orchard::keys::FullViewingKey { +impl ReadableWriteable for orchard::keys::FullViewingKey { const VERSION: u8 = 0; //Not applicable - fn read(reader: R, _: ()) -> io::Result { + fn read(reader: R, _input: ()) -> io::Result { Self::read(reader) } - fn write(&self, writer: W) -> io::Result<()> { + fn write(&self, writer: W, _input: ()) -> io::Result<()> { self.write(writer) } } @@ -991,17 +982,19 @@ impl ReadableWriteable<(sapling_crypto::Diversifier, &WalletCapability)> for sap let rseed = super::data::read_sapling_rseed(&mut reader)?; Ok( - ::wc_to_fvk(wallet_capability) - .expect("to get an fvk from a wc") - .fvk() - .vk - .to_payment_address(diversifier) - .unwrap() - .create_note(sapling_crypto::value::NoteValue::from_raw(value), rseed), + ::unified_key_store_to_fvk( + wallet_capability.unified_key_store(), + ) + .expect("to get an fvk from the unified key store") + .fvk() + .vk + .to_payment_address(diversifier) + .unwrap() + .create_note(sapling_crypto::value::NoteValue::from_raw(value), rseed), ) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_u8(Self::VERSION)?; writer.write_u64::(self.value().inner())?; super::data::write_sapling_rseed(&mut writer, self.rseed())?; @@ -1034,8 +1027,10 @@ impl ReadableWriteable<(orchard::keys::Diversifier, &WalletCapability)> for orch "Nullifier not for note", ))?; - let fvk = ::wc_to_fvk(wallet_capability) - .expect("to get an fvk from a wc"); + let fvk = ::unified_key_store_to_fvk( + wallet_capability.unified_key_store(), + ) + .expect("to get an fvk from the unified key store"); Option::from(orchard::note::Note::from_parts( fvk.address(diversifier, orchard::keys::Scope::External), orchard::value::NoteValue::from_raw(value), @@ -1045,7 +1040,7 @@ impl ReadableWriteable<(orchard::keys::Diversifier, &WalletCapability)> for orch .ok_or(io::Error::new(io::ErrorKind::InvalidInput, "Invalid note")) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_u8(Self::VERSION)?; writer.write_u64::(self.value().inner())?; writer.write_all(&self.rho().to_bytes())?; @@ -1092,8 +1087,10 @@ where reader.read_exact(&mut diversifier_bytes)?; let diversifier = T::Diversifier::from_bytes(diversifier_bytes); - let note = - >::read(&mut reader, (diversifier, wallet_capability))?; + let note = >::read( + &mut reader, + (diversifier, wallet_capability), + )?; let witnessed_position = if external_version >= 4 { Position::from(reader.read_u64::()?) @@ -1188,13 +1185,13 @@ where )) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { // Write a version number first, so we can later upgrade this if needed. writer.write_u8(Self::VERSION)?; writer.write_all(&self.diversifier().to_bytes())?; - self.note().write(&mut writer)?; + self.note().write(&mut writer, ())?; writer.write_u64::(u64::from(self.witnessed_position().ok_or( io::Error::new( io::ErrorKind::InvalidData, diff --git a/zingolib/src/wallet/transaction_context.rs b/zingolib/src/wallet/transaction_context.rs index 5626609ef..c7f801eb0 100644 --- a/zingolib/src/wallet/transaction_context.rs +++ b/zingolib/src/wallet/transaction_context.rs @@ -514,8 +514,8 @@ mod decrypt_transaction { }) .collect::>(); - let Ok(fvk) = D::wc_to_fvk(&self.key) else { - // skip scanning if wallet has not viewing capability + let Ok(fvk) = D::unified_key_store_to_fvk(self.key.unified_key_store()) else { + // skip scanning if wallet has no viewing capability return; }; let (ivk, ovk) = (fvk.derive_ivk::(), fvk.derive_ovk::()); diff --git a/zingolib/src/wallet/transaction_record.rs b/zingolib/src/wallet/transaction_record.rs index dc3986675..45b7f4821 100644 --- a/zingolib/src/wallet/transaction_record.rs +++ b/zingolib/src/wallet/transaction_record.rs @@ -517,8 +517,8 @@ impl TransactionRecord { writer.write_all(self.txid.as_ref())?; - zcash_encoding::Vector::write(&mut writer, &self.sapling_notes, |w, nd| nd.write(w))?; - zcash_encoding::Vector::write(&mut writer, &self.orchard_notes, |w, nd| nd.write(w))?; + zcash_encoding::Vector::write(&mut writer, &self.sapling_notes, |w, nd| nd.write(w, ()))?; + zcash_encoding::Vector::write(&mut writer, &self.orchard_notes, |w, nd| nd.write(w, ()))?; zcash_encoding::Vector::write(&mut writer, &self.transparent_outputs, |w, u| u.write(w))?; for pool in self.value_spent_by_pool() {