Merge pull request #570 from zcash/387-migrate-from-extfvk-to-ufvk

`zcash_client_*`: Migrate from `ExtendedFullViewingKey` to `UnifiedFullViewingKey`
This commit is contained in:
Kris Nuttycombe 2022-06-28 10:49:37 -06:00 committed by GitHub
commit 0142a3db1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 606 additions and 257 deletions

View File

@ -27,6 +27,8 @@ and this library adheres to Rust's notion of
provides substantially greater flexibility in transaction creation. provides substantially greater flexibility in transaction creation.
- `zcash_client_backend::address`: - `zcash_client_backend::address`:
- `RecipientAddress::Unified` - `RecipientAddress::Unified`
- `zcash_client_backend::data_api`:
`WalletRead::get_unified_full_viewing_keys`
- `zcash_client_backend::proto`: - `zcash_client_backend::proto`:
- `actions` field on `compact_formats::CompactTx` - `actions` field on `compact_formats::CompactTx`
- `compact_formats::CompactOrchardAction` - `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 - An `Error::MemoForbidden` error has been added to the
`data_api::error::Error` enum to report the condition where a memo was `data_api::error::Error` enum to report the condition where a memo was
specified to be sent to a transparent recipient. 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 - If no memo is provided when sending to a shielded recipient, the
empty memo will be used empty memo will be used
- `zcash_client_backend::keys::spending_key` has been moved to the - `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)` - `Zip321Error::ParseError(String)`
### Removed ### 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. - The hardcoded `data_api::wallet::ANCHOR_OFFSET` constant.
- `zcash_client_backend::wallet::AccountId` (moved to `zcash_primitives::zip32::AccountId`). - `zcash_client_backend::wallet::AccountId` (moved to `zcash_primitives::zip32::AccountId`).

View File

@ -8,7 +8,7 @@ use zcash_address::{
}; };
use zcash_primitives::{consensus, constants, legacy::TransparentAddress, sapling::PaymentAddress}; use zcash_primitives::{consensus, constants, legacy::TransparentAddress, sapling::PaymentAddress};
fn params_to_network<P: consensus::Parameters>(params: &P) -> Network { pub(crate) fn params_to_network<P: consensus::Parameters>(params: &P) -> Network {
// Use the Sapling HRP as an indicator of network. // Use the Sapling HRP as an indicator of network.
match params.hrp_sapling_payment_address() { match params.hrp_sapling_payment_address() {
constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS => Network::Main, constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS => Network::Main,
@ -108,6 +108,11 @@ impl UnifiedAddress {
self.sapling.as_ref() 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 { fn to_address(&self, net: Network) -> ZcashAddress {
let ua = unified::Address::try_from_items( let ua = unified::Address::try_from_items(
self.unknown self.unknown
@ -137,6 +142,11 @@ impl UnifiedAddress {
.expect("UnifiedAddress should only be constructed safely"); .expect("UnifiedAddress should only be constructed safely");
ZcashAddress::from_unified(net, ua) ZcashAddress::from_unified(net, ua)
} }
/// Returns the string encoding of this `UnifiedAddress` for the given network.
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
self.to_address(params_to_network(params)).to_string()
}
} }
/// An address that funds can be sent to. /// An address that funds can be sent to.

View File

@ -17,6 +17,7 @@ use zcash_primitives::{
use crate::{ use crate::{
address::RecipientAddress, address::RecipientAddress,
decrypt::DecryptedOutput, decrypt::DecryptedOutput,
keys::UnifiedFullViewingKey,
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
wallet::{SpendableNote, WalletTx}, wallet::{SpendableNote, WalletTx},
}; };
@ -115,12 +116,13 @@ pub trait WalletRead {
/// ///
/// This will return `Ok(None)` if the account identifier does not correspond /// This will return `Ok(None)` if the account identifier does not correspond
/// to a known account. /// to a known account.
// TODO: This does not appear to be the case.
fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error>; fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error>;
/// Returns all extended full viewing keys known about by this wallet. /// Returns all unified full viewing keys known to this wallet.
fn get_extended_full_viewing_keys( fn get_unified_full_viewing_keys(
&self, &self,
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, Self::Error>; ) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error>;
/// Checks whether the specified extended full viewing key is /// Checks whether the specified extended full viewing key is
/// associated with the account. /// associated with the account.
@ -329,6 +331,7 @@ pub mod testing {
}; };
use crate::{ use crate::{
keys::UnifiedFullViewingKey,
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
wallet::{SpendableNote, WalletTransparentOutput}, wallet::{SpendableNote, WalletTransparentOutput},
}; };
@ -385,9 +388,9 @@ pub mod testing {
Ok(None) Ok(None)
} }
fn get_extended_full_viewing_keys( fn get_unified_full_viewing_keys(
&self, &self,
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, Self::Error> { ) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
Ok(HashMap::new()) Ok(HashMap::new())
} }

View File

@ -84,7 +84,6 @@ use zcash_primitives::{
consensus::{self, BlockHeight, NetworkUpgrade}, consensus::{self, BlockHeight, NetworkUpgrade},
merkle_tree::CommitmentTree, merkle_tree::CommitmentTree,
sapling::Nullifier, sapling::Nullifier,
zip32::{AccountId, ExtendedFullViewingKey},
}; };
use crate::{ use crate::{
@ -210,9 +209,14 @@ where
.unwrap_or(sapling_activation_height - 1) .unwrap_or(sapling_activation_height - 1)
})?; })?;
// Fetch the ExtendedFullViewingKeys we are tracking // Fetch the UnifiedFullViewingKeys we are tracking
let extfvks = data.get_extended_full_viewing_keys()?; let ufvks = data.get_unified_full_viewing_keys()?;
let extfvks: Vec<(&AccountId, &ExtendedFullViewingKey)> = extfvks.iter().collect(); // 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 // Get the most recent CommitmentTree
let mut tree = data let mut tree = data
@ -244,7 +248,7 @@ where
scan_block( scan_block(
params, params,
block, block,
&extfvks, &dfvks,
&nullifiers, &nullifiers,
&mut tree, &mut tree,
&mut witness_refs[..], &mut witness_refs[..],

View File

@ -41,8 +41,8 @@ where
P: consensus::Parameters, P: consensus::Parameters,
D: WalletWrite<Error = E>, D: WalletWrite<Error = E>,
{ {
// Fetch the ExtendedFullViewingKeys we are tracking // Fetch the UnifiedFullViewingKeys we are tracking
let extfvks = data.get_extended_full_viewing_keys()?; let ufvks = data.get_unified_full_viewing_keys()?;
// Height is block height for mined transactions, and the "mempool height" (chain height + 1) // Height is block height for mined transactions, and the "mempool height" (chain height + 1)
// for mempool transactions. // for mempool transactions.
@ -54,7 +54,7 @@ where
.or_else(|| params.activation_height(NetworkUpgrade::Sapling)) .or_else(|| params.activation_height(NetworkUpgrade::Sapling))
.ok_or(Error::SaplingNotActive)?; .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())) { if !(sapling_outputs.is_empty() && tx.transparent_bundle().iter().all(|b| b.vout.is_empty())) {
data.store_decrypted_tx(&DecryptedTransaction { data.store_decrypted_tx(&DecryptedTransaction {

View File

@ -8,9 +8,11 @@ use zcash_primitives::{
Note, PaymentAddress, Note, PaymentAddress,
}, },
transaction::Transaction, transaction::Transaction,
zip32::{AccountId, ExtendedFullViewingKey}, zip32::AccountId,
}; };
use crate::keys::UnifiedFullViewingKey;
/// A decrypted shielded output. /// A decrypted shielded output.
pub struct DecryptedOutput { pub struct DecryptedOutput {
/// The index of the output within [`shielded_outputs`]. /// 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 /// Scans a [`Transaction`] for any information that can be decrypted by the set of
/// [`ExtendedFullViewingKey`]s. /// [`UnifiedFullViewingKey`]s.
pub fn decrypt_transaction<P: consensus::Parameters>( pub fn decrypt_transaction<P: consensus::Parameters>(
params: &P, params: &P,
height: BlockHeight, height: BlockHeight,
tx: &Transaction, tx: &Transaction,
extfvks: &HashMap<AccountId, ExtendedFullViewingKey>, ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
) -> Vec<DecryptedOutput> { ) -> Vec<DecryptedOutput> {
let mut decrypted = vec![]; let mut decrypted = vec![];
if let Some(bundle) = tx.sapling_bundle() { if let Some(bundle) = tx.sapling_bundle() {
for (account, extfvk) in extfvks.iter() { for (account, ufvk) in ufvks.iter() {
let ivk = extfvk.fvk.vk.ivk(); if let Some(dfvk) = ufvk.sapling() {
let ovk = extfvk.fvk.ovk; let ivk = dfvk.fvk().vk.ivk();
let ovk = dfvk.fvk().ovk;
for (index, output) in bundle.shielded_outputs.iter().enumerate() { for (index, output) in bundle.shielded_outputs.iter().enumerate() {
let ((note, to, memo), outgoing) = let ((note, to, memo), outgoing) =
match try_sapling_note_decryption(params, height, &ivk, output) { match try_sapling_note_decryption(params, height, &ivk, output) {
Some(ret) => (ret, false), Some(ret) => (ret, false),
None => match try_sapling_output_recovery(params, height, &ovk, output) { None => match try_sapling_output_recovery(params, height, &ovk, output)
Some(ret) => (ret, true), {
None => continue, Some(ret) => (ret, true),
}, None => continue,
}; },
};
decrypted.push(DecryptedOutput { decrypted.push(DecryptedOutput {
index, index,
note, note,
account: *account, account: *account,
to, to,
memo, memo,
outgoing, outgoing,
}) })
}
} }
} }
} }

View File

@ -1,10 +1,12 @@
//! Helper functions for managing light client key material. //! Helper functions for managing light client key material.
use zcash_address::unified::{self, Container, Encoding};
use zcash_primitives::{ use zcash_primitives::{
consensus, consensus,
sapling::keys as sapling_keys,
zip32::{AccountId, DiversifierIndex}, zip32::{AccountId, DiversifierIndex},
}; };
use crate::address::UnifiedAddress; use crate::address::{params_to_network, UnifiedAddress};
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use std::convert::TryInto; use std::convert::TryInto;
@ -107,10 +109,11 @@ impl UnifiedSpendingKey {
pub fn to_unified_full_viewing_key(&self) -> UnifiedFullViewingKey { pub fn to_unified_full_viewing_key(&self) -> UnifiedFullViewingKey {
UnifiedFullViewingKey { UnifiedFullViewingKey {
account: self.account,
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent: Some(self.transparent.to_account_pubkey()), 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)] #[derive(Clone, Debug)]
#[doc(hidden)] #[doc(hidden)]
pub struct UnifiedFullViewingKey { pub struct UnifiedFullViewingKey {
account: AccountId,
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent: Option<legacy::AccountPubKey>, transparent: Option<legacy::AccountPubKey>,
// TODO: This type is invalid for a UFVK; create a `sapling::DiversifiableFullViewingKey` sapling: Option<sapling_keys::DiversifiableFullViewingKey>,
// to replace it. orchard: Option<orchard::keys::FullViewingKey>,
sapling: Option<sapling::ExtendedFullViewingKey>, unknown: Vec<(u32, Vec<u8>)>,
} }
#[doc(hidden)] #[doc(hidden)]
impl UnifiedFullViewingKey { impl UnifiedFullViewingKey {
/// Construct a new unified full viewing key, if the required components are present. /// Construct a new unified full viewing key, if the required components are present.
pub fn new( pub fn new(
account: AccountId,
#[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>, #[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>,
sapling: Option<sapling::ExtendedFullViewingKey>, sapling: Option<sapling_keys::DiversifiableFullViewingKey>,
orchard: Option<orchard::keys::FullViewingKey>,
) -> Option<UnifiedFullViewingKey> { ) -> Option<UnifiedFullViewingKey> {
if sapling.is_none() { if sapling.is_none() {
None None
} else { } else {
Some(UnifiedFullViewingKey { Some(UnifiedFullViewingKey {
account,
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent, transparent,
sapling, 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 /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding.
/// keys are related. ///
pub fn account(&self) -> AccountId { /// [ZIP 316]: https://zips.z.cash/zip-0316
self.account pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> {
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::<Result<_, _>>()?;
Ok(Self {
#[cfg(feature = "transparent-inputs")]
transparent,
sapling,
orchard,
unknown,
})
}
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
pub fn encode<P: consensus::Parameters>(&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(&params_to_network(params))
} }
/// Returns the transparent component of the unified key at the /// Returns the transparent component of the unified key at the
@ -178,9 +278,8 @@ impl UnifiedFullViewingKey {
self.transparent.as_ref() self.transparent.as_ref()
} }
/// Returns the Sapling extended full viewing key component of this /// Returns the Sapling diversifiable full viewing key component of this unified key.
/// unified key. pub fn sapling(&self) -> Option<&sapling_keys::DiversifiableFullViewingKey> {
pub fn sapling(&self) -> Option<&sapling::ExtendedFullViewingKey> {
self.sapling.as_ref() self.sapling.as_ref()
} }
@ -256,13 +355,16 @@ impl UnifiedFullViewingKey {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::sapling; use super::{sapling, UnifiedFullViewingKey};
use zcash_primitives::zip32::AccountId; use zcash_primitives::{
consensus::MAIN_NETWORK,
zip32::{AccountId, ExtendedFullViewingKey},
};
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
crate::encoding::AddressCodec, crate::encoding::AddressCodec,
zcash_primitives::{consensus::MAIN_NETWORK, legacy, legacy::keys::IncomingViewingKey}, zcash_primitives::{legacy, legacy::keys::IncomingViewingKey},
}; };
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -291,4 +393,46 @@ mod tests {
.encode(&MAIN_NETWORK); .encode(&MAIN_NETWORK);
assert_eq!(taddr, "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4".to_string()); 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()),
);
}
} }

View File

@ -9,6 +9,8 @@ use zcash_primitives::{
consensus::{self, BlockHeight}, consensus::{self, BlockHeight},
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{ sapling::{
self,
keys::DiversifiableFullViewingKey,
note_encryption::{try_sapling_compact_note_decryption, SaplingDomain}, note_encryption::{try_sapling_compact_note_decryption, SaplingDomain},
Node, Note, Nullifier, PaymentAddress, SaplingIvk, Node, Note, Nullifier, PaymentAddress, SaplingIvk,
}, },
@ -127,6 +129,26 @@ pub trait ScanningKey {
fn nf(&self, note: &Note, witness: &IncrementalWitness<Node>) -> Self::Nf; fn nf(&self, note: &Note, witness: &IncrementalWitness<Node>) -> Self::Nf;
} }
impl ScanningKey for DiversifiableFullViewingKey {
type Nf = sapling::Nullifier;
fn try_decryption<
P: consensus::Parameters,
Output: ShieldedOutput<SaplingDomain<P>, 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<Node>) -> Self::Nf {
note.nf(&self.fvk().vk, witness.position() as u64)
}
}
/// The [`ScanningKey`] implementation for [`ExtendedFullViewingKey`]s. /// The [`ScanningKey`] implementation for [`ExtendedFullViewingKey`]s.
/// Nullifiers may be derived when scanning with these keys. /// Nullifiers may be derived when scanning with these keys.
/// ///

View File

@ -18,6 +18,16 @@ and this library adheres to Rust's notion of
rewinds exceed supported bounds. rewinds exceed supported bounds.
### Changed ### 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. - MSRV is now 1.56.1.
- Bumped dependencies to `ff 0.12`, `group 0.12`, `jubjub 0.9`. - Bumped dependencies to `ff 0.12`, `group 0.12`, `jubjub 0.9`.
- Renamed the following to use lower-case abbreviations (matching Rust - 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 method to be used in contexts where a transaction has just been
constructed, rather than only in the case that a transaction has constructed, rather than only in the case that a transaction has
been decrypted after being retrieved from the network. 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 ### Removed
(and in the future, outputs to other pools). This will require a migration, - `zcash_client_sqlite::wallet`:
which may need to be performed in multiple steps. Values for this column - `get_extended_full_viewing_keys` (use
should be assigned by inference from the address type in the stored data. `zcash_client_backend::data_api::WalletRead::get_unified_full_viewing_keys`
instead).
### Deprecated ### Deprecated
- A number of public API methods that are used internally to support the - A number of public API methods that are used internally to support the

View File

@ -101,7 +101,7 @@ mod tests {
init_wallet_db(&db_data).unwrap(); init_wallet_db(&db_data).unwrap();
// Add an account to the wallet // 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 // Empty chain should be valid
validate_chain( validate_chain(
@ -115,7 +115,7 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk.clone(), &dfvk,
Amount::from_u64(5).unwrap(), Amount::from_u64(5).unwrap(),
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -144,7 +144,7 @@ mod tests {
let (cb2, _) = fake_compact_block( let (cb2, _) = fake_compact_block(
sapling_activation_height() + 1, sapling_activation_height() + 1,
cb.hash(), cb.hash(),
extfvk, &dfvk,
Amount::from_u64(7).unwrap(), Amount::from_u64(7).unwrap(),
); );
insert_into_cache(&db_cache, &cb2); insert_into_cache(&db_cache, &cb2);
@ -180,19 +180,19 @@ mod tests {
init_wallet_db(&db_data).unwrap(); init_wallet_db(&db_data).unwrap();
// Add an account to the wallet // 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 // Create some fake CompactBlocks
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk.clone(), &dfvk,
Amount::from_u64(5).unwrap(), Amount::from_u64(5).unwrap(),
); );
let (cb2, _) = fake_compact_block( let (cb2, _) = fake_compact_block(
sapling_activation_height() + 1, sapling_activation_height() + 1,
cb.hash(), cb.hash(),
extfvk.clone(), &dfvk,
Amount::from_u64(7).unwrap(), Amount::from_u64(7).unwrap(),
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -214,13 +214,13 @@ mod tests {
let (cb3, _) = fake_compact_block( let (cb3, _) = fake_compact_block(
sapling_activation_height() + 2, sapling_activation_height() + 2,
BlockHash([1; 32]), BlockHash([1; 32]),
extfvk.clone(), &dfvk,
Amount::from_u64(8).unwrap(), Amount::from_u64(8).unwrap(),
); );
let (cb4, _) = fake_compact_block( let (cb4, _) = fake_compact_block(
sapling_activation_height() + 3, sapling_activation_height() + 3,
cb3.hash(), cb3.hash(),
extfvk, &dfvk,
Amount::from_u64(3).unwrap(), Amount::from_u64(3).unwrap(),
); );
insert_into_cache(&db_cache, &cb3); insert_into_cache(&db_cache, &cb3);
@ -250,19 +250,19 @@ mod tests {
init_wallet_db(&db_data).unwrap(); init_wallet_db(&db_data).unwrap();
// Add an account to the wallet // 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 // Create some fake CompactBlocks
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk.clone(), &dfvk,
Amount::from_u64(5).unwrap(), Amount::from_u64(5).unwrap(),
); );
let (cb2, _) = fake_compact_block( let (cb2, _) = fake_compact_block(
sapling_activation_height() + 1, sapling_activation_height() + 1,
cb.hash(), cb.hash(),
extfvk.clone(), &dfvk,
Amount::from_u64(7).unwrap(), Amount::from_u64(7).unwrap(),
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -284,13 +284,13 @@ mod tests {
let (cb3, _) = fake_compact_block( let (cb3, _) = fake_compact_block(
sapling_activation_height() + 2, sapling_activation_height() + 2,
cb2.hash(), cb2.hash(),
extfvk.clone(), &dfvk,
Amount::from_u64(8).unwrap(), Amount::from_u64(8).unwrap(),
); );
let (cb4, _) = fake_compact_block( let (cb4, _) = fake_compact_block(
sapling_activation_height() + 3, sapling_activation_height() + 3,
BlockHash([1; 32]), BlockHash([1; 32]),
extfvk, &dfvk,
Amount::from_u64(3).unwrap(), Amount::from_u64(3).unwrap(),
); );
insert_into_cache(&db_cache, &cb3); insert_into_cache(&db_cache, &cb3);
@ -320,7 +320,7 @@ mod tests {
init_wallet_db(&db_data).unwrap(); init_wallet_db(&db_data).unwrap();
// Add an account to the wallet // 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 // Account balance should be zero
assert_eq!( assert_eq!(
@ -334,12 +334,12 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk.clone(), &dfvk,
value, value,
); );
let (cb2, _) = 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, &cb);
insert_into_cache(&db_cache, &cb2); insert_into_cache(&db_cache, &cb2);
@ -389,14 +389,14 @@ mod tests {
init_wallet_db(&db_data).unwrap(); init_wallet_db(&db_data).unwrap();
// Add an account to the wallet // 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 // Create a block with height SAPLING_ACTIVATION_HEIGHT
let value = Amount::from_u64(50000).unwrap(); let value = Amount::from_u64(50000).unwrap();
let (cb1, _) = fake_compact_block( let (cb1, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk.clone(), &dfvk,
value, value,
); );
insert_into_cache(&db_cache, &cb1); insert_into_cache(&db_cache, &cb1);
@ -405,14 +405,10 @@ mod tests {
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value); assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
// We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next // We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next
let (cb2, _) = fake_compact_block( let (cb2, _) =
sapling_activation_height() + 1, fake_compact_block(sapling_activation_height() + 1, cb1.hash(), &dfvk, value);
cb1.hash(),
extfvk.clone(),
value,
);
let (cb3, _) = 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); insert_into_cache(&db_cache, &cb3);
match scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None) { match scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None) {
Err(SqliteClientError::BackendError(e)) => { Err(SqliteClientError::BackendError(e)) => {
@ -448,7 +444,7 @@ mod tests {
init_wallet_db(&db_data).unwrap(); init_wallet_db(&db_data).unwrap();
// Add an account to the wallet // 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 // Account balance should be zero
assert_eq!( assert_eq!(
@ -461,7 +457,7 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk.clone(), &dfvk,
value, value,
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -476,7 +472,7 @@ mod tests {
// Create a second fake CompactBlock sending more value to the address // Create a second fake CompactBlock sending more value to the address
let value2 = Amount::from_u64(7).unwrap(); let value2 = Amount::from_u64(7).unwrap();
let (cb2, _) = 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); insert_into_cache(&db_cache, &cb2);
// Scan the cache again // Scan the cache again
@ -500,7 +496,7 @@ mod tests {
init_wallet_db(&db_data).unwrap(); init_wallet_db(&db_data).unwrap();
// Add an account to the wallet // 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 // Account balance should be zero
assert_eq!( assert_eq!(
@ -513,7 +509,7 @@ mod tests {
let (cb, nf) = fake_compact_block( let (cb, nf) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk.clone(), &dfvk,
value, value,
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -535,7 +531,7 @@ mod tests {
sapling_activation_height() + 1, sapling_activation_height() + 1,
cb.hash(), cb.hash(),
(nf, value), (nf, value),
extfvk, &dfvk,
to2, to2,
value2, value2,
), ),

View File

@ -53,7 +53,7 @@ use zcash_client_backend::{
data_api::{ data_api::{
BlockSource, DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead, WalletWrite, BlockSource, DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead, WalletWrite,
}, },
encoding::encode_payment_address, keys::UnifiedFullViewingKey,
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
wallet::SpendableNote, wallet::SpendableNote,
}; };
@ -225,11 +225,11 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
wallet::get_tx_height(self, txid).map_err(SqliteClientError::from) wallet::get_tx_height(self, txid).map_err(SqliteClientError::from)
} }
fn get_extended_full_viewing_keys( fn get_unified_full_viewing_keys(
&self, &self,
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, Self::Error> { ) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
#[allow(deprecated)] #[allow(deprecated)]
wallet::get_extended_full_viewing_keys(self) wallet::get_unified_full_viewing_keys(self)
} }
fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error> { fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error> {
@ -381,10 +381,10 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
self.wallet_db.get_tx_height(txid) self.wallet_db.get_tx_height(txid)
} }
fn get_extended_full_viewing_keys( fn get_unified_full_viewing_keys(
&self, &self,
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, Self::Error> { ) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
self.wallet_db.get_extended_full_viewing_keys() self.wallet_db.get_unified_full_viewing_keys()
} }
fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error> { fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error> {
@ -721,14 +721,6 @@ impl BlockSource for BlockDb {
} }
} }
fn address_from_extfvk<P: consensus::Parameters>(
params: &P,
extfvk: &ExtendedFullViewingKey,
) -> String {
let addr = extfvk.default_address().1;
encode_payment_address(params.hrp_sapling_payment_address(), &addr)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use ff::PrimeField; use ff::PrimeField;
@ -753,8 +745,8 @@ mod tests {
legacy::TransparentAddress, legacy::TransparentAddress,
memo::MemoBytes, memo::MemoBytes,
sapling::{ sapling::{
note_encryption::sapling_note_encryption, util::generate_random_rseed, Note, Nullifier, keys::DiversifiableFullViewingKey, note_encryption::sapling_note_encryption,
PaymentAddress, util::generate_random_rseed, Note, Nullifier, PaymentAddress,
}, },
transaction::components::Amount, transaction::components::Amount,
zip32::ExtendedFullViewingKey, zip32::ExtendedFullViewingKey,
@ -791,11 +783,11 @@ mod tests {
#[cfg(test)] #[cfg(test)]
pub(crate) fn init_test_accounts_table( pub(crate) fn init_test_accounts_table(
db_data: &WalletDb<Network>, db_data: &WalletDb<Network>,
) -> (ExtendedFullViewingKey, Option<TransparentAddress>) { ) -> (DiversifiableFullViewingKey, Option<TransparentAddress>) {
let seed = [0u8; 32]; let seed = [0u8; 32];
let account = AccountId::from(0); let account = AccountId::from(0);
let extsk = sapling::spending_key(&seed, network().coin_type(), account); 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")] #[cfg(feature = "transparent-inputs")]
let (tkey, taddr) = { let (tkey, taddr) = {
@ -810,16 +802,16 @@ mod tests {
let taddr = None; let taddr = None;
let ufvk = UnifiedFullViewingKey::new( let ufvk = UnifiedFullViewingKey::new(
account,
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
tkey, tkey,
Some(extfvk.clone()), Some(dfvk.clone()),
None,
) )
.unwrap(); .unwrap();
init_accounts_table(db_data, &[ufvk]).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 /// 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( pub(crate) fn fake_compact_block(
height: BlockHeight, height: BlockHeight,
prev_hash: BlockHash, prev_hash: BlockHash,
extfvk: ExtendedFullViewingKey, dfvk: &DiversifiableFullViewingKey,
value: Amount, value: Amount,
) -> (CompactBlock, Nullifier) { ) -> (CompactBlock, Nullifier) {
let to = extfvk.default_address().1; let to = dfvk.default_address().1;
// Create a fake Note for the account // Create a fake Note for the account
let mut rng = OsRng; let mut rng = OsRng;
@ -842,7 +834,7 @@ mod tests {
rseed, rseed,
}; };
let encryptor = sapling_note_encryption::<_, Network>( let encryptor = sapling_note_encryption::<_, Network>(
Some(extfvk.fvk.ovk), Some(dfvk.fvk().ovk),
note.clone(), note.clone(),
to, to,
MemoBytes::empty(), MemoBytes::empty(),
@ -868,7 +860,7 @@ mod tests {
rng.fill_bytes(&mut cb.hash); rng.fill_bytes(&mut cb.hash);
cb.prevHash.extend_from_slice(&prev_hash.0); cb.prevHash.extend_from_slice(&prev_hash.0);
cb.vtx.push(ctx); 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 /// Create a fake CompactBlock at the given height, spending a single note from the
@ -877,7 +869,7 @@ mod tests {
height: BlockHeight, height: BlockHeight,
prev_hash: BlockHash, prev_hash: BlockHash,
(nf, in_value): (Nullifier, Amount), (nf, in_value): (Nullifier, Amount),
extfvk: ExtendedFullViewingKey, dfvk: &DiversifiableFullViewingKey,
to: PaymentAddress, to: PaymentAddress,
value: Amount, value: Amount,
) -> CompactBlock { ) -> CompactBlock {
@ -902,7 +894,7 @@ mod tests {
rseed, rseed,
}; };
let encryptor = sapling_note_encryption::<_, Network>( let encryptor = sapling_note_encryption::<_, Network>(
Some(extfvk.fvk.ovk), Some(dfvk.fvk().ovk),
note.clone(), note.clone(),
to, to,
MemoBytes::empty(), MemoBytes::empty(),
@ -921,7 +913,7 @@ mod tests {
// Create a fake Note for the change // Create a fake Note for the change
ctx.outputs.push({ 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 rseed = generate_random_rseed(&network(), height, &mut rng);
let note = Note { let note = Note {
g_d: change_addr.diversifier().g_d().unwrap(), g_d: change_addr.diversifier().g_d().unwrap(),
@ -930,7 +922,7 @@ mod tests {
rseed, rseed,
}; };
let encryptor = sapling_note_encryption::<_, Network>( let encryptor = sapling_note_encryption::<_, Network>(
Some(extfvk.fvk.ovk), Some(dfvk.fvk().ovk),
note.clone(), note.clone(),
change_addr, change_addr,
MemoBytes::empty(), MemoBytes::empty(),

View File

@ -17,17 +17,16 @@ use zcash_primitives::{
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
memo::{Memo, MemoBytes}, memo::{Memo, MemoBytes},
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{Node, Note, Nullifier, PaymentAddress}, sapling::{keys::DiversifiableFullViewingKey, Node, Note, Nullifier, PaymentAddress},
transaction::{components::Amount, Transaction, TxId}, transaction::{components::Amount, Transaction, TxId},
zip32::{AccountId, ExtendedFullViewingKey}, zip32::{AccountId, ExtendedFullViewingKey},
}; };
use zcash_client_backend::{ use zcash_client_backend::{
address::RecipientAddress,
data_api::error::Error, data_api::error::Error,
encoding::{ encoding::{encode_payment_address_p, encode_transparent_address_p},
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key, keys::UnifiedFullViewingKey,
encode_payment_address_p, encode_transparent_address_p,
},
wallet::{WalletShieldedOutput, WalletTx}, wallet::{WalletShieldedOutput, WalletTx},
DecryptedOutput, DecryptedOutput,
}; };
@ -162,44 +161,42 @@ pub fn get_address<P: consensus::Parameters>(
|row| row.get(0), |row| row.get(0),
)?; )?;
decode_payment_address(wdb.params.hrp_sapling_payment_address(), &addr) RecipientAddress::decode(&wdb.params, &addr)
.map_err(SqliteClientError::Bech32) .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. /// Returns the [`UnifiedFullViewingKey`]s for the wallet.
/// pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>(
/// [`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<P: consensus::Parameters>(
wdb: &WalletDb<P>, wdb: &WalletDb<P>,
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, SqliteClientError> { ) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, SqliteClientError> {
// Fetch the ExtendedFullViewingKeys we are tracking // Fetch the UnifiedFullViewingKeys we are tracking
let mut stmt_fetch_accounts = wdb let mut stmt_fetch_accounts = wdb
.conn .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 let rows = stmt_fetch_accounts
.query_map(NO_PARAMS, |row| { .query_map(NO_PARAMS, |row| {
let acct: u32 = row.get(0)?; let acct: u32 = row.get(0)?;
let extfvk = row.get(1).map(|extfvk: String| { let account = AccountId::from(acct);
decode_extended_full_viewing_key( let ufvk_str: String = row.get(1)?;
wdb.params.hrp_sapling_extended_full_viewing_key(), let ufvk = UnifiedFullViewingKey::decode(&wdb.params, &ufvk_str)
&extfvk, .map_err(SqliteClientError::CorruptedData);
)
.map_err(SqliteClientError::Bech32)
.and_then(|k| k.ok_or(SqliteClientError::IncorrectHrpExtFvk))
})?;
Ok((AccountId::from(acct), extfvk)) Ok((account, ufvk))
}) })
.map_err(SqliteClientError::from)?; .map_err(SqliteClientError::from)?;
let mut res: HashMap<AccountId, ExtendedFullViewingKey> = HashMap::new(); let mut res: HashMap<AccountId, UnifiedFullViewingKey> = HashMap::new();
for row in rows { for row in rows {
let (account_id, efvkr) = row?; let (account_id, ufvkr) = row?;
res.insert(account_id, efvkr?); res.insert(account_id, ufvkr?);
} }
Ok(res) Ok(res)
@ -218,16 +215,25 @@ pub fn is_valid_account_extfvk<P: consensus::Parameters>(
extfvk: &ExtendedFullViewingKey, extfvk: &ExtendedFullViewingKey,
) -> Result<bool, SqliteClientError> { ) -> Result<bool, SqliteClientError> {
wdb.conn wdb.conn
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")? .prepare("SELECT ufvk FROM accounts WHERE account = ?")?
.exists(&[ .query_row(&[u32::from(account).to_sql()?], |row| {
u32::from(account).to_sql()?, row.get(0).map(|ufvk_str: String| {
encode_extended_full_viewing_key( UnifiedFullViewingKey::decode(&wdb.params, &ufvk_str)
wdb.params.hrp_sapling_extended_full_viewing_key(), .map_err(SqliteClientError::CorruptedData)
extfvk, })
) })
.to_sql()?, .optional()
])
.map_err(SqliteClientError::from) .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 /// Returns the balance for the account, including all mined unspent notes that we know

View File

@ -7,11 +7,9 @@ use zcash_primitives::{
consensus::{self, BlockHeight}, consensus::{self, BlockHeight},
}; };
use zcash_client_backend::{ use zcash_client_backend::keys::UnifiedFullViewingKey;
encoding::encode_extended_full_viewing_key, keys::UnifiedFullViewingKey,
};
use crate::{address_from_extfvk, error::SqliteClientError, WalletDb}; use crate::{error::SqliteClientError, WalletDb};
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
@ -44,10 +42,12 @@ use {
/// init_wallet_db(&db).unwrap(); /// init_wallet_db(&db).unwrap();
/// ``` /// ```
pub fn init_wallet_db<P>(wdb: &WalletDb<P>) -> Result<(), rusqlite::Error> { pub fn init_wallet_db<P>(wdb: &WalletDb<P>) -> Result<(), rusqlite::Error> {
// TODO: Add migrations (https://github.com/zcash/librustzcash/issues/489)
// - extfvk column -> ufvk column
wdb.conn.execute( wdb.conn.execute(
"CREATE TABLE IF NOT EXISTS accounts ( "CREATE TABLE IF NOT EXISTS accounts (
account INTEGER PRIMARY KEY, account INTEGER PRIMARY KEY,
extfvk TEXT, ufvk TEXT,
address TEXT, address TEXT,
transparent_address TEXT transparent_address TEXT
)", )",
@ -179,8 +179,8 @@ pub fn init_wallet_db<P>(wdb: &WalletDb<P>) -> Result<(), rusqlite::Error> {
/// let seed = [0u8; 32]; // insecure; replace with a strong random seed /// let seed = [0u8; 32]; // insecure; replace with a strong random seed
/// let account = AccountId::from(0); /// let account = AccountId::from(0);
/// let extsk = sapling::spending_key(&seed, Network::TestNetwork.coin_type(), account); /// let extsk = sapling::spending_key(&seed, Network::TestNetwork.coin_type(), account);
/// let extfvk = ExtendedFullViewingKey::from(&extsk); /// let dfvk = ExtendedFullViewingKey::from(&extsk).into();
/// let ufvk = UnifiedFullViewingKey::new(account, None, Some(extfvk)).unwrap(); /// let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
/// init_accounts_table(&db_data, &[ufvk]).unwrap(); /// init_accounts_table(&db_data, &[ufvk]).unwrap();
/// # } /// # }
/// ``` /// ```
@ -199,17 +199,9 @@ pub fn init_accounts_table<P: consensus::Parameters>(
// Insert accounts atomically // Insert accounts atomically
wdb.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?; wdb.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
for key in keys.iter() { for (account, key) in (0u32..).zip(keys) {
let extfvk_str: Option<String> = key.sapling().map(|extfvk| { let ufvk_str: String = key.encode(&wdb.params);
encode_extended_full_viewing_key( let address_str: String = key.default_address().0.encode(&wdb.params);
wdb.params.hrp_sapling_extended_full_viewing_key(),
extfvk,
)
});
let address_str: Option<String> = key
.sapling()
.map(|extfvk| address_from_extfvk(&wdb.params, extfvk));
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let taddress_str: Option<String> = key.transparent().and_then(|k| { let taddress_str: Option<String> = key.transparent().and_then(|k| {
k.derive_external_ivk() k.derive_external_ivk()
@ -220,14 +212,9 @@ pub fn init_accounts_table<P: consensus::Parameters>(
let taddress_str: Option<String> = None; let taddress_str: Option<String> = None;
wdb.conn.execute( wdb.conn.execute(
"INSERT INTO accounts (account, extfvk, address, transparent_address) "INSERT INTO accounts (account, ufvk, address, transparent_address)
VALUES (?, ?, ?, ?)", VALUES (?, ?, ?, ?)",
params![ params![account, ufvk_str, address_str, taddress_str],
u32::from(key.account()),
extfvk_str,
address_str,
taddress_str,
],
)?; )?;
} }
wdb.conn.execute("COMMIT", NO_PARAMS)?; wdb.conn.execute("COMMIT", NO_PARAMS)?;
@ -306,6 +293,7 @@ mod tests {
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::{BlockHeight, Parameters}, consensus::{BlockHeight, Parameters},
sapling::keys::DiversifiableFullViewingKey,
zip32::ExtendedFullViewingKey, zip32::ExtendedFullViewingKey,
}; };
@ -332,22 +320,22 @@ mod tests {
// First call with data should initialise the accounts table // First call with data should initialise the accounts table
let extsk = sapling::spending_key(&seed, network().coin_type(), account); 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")] #[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new( let ufvk = UnifiedFullViewingKey::new(
account,
Some( Some(
transparent::AccountPrivKey::from_seed(&network(), &seed, account) transparent::AccountPrivKey::from_seed(&network(), &seed, account)
.unwrap() .unwrap()
.to_account_pubkey(), .to_account_pubkey(),
), ),
Some(extfvk), Some(dfvk),
None,
) )
.unwrap(); .unwrap();
#[cfg(not(feature = "transparent-inputs"))] #[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(); init_accounts_table(&db_data, &[ufvk.clone()]).unwrap();

View File

@ -165,7 +165,10 @@ mod tests {
block::BlockHash, block::BlockHash,
consensus::{BlockHeight, BranchId, Parameters}, consensus::{BlockHeight, BranchId, Parameters},
legacy::TransparentAddress, 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}, transaction::{components::Amount, Transaction},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
}; };
@ -207,8 +210,8 @@ mod tests {
// Add two accounts to the wallet // Add two accounts to the wallet
let extsk0 = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); 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 extsk1 = sapling::spending_key(&[1u8; 32], network().coin_type(), AccountId::from(1));
let extfvk0 = ExtendedFullViewingKey::from(&extsk0); let dfvk0 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk0));
let extfvk1 = ExtendedFullViewingKey::from(&extsk1); let dfvk1 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk1));
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let ufvks = { let ufvks = {
@ -219,24 +222,16 @@ mod tests {
transparent::AccountPrivKey::from_seed(&network(), &[1u8; 32], AccountId::from(1)) transparent::AccountPrivKey::from_seed(&network(), &[1u8; 32], AccountId::from(1))
.unwrap(); .unwrap();
[ [
UnifiedFullViewingKey::new( UnifiedFullViewingKey::new(Some(tsk0.to_account_pubkey()), Some(dfvk0), None)
AccountId::from(0), .unwrap(),
Some(tsk0.to_account_pubkey()), UnifiedFullViewingKey::new(Some(tsk1.to_account_pubkey()), Some(dfvk1), None)
Some(extfvk0), .unwrap(),
)
.unwrap(),
UnifiedFullViewingKey::new(
AccountId::from(1),
Some(tsk1.to_account_pubkey()),
Some(extfvk1),
)
.unwrap(),
] ]
}; };
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
let ufvks = [ let ufvks = [
UnifiedFullViewingKey::new(AccountId::from(0), Some(extfvk0)).unwrap(), UnifiedFullViewingKey::new(Some(dfvk0), None).unwrap(),
UnifiedFullViewingKey::new(AccountId::from(1), Some(extfvk1)).unwrap(), UnifiedFullViewingKey::new(Some(dfvk1), None).unwrap(),
]; ];
init_accounts_table(&db_data, &ufvks).unwrap(); init_accounts_table(&db_data, &ufvks).unwrap();
@ -285,12 +280,12 @@ mod tests {
// Add an account to the wallet // Add an account to the wallet
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); 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")] #[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"))] #[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(); init_accounts_table(&db_data, &[ufvk]).unwrap();
let to = extsk.default_address().1.into(); let to = extsk.default_address().1.into();
@ -329,11 +324,11 @@ mod tests {
// Add an account to the wallet // Add an account to the wallet
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); 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")] #[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"))] #[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(); init_accounts_table(&db_data, &[ufvk]).unwrap();
let to = extsk.default_address().1.into(); let to = extsk.default_address().1.into();
@ -377,12 +372,11 @@ mod tests {
// Add an account to the wallet // Add an account to the wallet
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); 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")] #[cfg(feature = "transparent-inputs")]
let ufvk = let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk.clone())).unwrap();
#[cfg(not(feature = "transparent-inputs"))] #[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(); init_accounts_table(&db_data, &[ufvk]).unwrap();
// Add funds to the wallet in a single note // Add funds to the wallet in a single note
@ -390,7 +384,7 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk.clone(), &dfvk,
value, value,
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -409,12 +403,7 @@ mod tests {
); );
// Add more funds to the wallet in a second note // Add more funds to the wallet in a second note
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(sapling_activation_height() + 1, cb.hash(), &dfvk, value);
sapling_activation_height() + 1,
cb.hash(),
extfvk.clone(),
value,
);
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); 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 // Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
// note is verified // note is verified
for i in 2..10 { for i in 2..10 {
let (cb, _) = fake_compact_block( let (cb, _) =
sapling_activation_height() + i, fake_compact_block(sapling_activation_height() + i, cb.hash(), &dfvk, value);
cb.hash(),
extfvk.clone(),
value,
);
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
} }
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); 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 // Mine block 11 so that the second note becomes verified
let (cb, _) = let (cb, _) = fake_compact_block(sapling_activation_height() + 10, cb.hash(), &dfvk, value);
fake_compact_block(sapling_activation_height() + 10, cb.hash(), extfvk, value);
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
@ -521,12 +505,11 @@ mod tests {
// Add an account to the wallet // Add an account to the wallet
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); 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")] #[cfg(feature = "transparent-inputs")]
let ufvk = let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk.clone())).unwrap();
#[cfg(not(feature = "transparent-inputs"))] #[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(); init_accounts_table(&db_data, &[ufvk]).unwrap();
// Add funds to the wallet in a single note // Add funds to the wallet in a single note
@ -534,7 +517,7 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk, &dfvk,
value, value,
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -585,7 +568,7 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height() + i, sapling_activation_height() + i,
cb.hash(), cb.hash(),
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])), &ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])).into(),
value, value,
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -616,7 +599,7 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height() + 22, sapling_activation_height() + 22,
cb.hash(), cb.hash(),
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])), &ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])).into(),
value, value,
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -651,12 +634,11 @@ mod tests {
// Add an account to the wallet // Add an account to the wallet
let extsk = sapling::spending_key(&[0u8; 32], network.coin_type(), AccountId::from(0)); 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")] #[cfg(feature = "transparent-inputs")]
let ufvk = let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk.clone())).unwrap();
#[cfg(not(feature = "transparent-inputs"))] #[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(); init_accounts_table(&db_data, &[ufvk]).unwrap();
// Add funds to the wallet in a single note // Add funds to the wallet in a single note
@ -664,7 +646,7 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk.clone(), &dfvk,
value, value,
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -721,7 +703,7 @@ mod tests {
try_sapling_output_recovery( try_sapling_output_recovery(
&network, &network,
sapling_activation_height(), sapling_activation_height(),
&extfvk.fvk.ovk, &dfvk.fvk().ovk,
output, output,
) )
}; };
@ -738,7 +720,7 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height() + i, sapling_activation_height() + i,
cb.hash(), cb.hash(),
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])), &ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])).into(),
value, value,
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);
@ -762,12 +744,11 @@ mod tests {
// Add an account to the wallet // Add an account to the wallet
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0)); 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")] #[cfg(feature = "transparent-inputs")]
let ufvk = let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
UnifiedFullViewingKey::new(AccountId::from(0), None, Some(extfvk.clone())).unwrap();
#[cfg(not(feature = "transparent-inputs"))] #[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(); init_accounts_table(&db_data, &[ufvk]).unwrap();
// Add funds to the wallet in a single note // Add funds to the wallet in a single note
@ -775,7 +756,7 @@ mod tests {
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
sapling_activation_height(), sapling_activation_height(),
BlockHash([0; 32]), BlockHash([0; 32]),
extfvk, &dfvk,
value, value,
); );
insert_into_cache(&db_cache, &cb); insert_into_cache(&db_cache, &cb);

View File

@ -6,6 +6,9 @@ and this library adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added
- `zcash_primitives::sapling::keys::DiversifiableFullViewingKey`
- `zcash_primitives::sapling::keys::Scope`
## [0.7.0] - 2022-06-24 ## [0.7.0] - 2022-06-24
### Changed ### Changed

View File

@ -4,16 +4,20 @@
//! //!
//! [section 4.2.2]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents //! [section 4.2.2]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents
use std::convert::TryInto;
use std::io::{self, Read, Write};
use crate::{ use crate::{
constants::{PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR}, constants::{PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR},
keys::{prf_expand, OutgoingViewingKey}, keys::{prf_expand, OutgoingViewingKey},
sapling::{ProofGenerationKey, ViewingKey}, zip32,
}; };
use ff::PrimeField; use ff::PrimeField;
use group::{Group, GroupEncoding}; use group::{Group, GroupEncoding};
use std::io::{self, Read, Write};
use subtle::CtOption; use subtle::CtOption;
use super::{PaymentAddress, ProofGenerationKey, SaplingIvk, ViewingKey};
/// A Sapling expanded spending key /// A Sapling expanded spending key
#[derive(Clone)] #[derive(Clone)]
pub struct ExpandedSpendingKey { pub struct ExpandedSpendingKey {
@ -22,7 +26,7 @@ pub struct ExpandedSpendingKey {
pub ovk: OutgoingViewingKey, pub ovk: OutgoingViewingKey,
} }
/// A Sapling full viewing key /// A Sapling key that provides the capability to view incoming and outgoing transactions.
#[derive(Debug)] #[derive(Debug)]
pub struct FullViewingKey { pub struct FullViewingKey {
pub vk: ViewingKey, 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<zip32::ExtendedFullViewingKey> 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<Self> {
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<PaymentAddress> {
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"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use proptest::collection::vec; use proptest::collection::vec;
@ -185,8 +341,8 @@ pub mod testing {
mod tests { mod tests {
use group::{Group, GroupEncoding}; use group::{Group, GroupEncoding};
use super::FullViewingKey; use super::{DiversifiableFullViewingKey, FullViewingKey};
use crate::constants::SPENDING_KEY_GENERATOR; use crate::{constants::SPENDING_KEY_GENERATOR, zip32};
#[test] #[test]
fn ak_must_be_prime_order() { fn ak_must_be_prime_order() {
@ -210,4 +366,24 @@ mod tests {
// nk is allowed to be the identity. // nk is allowed to be the identity.
assert!(FullViewingKey::read(&buf[..]).is_ok()); 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);
}
} }

View File

@ -316,7 +316,7 @@ pub struct ExtendedFullViewingKey {
child_index: ChildIndex, child_index: ChildIndex,
chain_code: ChainCode, chain_code: ChainCode,
pub fvk: FullViewingKey, pub fvk: FullViewingKey,
dk: DiversifierKey, pub(crate) dk: DiversifierKey,
} }
impl std::cmp::PartialEq for ExtendedSpendingKey { impl std::cmp::PartialEq for ExtendedSpendingKey {