Merge pull request #570 from zcash/387-migrate-from-extfvk-to-ufvk
`zcash_client_*`: Migrate from `ExtendedFullViewingKey` to `UnifiedFullViewingKey`
This commit is contained in:
commit
0142a3db1b
|
@ -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`).
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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[..],
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¶ms_to_network(params))
|
||||
}
|
||||
|
||||
/// Returns the transparent component of the unified key at the
|
||||
|
@ -178,9 +278,8 @@ impl UnifiedFullViewingKey {
|
|||
self.transparent.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the Sapling extended full viewing key component of this
|
||||
/// unified key.
|
||||
pub fn sapling(&self) -> Option<&sapling::ExtendedFullViewingKey> {
|
||||
/// Returns the Sapling diversifiable full viewing key component of this unified key.
|
||||
pub fn sapling(&self) -> Option<&sapling_keys::DiversifiableFullViewingKey> {
|
||||
self.sapling.as_ref()
|
||||
}
|
||||
|
||||
|
@ -256,13 +355,16 @@ impl UnifiedFullViewingKey {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::sapling;
|
||||
use zcash_primitives::zip32::AccountId;
|
||||
use super::{sapling, UnifiedFullViewingKey};
|
||||
use zcash_primitives::{
|
||||
consensus::MAIN_NETWORK,
|
||||
zip32::{AccountId, ExtendedFullViewingKey},
|
||||
};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
crate::encoding::AddressCodec,
|
||||
zcash_primitives::{consensus::MAIN_NETWORK, legacy, legacy::keys::IncomingViewingKey},
|
||||
zcash_primitives::{legacy, legacy::keys::IncomingViewingKey},
|
||||
};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
@ -291,4 +393,46 @@ mod tests {
|
|||
.encode(&MAIN_NETWORK);
|
||||
assert_eq!(taddr, "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ufvk_round_trip() {
|
||||
let account = 0.into();
|
||||
|
||||
let orchard = {
|
||||
let sk = orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, 0).unwrap();
|
||||
Some(orchard::keys::FullViewingKey::from(&sk))
|
||||
};
|
||||
|
||||
let sapling = {
|
||||
let extsk = sapling::spending_key(&[0; 32], 0, account);
|
||||
Some(ExtendedFullViewingKey::from(&extsk).into())
|
||||
};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let transparent = { None };
|
||||
|
||||
let ufvk = UnifiedFullViewingKey::new(
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
transparent,
|
||||
sapling,
|
||||
orchard,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let encoding = ufvk.encode(&MAIN_NETWORK);
|
||||
let decoded = UnifiedFullViewingKey::decode(&MAIN_NETWORK, &encoding).unwrap();
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
assert_eq!(
|
||||
decoded.transparent.map(|t| t.serialize()),
|
||||
ufvk.transparent.map(|t| t.serialize()),
|
||||
);
|
||||
assert_eq!(
|
||||
decoded.sapling.map(|s| s.to_bytes()),
|
||||
ufvk.sapling.map(|s| s.to_bytes()),
|
||||
);
|
||||
assert_eq!(
|
||||
decoded.orchard.map(|o| o.to_bytes()),
|
||||
ufvk.orchard.map(|o| o.to_bytes()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue