diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 5a2919ce5..4e2bac272 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -27,6 +27,8 @@ and this library adheres to Rust's notion of provides substantially greater flexibility in transaction creation. - `zcash_client_backend::address`: - `RecipientAddress::Unified` +- `zcash_client_backend::data_api`: + `WalletRead::get_unified_full_viewing_keys` - `zcash_client_backend::proto`: - `actions` field on `compact_formats::CompactTx` - `compact_formats::CompactOrchardAction` @@ -89,6 +91,9 @@ and this library adheres to Rust's notion of - An `Error::MemoForbidden` error has been added to the `data_api::error::Error` enum to report the condition where a memo was specified to be sent to a transparent recipient. +- `zcash_client_backend::decrypt`: + - `decrypt_transaction` now takes a `HashMap<_, UnifiedFullViewingKey>` + instead of `HashMap<_, ExtendedFullViewingKey>`. - If no memo is provided when sending to a shielded recipient, the empty memo will be used - `zcash_client_backend::keys::spending_key` has been moved to the @@ -104,6 +109,9 @@ and this library adheres to Rust's notion of - `Zip321Error::ParseError(String)` ### Removed +- `zcash_client_backend::data_api`: + - `WalletRead::get_extended_full_viewing_keys` (use + `WalletRead::get_unified_full_viewing_keys` instead). - The hardcoded `data_api::wallet::ANCHOR_OFFSET` constant. - `zcash_client_backend::wallet::AccountId` (moved to `zcash_primitives::zip32::AccountId`). diff --git a/zcash_client_backend/src/address.rs b/zcash_client_backend/src/address.rs index cfd8960f8..319c9a999 100644 --- a/zcash_client_backend/src/address.rs +++ b/zcash_client_backend/src/address.rs @@ -8,7 +8,7 @@ use zcash_address::{ }; use zcash_primitives::{consensus, constants, legacy::TransparentAddress, sapling::PaymentAddress}; -fn params_to_network(params: &P) -> Network { +pub(crate) fn params_to_network(params: &P) -> Network { // Use the Sapling HRP as an indicator of network. match params.hrp_sapling_payment_address() { constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS => Network::Main, @@ -108,6 +108,11 @@ impl UnifiedAddress { self.sapling.as_ref() } + /// Returns the transparent receiver within this Unified Address, if any. + pub fn transparent(&self) -> Option<&TransparentAddress> { + self.transparent.as_ref() + } + fn to_address(&self, net: Network) -> ZcashAddress { let ua = unified::Address::try_from_items( self.unknown @@ -137,6 +142,11 @@ impl UnifiedAddress { .expect("UnifiedAddress should only be constructed safely"); ZcashAddress::from_unified(net, ua) } + + /// Returns the string encoding of this `UnifiedAddress` for the given network. + pub fn encode(&self, params: &P) -> String { + self.to_address(params_to_network(params)).to_string() + } } /// An address that funds can be sent to. diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index aba7d2a91..f79a3d08f 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -17,6 +17,7 @@ use zcash_primitives::{ use crate::{ address::RecipientAddress, decrypt::DecryptedOutput, + keys::UnifiedFullViewingKey, proto::compact_formats::CompactBlock, wallet::{SpendableNote, WalletTx}, }; @@ -115,12 +116,13 @@ pub trait WalletRead { /// /// This will return `Ok(None)` if the account identifier does not correspond /// to a known account. + // TODO: This does not appear to be the case. fn get_address(&self, account: AccountId) -> Result, Self::Error>; - /// Returns all extended full viewing keys known about by this wallet. - fn get_extended_full_viewing_keys( + /// Returns all unified full viewing keys known to this wallet. + fn get_unified_full_viewing_keys( &self, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; /// Checks whether the specified extended full viewing key is /// associated with the account. @@ -329,6 +331,7 @@ pub mod testing { }; use crate::{ + keys::UnifiedFullViewingKey, proto::compact_formats::CompactBlock, wallet::{SpendableNote, WalletTransparentOutput}, }; @@ -385,9 +388,9 @@ pub mod testing { Ok(None) } - fn get_extended_full_viewing_keys( + fn get_unified_full_viewing_keys( &self, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(HashMap::new()) } diff --git a/zcash_client_backend/src/data_api/chain.rs b/zcash_client_backend/src/data_api/chain.rs index 076912737..7a7006c5f 100644 --- a/zcash_client_backend/src/data_api/chain.rs +++ b/zcash_client_backend/src/data_api/chain.rs @@ -84,7 +84,6 @@ use zcash_primitives::{ consensus::{self, BlockHeight, NetworkUpgrade}, merkle_tree::CommitmentTree, sapling::Nullifier, - zip32::{AccountId, ExtendedFullViewingKey}, }; use crate::{ @@ -210,9 +209,14 @@ where .unwrap_or(sapling_activation_height - 1) })?; - // Fetch the ExtendedFullViewingKeys we are tracking - let extfvks = data.get_extended_full_viewing_keys()?; - let extfvks: Vec<(&AccountId, &ExtendedFullViewingKey)> = extfvks.iter().collect(); + // Fetch the UnifiedFullViewingKeys we are tracking + let ufvks = data.get_unified_full_viewing_keys()?; + // TODO: Change `scan_block` to also scan Orchard. + // https://github.com/zcash/librustzcash/issues/403 + let dfvks: Vec<_> = ufvks + .iter() + .filter_map(|(account, ufvk)| ufvk.sapling().map(move |k| (account, k))) + .collect(); // Get the most recent CommitmentTree let mut tree = data @@ -244,7 +248,7 @@ where scan_block( params, block, - &extfvks, + &dfvks, &nullifiers, &mut tree, &mut witness_refs[..], diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index fee32c2dd..13a4f1b96 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -41,8 +41,8 @@ where P: consensus::Parameters, D: WalletWrite, { - // Fetch the ExtendedFullViewingKeys we are tracking - let extfvks = data.get_extended_full_viewing_keys()?; + // Fetch the UnifiedFullViewingKeys we are tracking + let ufvks = data.get_unified_full_viewing_keys()?; // Height is block height for mined transactions, and the "mempool height" (chain height + 1) // for mempool transactions. @@ -54,7 +54,7 @@ where .or_else(|| params.activation_height(NetworkUpgrade::Sapling)) .ok_or(Error::SaplingNotActive)?; - let sapling_outputs = decrypt_transaction(params, height, tx, &extfvks); + let sapling_outputs = decrypt_transaction(params, height, tx, &ufvks); if !(sapling_outputs.is_empty() && tx.transparent_bundle().iter().all(|b| b.vout.is_empty())) { data.store_decrypted_tx(&DecryptedTransaction { diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index 16e59ad93..29a666c83 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -8,9 +8,11 @@ use zcash_primitives::{ Note, PaymentAddress, }, transaction::Transaction, - zip32::{AccountId, ExtendedFullViewingKey}, + zip32::AccountId, }; +use crate::keys::UnifiedFullViewingKey; + /// A decrypted shielded output. pub struct DecryptedOutput { /// The index of the output within [`shielded_outputs`]. @@ -33,38 +35,41 @@ pub struct DecryptedOutput { } /// Scans a [`Transaction`] for any information that can be decrypted by the set of -/// [`ExtendedFullViewingKey`]s. +/// [`UnifiedFullViewingKey`]s. pub fn decrypt_transaction( params: &P, height: BlockHeight, tx: &Transaction, - extfvks: &HashMap, + ufvks: &HashMap, ) -> Vec { let mut decrypted = vec![]; if let Some(bundle) = tx.sapling_bundle() { - for (account, extfvk) in extfvks.iter() { - let ivk = extfvk.fvk.vk.ivk(); - let ovk = extfvk.fvk.ovk; + for (account, ufvk) in ufvks.iter() { + if let Some(dfvk) = ufvk.sapling() { + let ivk = dfvk.fvk().vk.ivk(); + let ovk = dfvk.fvk().ovk; - for (index, output) in bundle.shielded_outputs.iter().enumerate() { - let ((note, to, memo), outgoing) = - match try_sapling_note_decryption(params, height, &ivk, output) { - Some(ret) => (ret, false), - None => match try_sapling_output_recovery(params, height, &ovk, output) { - Some(ret) => (ret, true), - None => continue, - }, - }; + for (index, output) in bundle.shielded_outputs.iter().enumerate() { + let ((note, to, memo), outgoing) = + match try_sapling_note_decryption(params, height, &ivk, output) { + Some(ret) => (ret, false), + None => match try_sapling_output_recovery(params, height, &ovk, output) + { + Some(ret) => (ret, true), + None => continue, + }, + }; - decrypted.push(DecryptedOutput { - index, - note, - account: *account, - to, - memo, - outgoing, - }) + decrypted.push(DecryptedOutput { + index, + note, + account: *account, + to, + memo, + outgoing, + }) + } } } } diff --git a/zcash_client_backend/src/keys.rs b/zcash_client_backend/src/keys.rs index fa8e10f99..0515ff3e9 100644 --- a/zcash_client_backend/src/keys.rs +++ b/zcash_client_backend/src/keys.rs @@ -1,10 +1,12 @@ //! Helper functions for managing light client key material. +use zcash_address::unified::{self, Container, Encoding}; use zcash_primitives::{ consensus, + sapling::keys as sapling_keys, zip32::{AccountId, DiversifierIndex}, }; -use crate::address::UnifiedAddress; +use crate::address::{params_to_network, UnifiedAddress}; #[cfg(feature = "transparent-inputs")] use std::convert::TryInto; @@ -107,10 +109,11 @@ impl UnifiedSpendingKey { pub fn to_unified_full_viewing_key(&self) -> UnifiedFullViewingKey { UnifiedFullViewingKey { - account: self.account, #[cfg(feature = "transparent-inputs")] transparent: Some(self.transparent.to_account_pubkey()), - sapling: Some(sapling::ExtendedFullViewingKey::from(&self.sapling)), + sapling: Some(sapling::ExtendedFullViewingKey::from(&self.sapling).into()), + orchard: None, + unknown: vec![], } } @@ -137,38 +140,135 @@ impl UnifiedSpendingKey { #[derive(Clone, Debug)] #[doc(hidden)] pub struct UnifiedFullViewingKey { - account: AccountId, #[cfg(feature = "transparent-inputs")] transparent: Option, - // TODO: This type is invalid for a UFVK; create a `sapling::DiversifiableFullViewingKey` - // to replace it. - sapling: Option, + sapling: Option, + orchard: Option, + unknown: Vec<(u32, Vec)>, } #[doc(hidden)] impl UnifiedFullViewingKey { /// Construct a new unified full viewing key, if the required components are present. pub fn new( - account: AccountId, #[cfg(feature = "transparent-inputs")] transparent: Option, - sapling: Option, + sapling: Option, + orchard: Option, ) -> Option { if sapling.is_none() { None } else { Some(UnifiedFullViewingKey { - account, #[cfg(feature = "transparent-inputs")] transparent, sapling, + orchard, + // We don't allow constructing new UFVKs with unknown items, but we store + // this to allow parsing such UFVKs. + unknown: vec![], }) } } - /// Returns the ZIP32 account identifier to which all component - /// keys are related. - pub fn account(&self) -> AccountId { - self.account + /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding. + /// + /// [ZIP 316]: https://zips.z.cash/zip-0316 + pub fn decode(params: &P, encoding: &str) -> Result { + let (net, ufvk) = unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?; + let expected_net = params_to_network(params); + if net != expected_net { + return Err(format!( + "UFVK is for network {:?} but we expected {:?}", + net, expected_net, + )); + } + + let mut orchard = None; + let mut sapling = None; + #[cfg(feature = "transparent-inputs")] + let mut transparent = None; + + // We can use as-parsed order here for efficiency, because we're breaking out the + // receivers we support from the unknown receivers. + let unknown = ufvk + .items_as_parsed() + .iter() + .filter_map(|receiver| match receiver { + unified::Fvk::Orchard(data) => orchard::keys::FullViewingKey::from_bytes(data) + .ok_or("Invalid Orchard FVK in Unified FVK") + .map(|addr| { + orchard = Some(addr); + None + }) + .transpose(), + unified::Fvk::Sapling(data) => { + sapling_keys::DiversifiableFullViewingKey::from_bytes(data) + .ok_or("Invalid Sapling FVK in Unified FVK") + .map(|pa| { + sapling = Some(pa); + None + }) + .transpose() + } + #[cfg(feature = "transparent-inputs")] + unified::Fvk::P2pkh(data) => legacy::AccountPubKey::deserialize(data) + .map_err(|_| "Invalid transparent FVK in Unified FVK") + .map(|tfvk| { + transparent = Some(tfvk); + None + }) + .transpose(), + #[cfg(not(feature = "transparent-inputs"))] + unified::Fvk::P2pkh(data) => { + Some(Ok((unified::Typecode::P2pkh.into(), data.to_vec()))) + } + unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))), + }) + .collect::>()?; + + Ok(Self { + #[cfg(feature = "transparent-inputs")] + transparent, + sapling, + orchard, + unknown, + }) + } + + /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. + pub fn encode(&self, params: &P) -> String { + let items = std::iter::empty() + .chain( + self.orchard + .as_ref() + .map(|fvk| fvk.to_bytes()) + .map(unified::Fvk::Orchard), + ) + .chain( + self.sapling + .as_ref() + .map(|dfvk| dfvk.to_bytes()) + .map(unified::Fvk::Sapling), + ) + .chain( + self.unknown + .iter() + .map(|(typecode, data)| unified::Fvk::Unknown { + typecode: *typecode, + data: data.clone(), + }), + ); + #[cfg(feature = "transparent-inputs")] + let items = items.chain( + self.transparent + .as_ref() + .map(|tfvk| tfvk.serialize().try_into().unwrap()) + .map(unified::Fvk::P2pkh), + ); + + let ufvk = unified::Ufvk::try_from_items(items.collect()) + .expect("UnifiedFullViewingKey should only be constructed safely"); + ufvk.encode(¶ms_to_network(params)) } /// Returns the transparent component of the unified key at the @@ -178,9 +278,8 @@ impl UnifiedFullViewingKey { self.transparent.as_ref() } - /// Returns the Sapling extended full viewing key component of this - /// unified key. - pub fn sapling(&self) -> Option<&sapling::ExtendedFullViewingKey> { + /// Returns the Sapling diversifiable full viewing key component of this unified key. + pub fn sapling(&self) -> Option<&sapling_keys::DiversifiableFullViewingKey> { self.sapling.as_ref() } @@ -256,13 +355,16 @@ impl UnifiedFullViewingKey { #[cfg(test)] mod tests { - use super::sapling; - use zcash_primitives::zip32::AccountId; + use super::{sapling, UnifiedFullViewingKey}; + use zcash_primitives::{ + consensus::MAIN_NETWORK, + zip32::{AccountId, ExtendedFullViewingKey}, + }; #[cfg(feature = "transparent-inputs")] use { crate::encoding::AddressCodec, - zcash_primitives::{consensus::MAIN_NETWORK, legacy, legacy::keys::IncomingViewingKey}, + zcash_primitives::{legacy, legacy::keys::IncomingViewingKey}, }; #[cfg(feature = "transparent-inputs")] @@ -291,4 +393,46 @@ mod tests { .encode(&MAIN_NETWORK); assert_eq!(taddr, "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4".to_string()); } + + #[test] + fn ufvk_round_trip() { + let account = 0.into(); + + let orchard = { + let sk = orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, 0).unwrap(); + Some(orchard::keys::FullViewingKey::from(&sk)) + }; + + let sapling = { + let extsk = sapling::spending_key(&[0; 32], 0, account); + Some(ExtendedFullViewingKey::from(&extsk).into()) + }; + + #[cfg(feature = "transparent-inputs")] + let transparent = { None }; + + let ufvk = UnifiedFullViewingKey::new( + #[cfg(feature = "transparent-inputs")] + transparent, + sapling, + orchard, + ) + .unwrap(); + + let encoding = ufvk.encode(&MAIN_NETWORK); + let decoded = UnifiedFullViewingKey::decode(&MAIN_NETWORK, &encoding).unwrap(); + #[cfg(feature = "transparent-inputs")] + assert_eq!( + decoded.transparent.map(|t| t.serialize()), + ufvk.transparent.map(|t| t.serialize()), + ); + assert_eq!( + decoded.sapling.map(|s| s.to_bytes()), + ufvk.sapling.map(|s| s.to_bytes()), + ); + assert_eq!( + decoded.orchard.map(|o| o.to_bytes()), + ufvk.orchard.map(|o| o.to_bytes()), + ); + } } diff --git a/zcash_client_backend/src/welding_rig.rs b/zcash_client_backend/src/welding_rig.rs index 662dea3a1..2b651d680 100644 --- a/zcash_client_backend/src/welding_rig.rs +++ b/zcash_client_backend/src/welding_rig.rs @@ -9,6 +9,8 @@ use zcash_primitives::{ consensus::{self, BlockHeight}, merkle_tree::{CommitmentTree, IncrementalWitness}, sapling::{ + self, + keys::DiversifiableFullViewingKey, note_encryption::{try_sapling_compact_note_decryption, SaplingDomain}, Node, Note, Nullifier, PaymentAddress, SaplingIvk, }, @@ -127,6 +129,26 @@ pub trait ScanningKey { fn nf(&self, note: &Note, witness: &IncrementalWitness) -> Self::Nf; } +impl ScanningKey for DiversifiableFullViewingKey { + type Nf = sapling::Nullifier; + + fn try_decryption< + P: consensus::Parameters, + Output: ShieldedOutput, COMPACT_NOTE_SIZE>, + >( + &self, + params: &P, + height: BlockHeight, + output: &Output, + ) -> Option<(Note, PaymentAddress)> { + try_sapling_compact_note_decryption(params, height, &self.fvk().vk.ivk(), output) + } + + fn nf(&self, note: &Note, witness: &IncrementalWitness) -> Self::Nf { + note.nf(&self.fvk().vk, witness.position() as u64) + } +} + /// The [`ScanningKey`] implementation for [`ExtendedFullViewingKey`]s. /// Nullifiers may be derived when scanning with these keys. /// diff --git a/zcash_client_sqlite/CHANGELOG.md b/zcash_client_sqlite/CHANGELOG.md index 57854ea32..02168d2a7 100644 --- a/zcash_client_sqlite/CHANGELOG.md +++ b/zcash_client_sqlite/CHANGELOG.md @@ -18,6 +18,16 @@ and this library adheres to Rust's notion of rewinds exceed supported bounds. ### Changed +- Various **BREAKING CHANGES** have been made to the database tables. These will + require migrations, which may need to be performed in multiple steps. + - The `extfvk` column in the `accounts` table has been replaced by a `ufvk` + column. Values for this column should be derived from the wallet's seed and + the account number; the Sapling component of the resulting Unified Full + Viewing Key should match the old value in the `extfvk` column. + - A new non-null column, `output_pool` has been added to the `sent_notes` + table to enable distinguishing between Sapling and transparent outputs + (and in the future, outputs to other pools). Values for this column should + be assigned by inference from the address type in the stored data. - MSRV is now 1.56.1. - Bumped dependencies to `ff 0.12`, `group 0.12`, `jubjub 0.9`. - Renamed the following to use lower-case abbreviations (matching Rust @@ -38,11 +48,12 @@ and this library adheres to Rust's notion of method to be used in contexts where a transaction has just been constructed, rather than only in the case that a transaction has been decrypted after being retrieved from the network. -- A new non-null column, `output_pool` has been added to the `sent_notes` - table to enable distinguishing between Sapling and transparent outputs - (and in the future, outputs to other pools). This will require a migration, - which may need to be performed in multiple steps. Values for this column - should be assigned by inference from the address type in the stored data. + +### Removed +- `zcash_client_sqlite::wallet`: + - `get_extended_full_viewing_keys` (use + `zcash_client_backend::data_api::WalletRead::get_unified_full_viewing_keys` + instead). ### Deprecated - A number of public API methods that are used internally to support the diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index 66866f655..5902d9542 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -101,7 +101,7 @@ mod tests { init_wallet_db(&db_data).unwrap(); // Add an account to the wallet - let (extfvk, _taddr) = init_test_accounts_table(&db_data); + let (dfvk, _taddr) = init_test_accounts_table(&db_data); // Empty chain should be valid validate_chain( @@ -115,7 +115,7 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk.clone(), + &dfvk, Amount::from_u64(5).unwrap(), ); insert_into_cache(&db_cache, &cb); @@ -144,7 +144,7 @@ mod tests { let (cb2, _) = fake_compact_block( sapling_activation_height() + 1, cb.hash(), - extfvk, + &dfvk, Amount::from_u64(7).unwrap(), ); insert_into_cache(&db_cache, &cb2); @@ -180,19 +180,19 @@ mod tests { init_wallet_db(&db_data).unwrap(); // Add an account to the wallet - let (extfvk, _taddr) = init_test_accounts_table(&db_data); + let (dfvk, _taddr) = init_test_accounts_table(&db_data); // Create some fake CompactBlocks let (cb, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk.clone(), + &dfvk, Amount::from_u64(5).unwrap(), ); let (cb2, _) = fake_compact_block( sapling_activation_height() + 1, cb.hash(), - extfvk.clone(), + &dfvk, Amount::from_u64(7).unwrap(), ); insert_into_cache(&db_cache, &cb); @@ -214,13 +214,13 @@ mod tests { let (cb3, _) = fake_compact_block( sapling_activation_height() + 2, BlockHash([1; 32]), - extfvk.clone(), + &dfvk, Amount::from_u64(8).unwrap(), ); let (cb4, _) = fake_compact_block( sapling_activation_height() + 3, cb3.hash(), - extfvk, + &dfvk, Amount::from_u64(3).unwrap(), ); insert_into_cache(&db_cache, &cb3); @@ -250,19 +250,19 @@ mod tests { init_wallet_db(&db_data).unwrap(); // Add an account to the wallet - let (extfvk, _taddr) = init_test_accounts_table(&db_data); + let (dfvk, _taddr) = init_test_accounts_table(&db_data); // Create some fake CompactBlocks let (cb, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk.clone(), + &dfvk, Amount::from_u64(5).unwrap(), ); let (cb2, _) = fake_compact_block( sapling_activation_height() + 1, cb.hash(), - extfvk.clone(), + &dfvk, Amount::from_u64(7).unwrap(), ); insert_into_cache(&db_cache, &cb); @@ -284,13 +284,13 @@ mod tests { let (cb3, _) = fake_compact_block( sapling_activation_height() + 2, cb2.hash(), - extfvk.clone(), + &dfvk, Amount::from_u64(8).unwrap(), ); let (cb4, _) = fake_compact_block( sapling_activation_height() + 3, BlockHash([1; 32]), - extfvk, + &dfvk, Amount::from_u64(3).unwrap(), ); insert_into_cache(&db_cache, &cb3); @@ -320,7 +320,7 @@ mod tests { init_wallet_db(&db_data).unwrap(); // Add an account to the wallet - let (extfvk, _taddr) = init_test_accounts_table(&db_data); + let (dfvk, _taddr) = init_test_accounts_table(&db_data); // Account balance should be zero assert_eq!( @@ -334,12 +334,12 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk.clone(), + &dfvk, value, ); let (cb2, _) = - fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2); + fake_compact_block(sapling_activation_height() + 1, cb.hash(), &dfvk, value2); insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb2); @@ -389,14 +389,14 @@ mod tests { init_wallet_db(&db_data).unwrap(); // Add an account to the wallet - let (extfvk, _taddr) = init_test_accounts_table(&db_data); + let (dfvk, _taddr) = init_test_accounts_table(&db_data); // Create a block with height SAPLING_ACTIVATION_HEIGHT let value = Amount::from_u64(50000).unwrap(); let (cb1, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk.clone(), + &dfvk, value, ); insert_into_cache(&db_cache, &cb1); @@ -405,14 +405,10 @@ mod tests { assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value); // We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb1.hash(), - extfvk.clone(), - value, - ); + let (cb2, _) = + fake_compact_block(sapling_activation_height() + 1, cb1.hash(), &dfvk, value); let (cb3, _) = - fake_compact_block(sapling_activation_height() + 2, cb2.hash(), extfvk, value); + fake_compact_block(sapling_activation_height() + 2, cb2.hash(), &dfvk, value); insert_into_cache(&db_cache, &cb3); match scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None) { Err(SqliteClientError::BackendError(e)) => { @@ -448,7 +444,7 @@ mod tests { init_wallet_db(&db_data).unwrap(); // Add an account to the wallet - let (extfvk, _taddr) = init_test_accounts_table(&db_data); + let (dfvk, _taddr) = init_test_accounts_table(&db_data); // Account balance should be zero assert_eq!( @@ -461,7 +457,7 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk.clone(), + &dfvk, value, ); insert_into_cache(&db_cache, &cb); @@ -476,7 +472,7 @@ mod tests { // Create a second fake CompactBlock sending more value to the address let value2 = Amount::from_u64(7).unwrap(); let (cb2, _) = - fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2); + fake_compact_block(sapling_activation_height() + 1, cb.hash(), &dfvk, value2); insert_into_cache(&db_cache, &cb2); // Scan the cache again @@ -500,7 +496,7 @@ mod tests { init_wallet_db(&db_data).unwrap(); // Add an account to the wallet - let (extfvk, _taddr) = init_test_accounts_table(&db_data); + let (dfvk, _taddr) = init_test_accounts_table(&db_data); // Account balance should be zero assert_eq!( @@ -513,7 +509,7 @@ mod tests { let (cb, nf) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk.clone(), + &dfvk, value, ); insert_into_cache(&db_cache, &cb); @@ -535,7 +531,7 @@ mod tests { sapling_activation_height() + 1, cb.hash(), (nf, value), - extfvk, + &dfvk, to2, value2, ), diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 64a2752d9..f9f1fe7b4 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -53,7 +53,7 @@ use zcash_client_backend::{ data_api::{ BlockSource, DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead, WalletWrite, }, - encoding::encode_payment_address, + keys::UnifiedFullViewingKey, proto::compact_formats::CompactBlock, wallet::SpendableNote, }; @@ -225,11 +225,11 @@ impl WalletRead for WalletDb

{ wallet::get_tx_height(self, txid).map_err(SqliteClientError::from) } - fn get_extended_full_viewing_keys( + fn get_unified_full_viewing_keys( &self, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { #[allow(deprecated)] - wallet::get_extended_full_viewing_keys(self) + wallet::get_unified_full_viewing_keys(self) } fn get_address(&self, account: AccountId) -> Result, Self::Error> { @@ -381,10 +381,10 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> { self.wallet_db.get_tx_height(txid) } - fn get_extended_full_viewing_keys( + fn get_unified_full_viewing_keys( &self, - ) -> Result, Self::Error> { - self.wallet_db.get_extended_full_viewing_keys() + ) -> Result, Self::Error> { + self.wallet_db.get_unified_full_viewing_keys() } fn get_address(&self, account: AccountId) -> Result, Self::Error> { @@ -721,14 +721,6 @@ impl BlockSource for BlockDb { } } -fn address_from_extfvk( - params: &P, - extfvk: &ExtendedFullViewingKey, -) -> String { - let addr = extfvk.default_address().1; - encode_payment_address(params.hrp_sapling_payment_address(), &addr) -} - #[cfg(test)] mod tests { use ff::PrimeField; @@ -753,8 +745,8 @@ mod tests { legacy::TransparentAddress, memo::MemoBytes, sapling::{ - note_encryption::sapling_note_encryption, util::generate_random_rseed, Note, Nullifier, - PaymentAddress, + keys::DiversifiableFullViewingKey, note_encryption::sapling_note_encryption, + util::generate_random_rseed, Note, Nullifier, PaymentAddress, }, transaction::components::Amount, zip32::ExtendedFullViewingKey, @@ -791,11 +783,11 @@ mod tests { #[cfg(test)] pub(crate) fn init_test_accounts_table( db_data: &WalletDb, - ) -> (ExtendedFullViewingKey, Option) { + ) -> (DiversifiableFullViewingKey, Option) { let seed = [0u8; 32]; let account = AccountId::from(0); let extsk = sapling::spending_key(&seed, network().coin_type(), account); - let extfvk = ExtendedFullViewingKey::from(&extsk); + let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); #[cfg(feature = "transparent-inputs")] let (tkey, taddr) = { @@ -810,16 +802,16 @@ mod tests { let taddr = None; let ufvk = UnifiedFullViewingKey::new( - account, #[cfg(feature = "transparent-inputs")] tkey, - Some(extfvk.clone()), + Some(dfvk.clone()), + None, ) .unwrap(); init_accounts_table(db_data, &[ufvk]).unwrap(); - (extfvk, taddr) + (dfvk, taddr) } /// Create a fake CompactBlock at the given height, containing a single output paying @@ -827,10 +819,10 @@ mod tests { pub(crate) fn fake_compact_block( height: BlockHeight, prev_hash: BlockHash, - extfvk: ExtendedFullViewingKey, + dfvk: &DiversifiableFullViewingKey, value: Amount, ) -> (CompactBlock, Nullifier) { - let to = extfvk.default_address().1; + let to = dfvk.default_address().1; // Create a fake Note for the account let mut rng = OsRng; @@ -842,7 +834,7 @@ mod tests { rseed, }; let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), + Some(dfvk.fvk().ovk), note.clone(), to, MemoBytes::empty(), @@ -868,7 +860,7 @@ mod tests { rng.fill_bytes(&mut cb.hash); cb.prevHash.extend_from_slice(&prev_hash.0); cb.vtx.push(ctx); - (cb, note.nf(&extfvk.fvk.vk, 0)) + (cb, note.nf(&dfvk.fvk().vk, 0)) } /// Create a fake CompactBlock at the given height, spending a single note from the @@ -877,7 +869,7 @@ mod tests { height: BlockHeight, prev_hash: BlockHash, (nf, in_value): (Nullifier, Amount), - extfvk: ExtendedFullViewingKey, + dfvk: &DiversifiableFullViewingKey, to: PaymentAddress, value: Amount, ) -> CompactBlock { @@ -902,7 +894,7 @@ mod tests { rseed, }; let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), + Some(dfvk.fvk().ovk), note.clone(), to, MemoBytes::empty(), @@ -921,7 +913,7 @@ mod tests { // Create a fake Note for the change ctx.outputs.push({ - let change_addr = extfvk.default_address().1; + let change_addr = dfvk.default_address().1; let rseed = generate_random_rseed(&network(), height, &mut rng); let note = Note { g_d: change_addr.diversifier().g_d().unwrap(), @@ -930,7 +922,7 @@ mod tests { rseed, }; let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), + Some(dfvk.fvk().ovk), note.clone(), change_addr, MemoBytes::empty(), diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 8b17042ef..974f883fb 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -17,17 +17,16 @@ use zcash_primitives::{ consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, memo::{Memo, MemoBytes}, merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::{Node, Note, Nullifier, PaymentAddress}, + sapling::{keys::DiversifiableFullViewingKey, Node, Note, Nullifier, PaymentAddress}, transaction::{components::Amount, Transaction, TxId}, zip32::{AccountId, ExtendedFullViewingKey}, }; use zcash_client_backend::{ + address::RecipientAddress, data_api::error::Error, - encoding::{ - decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key, - encode_payment_address_p, encode_transparent_address_p, - }, + encoding::{encode_payment_address_p, encode_transparent_address_p}, + keys::UnifiedFullViewingKey, wallet::{WalletShieldedOutput, WalletTx}, DecryptedOutput, }; @@ -162,44 +161,42 @@ pub fn get_address( |row| row.get(0), )?; - decode_payment_address(wdb.params.hrp_sapling_payment_address(), &addr) - .map_err(SqliteClientError::Bech32) + RecipientAddress::decode(&wdb.params, &addr) + .ok_or_else(|| { + SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) + }) + .map(|addr| match addr { + // TODO: Return the UA, not its Sapling component. + RecipientAddress::Unified(ua) => ua.sapling().cloned(), + _ => None, + }) } -/// Returns the [`ExtendedFullViewingKey`]s for the wallet. -/// -/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey -#[deprecated( - note = "This function will be removed in a future release. Use zcash_client_backend::data_api::WalletRead::get_extended_full_viewing_keys instead." -)] -pub fn get_extended_full_viewing_keys( +/// Returns the [`UnifiedFullViewingKey`]s for the wallet. +pub(crate) fn get_unified_full_viewing_keys( wdb: &WalletDb

, -) -> Result, SqliteClientError> { - // Fetch the ExtendedFullViewingKeys we are tracking +) -> Result, SqliteClientError> { + // Fetch the UnifiedFullViewingKeys we are tracking let mut stmt_fetch_accounts = wdb .conn - .prepare("SELECT account, extfvk FROM accounts ORDER BY account ASC")?; + .prepare("SELECT account, ufvk FROM accounts ORDER BY account ASC")?; let rows = stmt_fetch_accounts .query_map(NO_PARAMS, |row| { let acct: u32 = row.get(0)?; - let extfvk = row.get(1).map(|extfvk: String| { - decode_extended_full_viewing_key( - wdb.params.hrp_sapling_extended_full_viewing_key(), - &extfvk, - ) - .map_err(SqliteClientError::Bech32) - .and_then(|k| k.ok_or(SqliteClientError::IncorrectHrpExtFvk)) - })?; + let account = AccountId::from(acct); + let ufvk_str: String = row.get(1)?; + let ufvk = UnifiedFullViewingKey::decode(&wdb.params, &ufvk_str) + .map_err(SqliteClientError::CorruptedData); - Ok((AccountId::from(acct), extfvk)) + Ok((account, ufvk)) }) .map_err(SqliteClientError::from)?; - let mut res: HashMap = HashMap::new(); + let mut res: HashMap = HashMap::new(); for row in rows { - let (account_id, efvkr) = row?; - res.insert(account_id, efvkr?); + let (account_id, ufvkr) = row?; + res.insert(account_id, ufvkr?); } Ok(res) @@ -218,16 +215,25 @@ pub fn is_valid_account_extfvk( extfvk: &ExtendedFullViewingKey, ) -> Result { wdb.conn - .prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")? - .exists(&[ - u32::from(account).to_sql()?, - encode_extended_full_viewing_key( - wdb.params.hrp_sapling_extended_full_viewing_key(), - extfvk, - ) - .to_sql()?, - ]) + .prepare("SELECT ufvk FROM accounts WHERE account = ?")? + .query_row(&[u32::from(account).to_sql()?], |row| { + row.get(0).map(|ufvk_str: String| { + UnifiedFullViewingKey::decode(&wdb.params, &ufvk_str) + .map_err(SqliteClientError::CorruptedData) + }) + }) + .optional() .map_err(SqliteClientError::from) + .and_then(|row| { + if let Some(ufvk) = row { + ufvk.map(|ufvk| { + ufvk.sapling().map(|dfvk| dfvk.to_bytes()) + == Some(DiversifiableFullViewingKey::from(extfvk.clone()).to_bytes()) + }) + } else { + Ok(false) + } + }) } /// Returns the balance for the account, including all mined unspent notes that we know diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index 7488bb16c..4a6cf8075 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -7,11 +7,9 @@ use zcash_primitives::{ consensus::{self, BlockHeight}, }; -use zcash_client_backend::{ - encoding::encode_extended_full_viewing_key, keys::UnifiedFullViewingKey, -}; +use zcash_client_backend::keys::UnifiedFullViewingKey; -use crate::{address_from_extfvk, error::SqliteClientError, WalletDb}; +use crate::{error::SqliteClientError, WalletDb}; #[cfg(feature = "transparent-inputs")] use { @@ -44,10 +42,12 @@ use { /// init_wallet_db(&db).unwrap(); /// ``` pub fn init_wallet_db

(wdb: &WalletDb

) -> Result<(), rusqlite::Error> { + // TODO: Add migrations (https://github.com/zcash/librustzcash/issues/489) + // - extfvk column -> ufvk column wdb.conn.execute( "CREATE TABLE IF NOT EXISTS accounts ( account INTEGER PRIMARY KEY, - extfvk TEXT, + ufvk TEXT, address TEXT, transparent_address TEXT )", @@ -179,8 +179,8 @@ pub fn init_wallet_db

(wdb: &WalletDb

) -> Result<(), rusqlite::Error> { /// let seed = [0u8; 32]; // insecure; replace with a strong random seed /// let account = AccountId::from(0); /// let extsk = sapling::spending_key(&seed, Network::TestNetwork.coin_type(), account); -/// let extfvk = ExtendedFullViewingKey::from(&extsk); -/// let ufvk = UnifiedFullViewingKey::new(account, None, Some(extfvk)).unwrap(); +/// let dfvk = ExtendedFullViewingKey::from(&extsk).into(); +/// let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap(); /// init_accounts_table(&db_data, &[ufvk]).unwrap(); /// # } /// ``` @@ -199,17 +199,9 @@ pub fn init_accounts_table( // Insert accounts atomically wdb.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?; - for key in keys.iter() { - let extfvk_str: Option = key.sapling().map(|extfvk| { - encode_extended_full_viewing_key( - wdb.params.hrp_sapling_extended_full_viewing_key(), - extfvk, - ) - }); - - let address_str: Option = key - .sapling() - .map(|extfvk| address_from_extfvk(&wdb.params, extfvk)); + for (account, key) in (0u32..).zip(keys) { + let ufvk_str: String = key.encode(&wdb.params); + let address_str: String = key.default_address().0.encode(&wdb.params); #[cfg(feature = "transparent-inputs")] let taddress_str: Option = key.transparent().and_then(|k| { k.derive_external_ivk() @@ -220,14 +212,9 @@ pub fn init_accounts_table( let taddress_str: Option = None; wdb.conn.execute( - "INSERT INTO accounts (account, extfvk, address, transparent_address) + "INSERT INTO accounts (account, ufvk, address, transparent_address) VALUES (?, ?, ?, ?)", - params![ - u32::from(key.account()), - extfvk_str, - address_str, - taddress_str, - ], + params![account, ufvk_str, address_str, taddress_str], )?; } wdb.conn.execute("COMMIT", NO_PARAMS)?; @@ -306,6 +293,7 @@ mod tests { use zcash_primitives::{ block::BlockHash, consensus::{BlockHeight, Parameters}, + sapling::keys::DiversifiableFullViewingKey, zip32::ExtendedFullViewingKey, }; @@ -332,22 +320,22 @@ mod tests { // First call with data should initialise the accounts table let extsk = sapling::spending_key(&seed, network().coin_type(), account); - let extfvk = ExtendedFullViewingKey::from(&extsk); + let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); #[cfg(feature = "transparent-inputs")] let ufvk = UnifiedFullViewingKey::new( - account, Some( transparent::AccountPrivKey::from_seed(&network(), &seed, account) .unwrap() .to_account_pubkey(), ), - Some(extfvk), + Some(dfvk), + None, ) .unwrap(); #[cfg(not(feature = "transparent-inputs"))] - let ufvk = UnifiedFullViewingKey::new(account, Some(extfvk)).unwrap(); + let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap(); init_accounts_table(&db_data, &[ufvk.clone()]).unwrap(); diff --git a/zcash_client_sqlite/src/wallet/transact.rs b/zcash_client_sqlite/src/wallet/transact.rs index c6ea61916..8b703dfc6 100644 --- a/zcash_client_sqlite/src/wallet/transact.rs +++ b/zcash_client_sqlite/src/wallet/transact.rs @@ -165,7 +165,10 @@ mod tests { block::BlockHash, consensus::{BlockHeight, BranchId, Parameters}, legacy::TransparentAddress, - sapling::{note_encryption::try_sapling_output_recovery, prover::TxProver}, + sapling::{ + keys::DiversifiableFullViewingKey, note_encryption::try_sapling_output_recovery, + prover::TxProver, + }, transaction::{components::Amount, Transaction}, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, }; @@ -207,8 +210,8 @@ mod tests { // Add two accounts to the wallet let extsk0 = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); let extsk1 = sapling::spending_key(&[1u8; 32], network().coin_type(), AccountId::from(1)); - let extfvk0 = ExtendedFullViewingKey::from(&extsk0); - let extfvk1 = ExtendedFullViewingKey::from(&extsk1); + let dfvk0 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk0)); + let dfvk1 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk1)); #[cfg(feature = "transparent-inputs")] let ufvks = { @@ -219,24 +222,16 @@ mod tests { transparent::AccountPrivKey::from_seed(&network(), &[1u8; 32], AccountId::from(1)) .unwrap(); [ - UnifiedFullViewingKey::new( - AccountId::from(0), - Some(tsk0.to_account_pubkey()), - Some(extfvk0), - ) - .unwrap(), - UnifiedFullViewingKey::new( - AccountId::from(1), - Some(tsk1.to_account_pubkey()), - Some(extfvk1), - ) - .unwrap(), + UnifiedFullViewingKey::new(Some(tsk0.to_account_pubkey()), Some(dfvk0), None) + .unwrap(), + UnifiedFullViewingKey::new(Some(tsk1.to_account_pubkey()), Some(dfvk1), None) + .unwrap(), ] }; #[cfg(not(feature = "transparent-inputs"))] let ufvks = [ - UnifiedFullViewingKey::new(AccountId::from(0), Some(extfvk0)).unwrap(), - UnifiedFullViewingKey::new(AccountId::from(1), Some(extfvk1)).unwrap(), + UnifiedFullViewingKey::new(Some(dfvk0), None).unwrap(), + UnifiedFullViewingKey::new(Some(dfvk1), None).unwrap(), ]; init_accounts_table(&db_data, &ufvks).unwrap(); @@ -285,12 +280,12 @@ mod tests { // Add an account to the wallet let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); - let extfvk = ExtendedFullViewingKey::from(&extsk); + let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); #[cfg(feature = "transparent-inputs")] - let ufvk = UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk)).unwrap(); + let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap(); #[cfg(not(feature = "transparent-inputs"))] - let ufvk = UnifiedFullViewingKey::new(AccountId::from(0), Some(extfvk)).unwrap(); + let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap(); init_accounts_table(&db_data, &[ufvk]).unwrap(); let to = extsk.default_address().1.into(); @@ -329,11 +324,11 @@ mod tests { // Add an account to the wallet let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); - let extfvk = ExtendedFullViewingKey::from(&extsk); + let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); #[cfg(feature = "transparent-inputs")] - let ufvk = UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk)).unwrap(); + let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap(); #[cfg(not(feature = "transparent-inputs"))] - let ufvk = UnifiedFullViewingKey::new(AccountId::from(0), Some(extfvk)).unwrap(); + let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap(); init_accounts_table(&db_data, &[ufvk]).unwrap(); let to = extsk.default_address().1.into(); @@ -377,12 +372,11 @@ mod tests { // Add an account to the wallet let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); - let extfvk = ExtendedFullViewingKey::from(&extsk); + let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); #[cfg(feature = "transparent-inputs")] - let ufvk = - UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk.clone())).unwrap(); + let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap(); #[cfg(not(feature = "transparent-inputs"))] - let ufvk = UnifiedFullViewingKey::new(AccountId::from(0), Some(extfvk.clone())).unwrap(); + let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap(); init_accounts_table(&db_data, &[ufvk]).unwrap(); // Add funds to the wallet in a single note @@ -390,7 +384,7 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk.clone(), + &dfvk, value, ); insert_into_cache(&db_cache, &cb); @@ -409,12 +403,7 @@ mod tests { ); // Add more funds to the wallet in a second note - let (cb, _) = fake_compact_block( - sapling_activation_height() + 1, - cb.hash(), - extfvk.clone(), - value, - ); + let (cb, _) = fake_compact_block(sapling_activation_height() + 1, cb.hash(), &dfvk, value); insert_into_cache(&db_cache, &cb); scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); @@ -457,12 +446,8 @@ mod tests { // Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second // note is verified for i in 2..10 { - let (cb, _) = fake_compact_block( - sapling_activation_height() + i, - cb.hash(), - extfvk.clone(), - value, - ); + let (cb, _) = + fake_compact_block(sapling_activation_height() + i, cb.hash(), &dfvk, value); insert_into_cache(&db_cache, &cb); } scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); @@ -488,8 +473,7 @@ mod tests { } // Mine block 11 so that the second note becomes verified - let (cb, _) = - fake_compact_block(sapling_activation_height() + 10, cb.hash(), extfvk, value); + let (cb, _) = fake_compact_block(sapling_activation_height() + 10, cb.hash(), &dfvk, value); insert_into_cache(&db_cache, &cb); scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); @@ -521,12 +505,11 @@ mod tests { // Add an account to the wallet let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); - let extfvk = ExtendedFullViewingKey::from(&extsk); + let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); #[cfg(feature = "transparent-inputs")] - let ufvk = - UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk.clone())).unwrap(); + let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap(); #[cfg(not(feature = "transparent-inputs"))] - let ufvk = UnifiedFullViewingKey::new(AccountId::from(0), Some(extfvk.clone())).unwrap(); + let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap(); init_accounts_table(&db_data, &[ufvk]).unwrap(); // Add funds to the wallet in a single note @@ -534,7 +517,7 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk, + &dfvk, value, ); insert_into_cache(&db_cache, &cb); @@ -585,7 +568,7 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height() + i, cb.hash(), - ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])), + &ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])).into(), value, ); insert_into_cache(&db_cache, &cb); @@ -616,7 +599,7 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height() + 22, cb.hash(), - ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])), + &ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])).into(), value, ); insert_into_cache(&db_cache, &cb); @@ -651,12 +634,11 @@ mod tests { // Add an account to the wallet let extsk = sapling::spending_key(&[0u8; 32], network.coin_type(), AccountId::from(0)); - let extfvk = ExtendedFullViewingKey::from(&extsk); + let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); #[cfg(feature = "transparent-inputs")] - let ufvk = - UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk.clone())).unwrap(); + let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap(); #[cfg(not(feature = "transparent-inputs"))] - let ufvk = UnifiedFullViewingKey::new(AccountId::from(0), Some(extfvk.clone())).unwrap(); + let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap(); init_accounts_table(&db_data, &[ufvk]).unwrap(); // Add funds to the wallet in a single note @@ -664,7 +646,7 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk.clone(), + &dfvk, value, ); insert_into_cache(&db_cache, &cb); @@ -721,7 +703,7 @@ mod tests { try_sapling_output_recovery( &network, sapling_activation_height(), - &extfvk.fvk.ovk, + &dfvk.fvk().ovk, output, ) }; @@ -738,7 +720,7 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height() + i, cb.hash(), - ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])), + &ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])).into(), value, ); insert_into_cache(&db_cache, &cb); @@ -762,12 +744,11 @@ mod tests { // Add an account to the wallet let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); - let extfvk = ExtendedFullViewingKey::from(&extsk); + let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); #[cfg(feature = "transparent-inputs")] - let ufvk = - UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk.clone())).unwrap(); + let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap(); #[cfg(not(feature = "transparent-inputs"))] - let ufvk = UnifiedFullViewingKey::new(AccountId::from(0), Some(extfvk.clone())).unwrap(); + let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap(); init_accounts_table(&db_data, &[ufvk]).unwrap(); // Add funds to the wallet in a single note @@ -775,7 +756,7 @@ mod tests { let (cb, _) = fake_compact_block( sapling_activation_height(), BlockHash([0; 32]), - extfvk, + &dfvk, value, ); insert_into_cache(&db_cache, &cb); diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md index f7970769a..8d6aaebd5 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -6,6 +6,9 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `zcash_primitives::sapling::keys::DiversifiableFullViewingKey` +- `zcash_primitives::sapling::keys::Scope` ## [0.7.0] - 2022-06-24 ### Changed diff --git a/zcash_primitives/src/sapling/keys.rs b/zcash_primitives/src/sapling/keys.rs index 80de1379f..822f3a438 100644 --- a/zcash_primitives/src/sapling/keys.rs +++ b/zcash_primitives/src/sapling/keys.rs @@ -4,16 +4,20 @@ //! //! [section 4.2.2]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents +use std::convert::TryInto; +use std::io::{self, Read, Write}; + use crate::{ constants::{PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR}, keys::{prf_expand, OutgoingViewingKey}, - sapling::{ProofGenerationKey, ViewingKey}, + zip32, }; use ff::PrimeField; use group::{Group, GroupEncoding}; -use std::io::{self, Read, Write}; use subtle::CtOption; +use super::{PaymentAddress, ProofGenerationKey, SaplingIvk, ViewingKey}; + /// A Sapling expanded spending key #[derive(Clone)] pub struct ExpandedSpendingKey { @@ -22,7 +26,7 @@ pub struct ExpandedSpendingKey { pub ovk: OutgoingViewingKey, } -/// A Sapling full viewing key +/// A Sapling key that provides the capability to view incoming and outgoing transactions. #[derive(Debug)] pub struct FullViewingKey { pub vk: ViewingKey, @@ -157,6 +161,158 @@ impl FullViewingKey { } } +/// The scope of a viewing key or address. +/// +/// A "scope" narrows the visibility or usage to a level below "full". +/// +/// Consistent usage of `Scope` enables the user to provide consistent views over a wallet +/// to other people. For example, a user can give an external [`SaplingIvk`] to a merchant +/// terminal, enabling it to only detect "real" transactions from customers and not +/// internal transactions from the wallet. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Scope { + /// A scope used for wallet-external operations, namely deriving addresses to give to + /// other users in order to receive funds. + External, + /// A scope used for wallet-internal operations, such as creating change notes, + /// auto-shielding, and note management. + Internal, +} + +/// A Sapling key that provides the capability to view incoming and outgoing transactions. +/// +/// This key is useful anywhere you need to maintain accurate balance, but do not want the +/// ability to spend funds (such as a view-only wallet). +/// +/// It comprises the subset of the ZIP 32 extended full viewing key that is used for the +/// Sapling item in a [ZIP 316 Unified Full Viewing Key][zip-0316-ufvk]. +/// +/// [zip-0316-ufvk]: https://zips.z.cash/zip-0316#encoding-of-unified-full-incoming-viewing-keys +#[derive(Clone, Debug)] +pub struct DiversifiableFullViewingKey { + fvk: FullViewingKey, + dk: zip32::DiversifierKey, +} + +impl From for DiversifiableFullViewingKey { + fn from(extfvk: zip32::ExtendedFullViewingKey) -> Self { + Self { + fvk: extfvk.fvk, + dk: extfvk.dk, + } + } +} + +impl DiversifiableFullViewingKey { + /// Parses a `DiversifiableFullViewingKey` from its raw byte encoding. + /// + /// Returns `None` if the bytes do not contain a valid encoding of a diversifiable + /// Sapling full viewing key. + pub fn from_bytes(bytes: &[u8; 128]) -> Option { + FullViewingKey::read(&bytes[..96]).ok().map(|fvk| Self { + fvk, + dk: zip32::DiversifierKey(bytes[96..].try_into().unwrap()), + }) + } + + /// Returns the raw encoding of this `DiversifiableFullViewingKey`. + pub fn to_bytes(&self) -> [u8; 128] { + let mut bytes = [0; 128]; + self.fvk + .write(&mut bytes[..96]) + .expect("slice should be the correct length"); + bytes[96..].copy_from_slice(&self.dk.0); + bytes + } + + /// Derives the internal `DiversifiableFullViewingKey` corresponding to `self` (which + /// is assumed here to be an external DFVK). + fn derive_internal(&self) -> Self { + let (fvk, dk) = zip32::sapling_derive_internal_fvk(&self.fvk, &self.dk); + Self { fvk, dk } + } + + /// Exposes the [`FullViewingKey`] component of this diversifiable full viewing key. + pub fn fvk(&self) -> &FullViewingKey { + &self.fvk + } + + /// Derives an incoming viewing key corresponding to this full viewing key. + pub fn to_ivk(&self, scope: Scope) -> SaplingIvk { + match scope { + Scope::External => self.fvk.vk.ivk(), + Scope::Internal => self.derive_internal().fvk.vk.ivk(), + } + } + + /// Derives an outgoing viewing key corresponding to this full viewing key. + pub fn to_ovk(&self, scope: Scope) -> OutgoingViewingKey { + match scope { + Scope::External => self.fvk.ovk, + Scope::Internal => self.derive_internal().fvk.ovk, + } + } + + /// Attempts to produce a valid payment address for the given diversifier index. + /// + /// Returns `None` if the diversifier index does not produce a valid diversifier for + /// this `DiversifiableFullViewingKey`. + pub fn address(&self, j: zip32::DiversifierIndex) -> Option { + zip32::sapling_address(&self.fvk, &self.dk, j) + } + + /// Finds the next valid payment address starting from the given diversifier index. + /// + /// This searches the diversifier space starting at `j` and incrementing, to find an + /// index which will produce a valid diversifier (a 50% probability for each index). + /// + /// Returns the index at which the valid diversifier was found along with the payment + /// address constructed using that diversifier, or `None` if the maximum index was + /// reached and no valid diversifier was found. + pub fn find_address( + &self, + j: zip32::DiversifierIndex, + ) -> Option<(zip32::DiversifierIndex, PaymentAddress)> { + zip32::sapling_find_address(&self.fvk, &self.dk, j) + } + + /// Returns the payment address corresponding to the smallest valid diversifier index, + /// along with that index. + // TODO: See if this is only used in tests. + pub fn default_address(&self) -> (zip32::DiversifierIndex, PaymentAddress) { + zip32::sapling_default_address(&self.fvk, &self.dk) + } + + /// Attempts to decrypt the given address's diversifier with this full viewing key. + /// + /// This method extracts the diversifier from the given address and decrypts it as a + /// diversifier index, then verifies that this diversifier index produces the same + /// address. Decryption is attempted using both the internal and external parts of the + /// full viewing key. + /// + /// Returns the decrypted diversifier index and its scope, or `None` if the address + /// was not generated from this key. + pub fn decrypt_diversifier( + &self, + addr: &PaymentAddress, + ) -> Option<(zip32::DiversifierIndex, Scope)> { + let j_external = self.dk.diversifier_index(addr.diversifier()); + if self.address(j_external).as_ref() == Some(addr) { + return Some((j_external, Scope::External)); + } + + let j_internal = self + .derive_internal() + .dk + .diversifier_index(addr.diversifier()); + if self.address(j_internal).as_ref() == Some(addr) { + return Some((j_internal, Scope::Internal)); + } + + None + } +} + #[cfg(any(test, feature = "test-dependencies"))] pub mod testing { use proptest::collection::vec; @@ -185,8 +341,8 @@ pub mod testing { mod tests { use group::{Group, GroupEncoding}; - use super::FullViewingKey; - use crate::constants::SPENDING_KEY_GENERATOR; + use super::{DiversifiableFullViewingKey, FullViewingKey}; + use crate::{constants::SPENDING_KEY_GENERATOR, zip32}; #[test] fn ak_must_be_prime_order() { @@ -210,4 +366,24 @@ mod tests { // nk is allowed to be the identity. assert!(FullViewingKey::read(&buf[..]).is_ok()); } + + #[test] + fn dfvk_round_trip() { + let dfvk = { + let extsk = zip32::ExtendedSpendingKey::master(&[]); + let extfvk = zip32::ExtendedFullViewingKey::from(&extsk); + DiversifiableFullViewingKey::from(extfvk) + }; + + // Check value -> bytes -> parsed round trip. + let dfvk_bytes = dfvk.to_bytes(); + let dfvk_parsed = DiversifiableFullViewingKey::from_bytes(&dfvk_bytes).unwrap(); + assert_eq!(dfvk_parsed.fvk.vk.ak, dfvk.fvk.vk.ak); + assert_eq!(dfvk_parsed.fvk.vk.nk, dfvk.fvk.vk.nk); + assert_eq!(dfvk_parsed.fvk.ovk, dfvk.fvk.ovk); + assert_eq!(dfvk_parsed.dk, dfvk.dk); + + // Check bytes -> parsed -> bytes round trip. + assert_eq!(dfvk_parsed.to_bytes(), dfvk_bytes); + } } diff --git a/zcash_primitives/src/zip32.rs b/zcash_primitives/src/zip32.rs index 21df6e3e3..b79f3bb07 100644 --- a/zcash_primitives/src/zip32.rs +++ b/zcash_primitives/src/zip32.rs @@ -316,7 +316,7 @@ pub struct ExtendedFullViewingKey { child_index: ChildIndex, chain_code: ChainCode, pub fvk: FullViewingKey, - dk: DiversifierKey, + pub(crate) dk: DiversifierKey, } impl std::cmp::PartialEq for ExtendedSpendingKey {