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.
- `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`).

View File

@ -8,7 +8,7 @@ use zcash_address::{
};
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.
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<P: consensus::Parameters>(&self, params: &P) -> String {
self.to_address(params_to_network(params)).to_string()
}
}
/// An address that funds can be sent to.

View File

@ -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<Option<PaymentAddress>, 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<HashMap<AccountId, ExtendedFullViewingKey>, Self::Error>;
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, 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<HashMap<AccountId, ExtendedFullViewingKey>, Self::Error> {
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
Ok(HashMap::new())
}

View File

@ -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[..],

View File

@ -41,8 +41,8 @@ where
P: consensus::Parameters,
D: WalletWrite<Error = E>,
{
// 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 {

View File

@ -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<P: consensus::Parameters>(
params: &P,
height: BlockHeight,
tx: &Transaction,
extfvks: &HashMap<AccountId, ExtendedFullViewingKey>,
ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
) -> Vec<DecryptedOutput> {
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,
})
}
}
}
}

View File

@ -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<legacy::AccountPubKey>,
// TODO: This type is invalid for a UFVK; create a `sapling::DiversifiableFullViewingKey`
// to replace it.
sapling: Option<sapling::ExtendedFullViewingKey>,
sapling: Option<sapling_keys::DiversifiableFullViewingKey>,
orchard: Option<orchard::keys::FullViewingKey>,
unknown: Vec<(u32, Vec<u8>)>,
}
#[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<legacy::AccountPubKey>,
sapling: Option<sapling::ExtendedFullViewingKey>,
sapling: Option<sapling_keys::DiversifiableFullViewingKey>,
orchard: Option<orchard::keys::FullViewingKey>,
) -> Option<UnifiedFullViewingKey> {
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<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
@ -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()),
);
}
}

View File

@ -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<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.
/// 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.
### 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

View File

@ -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,
),

View File

@ -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<P: consensus::Parameters> WalletRead for WalletDb<P> {
wallet::get_tx_height(self, txid).map_err(SqliteClientError::from)
}
fn get_extended_full_viewing_keys(
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, Self::Error> {
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, 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<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)
}
fn get_extended_full_viewing_keys(
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, Self::Error> {
self.wallet_db.get_extended_full_viewing_keys()
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
self.wallet_db.get_unified_full_viewing_keys()
}
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)]
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<Network>,
) -> (ExtendedFullViewingKey, Option<TransparentAddress>) {
) -> (DiversifiableFullViewingKey, Option<TransparentAddress>) {
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(),

View File

@ -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<P: consensus::Parameters>(
|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<P: consensus::Parameters>(
/// Returns the [`UnifiedFullViewingKey`]s for the wallet.
pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>(
wdb: &WalletDb<P>,
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, SqliteClientError> {
// Fetch the ExtendedFullViewingKeys we are tracking
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, 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<AccountId, ExtendedFullViewingKey> = HashMap::new();
let mut res: HashMap<AccountId, UnifiedFullViewingKey> = 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<P: consensus::Parameters>(
extfvk: &ExtendedFullViewingKey,
) -> Result<bool, SqliteClientError> {
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

View File

@ -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<P>(wdb: &WalletDb<P>) -> 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<P>(wdb: &WalletDb<P>) -> 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<P: consensus::Parameters>(
// Insert accounts atomically
wdb.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
for key in keys.iter() {
let extfvk_str: Option<String> = key.sapling().map(|extfvk| {
encode_extended_full_viewing_key(
wdb.params.hrp_sapling_extended_full_viewing_key(),
extfvk,
)
});
let address_str: Option<String> = 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<String> = key.transparent().and_then(|k| {
k.derive_external_ivk()
@ -220,14 +212,9 @@ pub fn init_accounts_table<P: consensus::Parameters>(
let taddress_str: Option<String> = 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();

View File

@ -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);

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).
## [Unreleased]
### Added
- `zcash_primitives::sapling::keys::DiversifiableFullViewingKey`
- `zcash_primitives::sapling::keys::Scope`
## [0.7.0] - 2022-06-24
### Changed

View File

@ -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<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"))]
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);
}
}

View File

@ -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 {