zcash_client_backend: Add receiver type selection to unified address derivation.

This commit is contained in:
Kris Nuttycombe 2023-04-28 11:40:16 -06:00
parent c3a630bac3
commit 6cbdd494cf
10 changed files with 170 additions and 42 deletions

1
Cargo.lock generated
View File

@ -3041,6 +3041,7 @@ dependencies = [
"zcash_note_encryption", "zcash_note_encryption",
"zcash_primitives", "zcash_primitives",
"zcash_proofs", "zcash_proofs",
"zip32",
] ]
[[package]] [[package]]

View File

@ -29,6 +29,9 @@ and this library adheres to Rust's notion of
functionality and move it behind the `transparent-inputs` feature flag. functionality and move it behind the `transparent-inputs` feature flag.
- `zcash_client_backend::fees::{standard, orchard, sapling}` - `zcash_client_backend::fees::{standard, orchard, sapling}`
- `zcash_client_backend::fees::ChangeValue::{new, orchard}` - `zcash_client_backend::fees::ChangeValue::{new, orchard}`
- `zcash_client_backend::keys`:
- `AddressGenerationError`
- `UnifiedAddressRequest`
- `zcash_client_backend::wallet`: - `zcash_client_backend::wallet`:
- `Note` - `Note`
- `ReceivedNote` - `ReceivedNote`
@ -81,6 +84,8 @@ and this library adheres to Rust's notion of
- Fields of `Balance` and `AccountBalance` have been made private and the values - Fields of `Balance` and `AccountBalance` have been made private and the values
of these fields have been made available via methods having the same names of these fields have been made available via methods having the same names
as the previously-public fields. as the previously-public fields.
- `WalletWrite::get_next_available_address` now takes an additional
`UnifiedAddressRequest` argument.
- `chain::scan_cached_blocks` now returns a `ScanSummary` containing metadata - `chain::scan_cached_blocks` now returns a `ScanSummary` containing metadata
about the scanned blocks on success. about the scanned blocks on success.
- `error::Error` enum changes: - `error::Error` enum changes:
@ -199,8 +204,15 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::keys`: - `zcash_client_backend::keys`:
- `DerivationError::Orchard` is now only available when the `orchard` feature - `DerivationError::Orchard` is now only available when the `orchard` feature
is enabled. is enabled.
- `UnifiedSpendingKey::address` now takes an argument that specifies the
receivers to be generated in the resulting address. Also, it now returns
`Result<UnifiedAddress, AddressGenerationError>` instead of
`Option<UnifiedAddress>` so that we may better report to the user how
address generation has failed.
- `UnifiedSpendingKey::orchard` is now only available when the `orchard` - `UnifiedSpendingKey::orchard` is now only available when the `orchard`
feature is enabled. feature is enabled.
- `UnifiedSpendingKey::transparent` is now only available when the
`transparent-inputs` feature is enabled.
- `UnifiedFullViewingKey::new` no longer takes an Orchard full viewing key - `UnifiedFullViewingKey::new` no longer takes an Orchard full viewing key
argument unless the `orchard` feature is enabled. argument unless the `orchard` feature is enabled.

View File

@ -29,6 +29,7 @@ zcash_address.workspace = true
zcash_encoding.workspace = true zcash_encoding.workspace = true
zcash_note_encryption.workspace = true zcash_note_encryption.workspace = true
zcash_primitives.workspace = true zcash_primitives.workspace = true
zip32.workspace = true
# Dependencies exposed in a public API: # Dependencies exposed in a public API:
# (Breaking upgrades to these require a breaking upgrade to this crate.) # (Breaking upgrades to these require a breaking upgrade to this crate.)

View File

@ -26,7 +26,7 @@ use zcash_primitives::{
use crate::{ use crate::{
address::{AddressMetadata, UnifiedAddress}, address::{AddressMetadata, UnifiedAddress},
decrypt::DecryptedOutput, decrypt::DecryptedOutput,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proto::service::TreeState, proto::service::TreeState,
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx}, wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
ShieldedProtocol, ShieldedProtocol,
@ -1004,6 +1004,7 @@ pub trait WalletWrite: WalletRead {
fn get_next_available_address( fn get_next_available_address(
&mut self, &mut self,
account: AccountId, account: AccountId,
request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, Self::Error>; ) -> Result<Option<UnifiedAddress>, Self::Error>;
/// Updates the state of the wallet database by persisting the provided block information, /// Updates the state of the wallet database by persisting the provided block information,
@ -1104,7 +1105,7 @@ pub mod testing {
use crate::{ use crate::{
address::{AddressMetadata, UnifiedAddress}, address::{AddressMetadata, UnifiedAddress},
keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput}, wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput},
ShieldedProtocol, ShieldedProtocol,
}; };
@ -1301,6 +1302,7 @@ pub mod testing {
fn get_next_available_address( fn get_next_available_address(
&mut self, &mut self,
_account: AccountId, _account: AccountId,
_request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, Self::Error> { ) -> Result<Option<UnifiedAddress>, Self::Error> {
Ok(None) Ok(None)
} }

View File

@ -86,6 +86,7 @@ where
// TODO: implement a less naive strategy for selecting the pool to which change will be sent. // TODO: implement a less naive strategy for selecting the pool to which change will be sent.
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
#[allow(clippy::if_same_then_else)]
let (change_pool, sapling_change, orchard_change) = let (change_pool, sapling_change, orchard_change) =
if orchard_in.is_positive() || orchard_out.is_positive() { if orchard_in.is_positive() || orchard_out.is_positive() {
// Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs // Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs

View File

@ -1,5 +1,5 @@
//! Helper functions for managing light client key material. //! Helper functions for managing light client key material.
use zcash_address::unified::{self, Container, Encoding}; use zcash_address::unified::{self, Container, Encoding, Typecode};
use zcash_primitives::{ use zcash_primitives::{
consensus, consensus,
zip32::{AccountId, DiversifierIndex}, zip32::{AccountId, DiversifierIndex},
@ -21,16 +21,18 @@ use {
byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}, byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt},
std::convert::TryFrom, std::convert::TryFrom,
std::io::{Read, Write}, std::io::{Read, Write},
zcash_address::unified::Typecode,
zcash_encoding::CompactSize, zcash_encoding::CompactSize,
zcash_primitives::consensus::BranchId, zcash_primitives::consensus::BranchId,
}; };
#[cfg(feature = "orchard")]
use orchard::{self, keys::Scope};
pub mod sapling { pub mod sapling {
pub use sapling::zip32::{ pub use sapling::zip32::{
DiversifiableFullViewingKey, ExtendedFullViewingKey, ExtendedSpendingKey, DiversifiableFullViewingKey, ExtendedFullViewingKey, ExtendedSpendingKey,
}; };
use zcash_primitives::zip32::{AccountId, ChildIndex}; use zip32::{AccountId, ChildIndex};
/// Derives the ZIP 32 [`ExtendedSpendingKey`] for a given coin type and account from the /// Derives the ZIP 32 [`ExtendedSpendingKey`] for a given coin type and account from the
/// given seed. /// given seed.
@ -44,11 +46,11 @@ pub mod sapling {
/// ``` /// ```
/// use zcash_primitives::{ /// use zcash_primitives::{
/// constants::testnet::COIN_TYPE, /// constants::testnet::COIN_TYPE,
/// zip32::AccountId,
/// }; /// };
/// use zcash_client_backend::{ /// use zcash_client_backend::{
/// keys::sapling, /// keys::sapling,
/// }; /// };
/// use zip32::AccountId;
/// ///
/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::ZERO); /// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::ZERO);
/// ``` /// ```
@ -160,8 +162,7 @@ impl Era {
} }
} }
/// A set of spending keys that are all associated with a single /// A set of spending keys that are all associated with a single ZIP-0032 account identifier.
/// ZIP-0032 account identifier.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[doc(hidden)] #[doc(hidden)]
pub struct UnifiedSpendingKey { pub struct UnifiedSpendingKey {
@ -397,6 +398,65 @@ impl UnifiedSpendingKey {
} }
} }
/// Errors that can occur in the generation of unified addresses.
#[derive(Clone, Debug)]
pub enum AddressGenerationError {
/// The requested diversifier index was outside the range of valid transparent
/// child address indices.
InvalidTransparentChildIndex(DiversifierIndex),
/// The diversifier index could not be mapped to a valid Sapling diversifier.
InvalidSaplingDiversifierIndex(DiversifierIndex),
/// A requested address typecode was not recognized, so we are unable to generate the address
/// as requested.
ReceiverTypeNotSupported(Typecode),
/// A Unified address cannot be generated without at least one shielded receiver being
/// included.
ShieldedReceiverRequired,
}
/// Specification for how a unified address should be generated from a unified viewing key.
#[derive(Clone, Copy, Debug)]
pub struct UnifiedAddressRequest {
#[cfg(feature = "orchard")]
has_orchard: bool,
has_sapling: bool,
#[cfg(feature = "transparent-inputs")]
has_p2pkh: bool,
}
impl UnifiedAddressRequest {
pub const DEFAULT: UnifiedAddressRequest = Self {
#[cfg(feature = "orchard")]
has_orchard: false, // FIXME: Always request Orchard receivers once we can receive Orchard funds
has_sapling: true,
#[cfg(feature = "transparent-inputs")]
has_p2pkh: true,
};
pub fn new(
#[cfg(feature = "orchard")] has_orchard: bool,
has_sapling: bool,
#[cfg(feature = "transparent-inputs")] has_p2pkh: bool,
) -> Option<Self> {
#[cfg(feature = "orchard")]
let has_shielded_receiver = has_orchard || has_sapling;
#[cfg(not(feature = "orchard"))]
let has_shielded_receiver = has_sapling;
if !has_shielded_receiver {
None
} else {
Some(Self {
#[cfg(feature = "orchard")]
has_orchard,
has_sapling,
#[cfg(feature = "transparent-inputs")]
has_p2pkh,
})
}
}
}
/// A [ZIP 316](https://zips.z.cash/zip-0316) unified full viewing key. /// A [ZIP 316](https://zips.z.cash/zip-0316) unified full viewing key.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[doc(hidden)] #[doc(hidden)]
@ -560,29 +620,55 @@ impl UnifiedFullViewingKey {
self.orchard.as_ref() self.orchard.as_ref()
} }
/// Attempts to derive the Unified Address for the given diversifier index. /// Attempts to derive the Unified Address for the given diversifier index and
/// receiver types.
/// ///
/// Returns `None` if the specified index does not produce a valid diversifier. /// Returns `None` if the specified index does not produce a valid diversifier.
// TODO: Allow filtering down by receiver types? pub fn address(
pub fn address(&self, j: DiversifierIndex) -> Option<UnifiedAddress> { &self,
let sapling = if let Some(extfvk) = self.sapling.as_ref() { j: DiversifierIndex,
Some(extfvk.address(j)?) request: UnifiedAddressRequest,
) -> Result<UnifiedAddress, AddressGenerationError> {
#[cfg(feature = "orchard")]
let orchard = {
let orchard_j = orchard::keys::DiversifierIndex::from(*j.as_bytes());
self.orchard
.as_ref()
.filter(|_| request.has_orchard)
.map(|ofvk| ofvk.address_at(orchard_j, Scope::External))
};
let sapling = if let Some(extfvk) = self.sapling.as_ref().filter(|_| request.has_sapling) {
// If a Sapling receiver type is requested, we must be able to construct an
// address; if we're unable to do so, then no Unified Address exists at this
// diversifier and we use `?` to early-return from this method.
Some(
extfvk
.address(j)
.ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(j))?,
)
} else { } else {
None None
}; };
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let transparent = if let Some(tfvk) = self.transparent.as_ref() { let transparent = if let Some(tfvk) =
self.transparent.as_ref().filter(|_| request.has_p2pkh)
{
// If a transparent receiver type is requested, we must be able to construct an
// address; if we're unable to do so, then no Unified Address exists at this
// diversifier.
match to_transparent_child_index(j) { match to_transparent_child_index(j) {
Some(transparent_j) => match tfvk Some(transparent_j) => match tfvk
.derive_external_ivk() .derive_external_ivk()
.and_then(|tivk| tivk.derive_address(transparent_j)) .and_then(|tivk| tivk.derive_address(transparent_j))
{ {
Ok(taddr) => Some(taddr), Ok(taddr) => Some(taddr),
Err(_) => return None, Err(_) => return Err(AddressGenerationError::InvalidTransparentChildIndex(j)),
}, },
// Diversifier doesn't generate a valid transparent child index. // Diversifier doesn't generate a valid transparent child index, so we eagerly
None => return None, // return `None`.
None => return Err(AddressGenerationError::InvalidTransparentChildIndex(j)),
} }
} else { } else {
None None
@ -592,10 +678,11 @@ impl UnifiedFullViewingKey {
UnifiedAddress::from_receivers( UnifiedAddress::from_receivers(
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
None, orchard,
sapling, sapling,
transparent, transparent,
) )
.ok_or(AddressGenerationError::ShieldedReceiverRequired)
} }
/// Searches the diversifier space starting at diversifier index `j` for one which will /// Searches the diversifier space starting at diversifier index `j` for one which will
@ -606,6 +693,7 @@ impl UnifiedFullViewingKey {
pub fn find_address( pub fn find_address(
&self, &self,
mut j: DiversifierIndex, mut j: DiversifierIndex,
request: UnifiedAddressRequest,
) -> Option<(UnifiedAddress, DiversifierIndex)> { ) -> Option<(UnifiedAddress, DiversifierIndex)> {
// If we need to generate a transparent receiver, check that the user has not // If we need to generate a transparent receiver, check that the user has not
// specified an invalid transparent child index, from which we can never search to // specified an invalid transparent child index, from which we can never search to
@ -617,12 +705,19 @@ impl UnifiedFullViewingKey {
// Find a working diversifier and construct the associated address. // Find a working diversifier and construct the associated address.
loop { loop {
let res = self.address(j); let res = self.address(j, request);
if let Some(ua) = res { match res {
break Some((ua, j)); Ok(ua) => {
} break Some((ua, j));
if j.increment().is_err() { }
break None; Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => {
if j.increment().is_err() {
break None;
}
}
Err(_) => {
break None;
}
} }
} }
} }
@ -630,7 +725,8 @@ impl UnifiedFullViewingKey {
/// Returns the Unified Address corresponding to the smallest valid diversifier index, /// Returns the Unified Address corresponding to the smallest valid diversifier index,
/// along with that index. /// along with that index.
pub fn default_address(&self) -> (UnifiedAddress, DiversifierIndex) { pub fn default_address(&self) -> (UnifiedAddress, DiversifierIndex) {
self.find_address(DiversifierIndex::new()) // FIXME: Enable Orchard keys
self.find_address(DiversifierIndex::new(), UnifiedAddressRequest::DEFAULT)
.expect("UFVK should have at least one valid diversifier") .expect("UFVK should have at least one valid diversifier")
} }
} }
@ -663,19 +759,18 @@ mod tests {
use proptest::prelude::proptest; use proptest::prelude::proptest;
use super::{sapling, UnifiedFullViewingKey}; use super::{sapling, UnifiedFullViewingKey};
use zcash_primitives::{consensus::MAIN_NETWORK, zip32::AccountId}; use zcash_primitives::consensus::MAIN_NETWORK;
use zip32::AccountId;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
crate::{address::Address, encoding::AddressCodec}, crate::{address::Address, encoding::AddressCodec},
zcash_address::test_vectors, zcash_address::test_vectors,
zcash_primitives::{ zcash_primitives::legacy::{
legacy::{ self,
self, keys::{AccountPrivKey, IncomingViewingKey},
keys::{AccountPrivKey, IncomingViewingKey},
},
zip32::DiversifierIndex,
}, },
zip32::DiversifierIndex,
}; };
#[cfg(feature = "unstable")] #[cfg(feature = "unstable")]
@ -797,6 +892,8 @@ mod tests {
#[test] #[test]
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
fn ufvk_derivation() { fn ufvk_derivation() {
use crate::keys::UnifiedAddressRequest;
use super::UnifiedSpendingKey; use super::UnifiedSpendingKey;
for tv in test_vectors::UNIFIED { for tv in test_vectors::UNIFIED {
@ -816,8 +913,14 @@ mod tests {
continue; continue;
} }
let ua = ufvk.address(d_idx).unwrap_or_else(|| panic!("diversifier index {} should have produced a valid unified address for account {}", let ua = ufvk
tv.diversifier_index, tv.account)); .address(d_idx, UnifiedAddressRequest::DEFAULT)
.unwrap_or_else(|err| {
panic!(
"unified address generation failed for account {}: {:?}",
tv.account, err
)
});
match Address::decode(&MAIN_NETWORK, tv.unified_addr) { match Address::decode(&MAIN_NETWORK, tv.unified_addr) {
Some(Address::Unified(tvua)) => { Some(Address::Unified(tvua)) => {

View File

@ -794,8 +794,7 @@ pub mod testing {
label in option::of(any::<String>()), label in option::of(any::<String>()),
// prevent duplicates by generating a set rather than a vec // prevent duplicates by generating a set rather than a vec
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3), other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
) -> Payment { ) -> Payment {
let is_shielded = match recipient_address { let is_shielded = match recipient_address {
Address::Transparent(_) => false, Address::Transparent(_) => false,
Address::Sapling(_) | Address::Unified(_) => true, Address::Sapling(_) | Address::Unified(_) => true,

View File

@ -19,6 +19,7 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[dependencies] [dependencies]
zcash_address.workspace = true
zcash_client_backend = { workspace = true, features = ["unstable-serialization", "unstable-spanning-tree"] } zcash_client_backend = { workspace = true, features = ["unstable-serialization", "unstable-spanning-tree"] }
zcash_encoding.workspace = true zcash_encoding.workspace = true
zcash_primitives.workspace = true zcash_primitives.workspace = true

View File

@ -67,7 +67,7 @@ use zcash_client_backend::{
ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary,
WalletWrite, SAPLING_SHARD_HEIGHT, WalletWrite, SAPLING_SHARD_HEIGHT,
}, },
keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput}, wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
DecryptedOutput, PoolType, ShieldedProtocol, TransferType, DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
@ -401,6 +401,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
fn get_next_available_address( fn get_next_available_address(
&mut self, &mut self,
account: AccountId, account: AccountId,
request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, Self::Error> { ) -> Result<Option<UnifiedAddress>, Self::Error> {
self.transactionally( self.transactionally(
|wdb| match wdb.get_unified_full_viewing_keys()?.get(&account) { |wdb| match wdb.get_unified_full_viewing_keys()?.get(&account) {
@ -417,7 +418,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
}; };
let (addr, diversifier_index) = ufvk let (addr, diversifier_index) = ufvk
.find_address(search_from) .find_address(search_from, request)
.ok_or(SqliteClientError::DiversifierIndexOutOfRange)?; .ok_or(SqliteClientError::DiversifierIndexOutOfRange)?;
wallet::insert_address( wallet::insert_address(
@ -1109,7 +1110,10 @@ extern crate assert_matches;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use zcash_client_backend::data_api::{AccountBirthday, WalletRead, WalletWrite}; use zcash_client_backend::{
data_api::{AccountBirthday, WalletRead, WalletWrite},
keys::UnifiedAddressRequest,
};
use crate::{testing::TestBuilder, AccountId}; use crate::{testing::TestBuilder, AccountId};
@ -1132,7 +1136,11 @@ mod tests {
let current_addr = st.wallet().get_current_address(account).unwrap(); let current_addr = st.wallet().get_current_address(account).unwrap();
assert!(current_addr.is_some()); assert!(current_addr.is_some());
let addr2 = st.wallet_mut().get_next_available_address(account).unwrap(); // TODO: Add Orchard
let addr2 = st
.wallet_mut()
.get_next_available_address(account, UnifiedAddressRequest::DEFAULT)
.unwrap();
assert!(addr2.is_some()); assert!(addr2.is_some());
assert_ne!(current_addr, addr2); assert_ne!(current_addr, addr2);

View File

@ -1060,7 +1060,7 @@ mod tests {
#[test] #[test]
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
fn account_produces_expected_ua_sequence() { fn account_produces_expected_ua_sequence() {
use zcash_client_backend::data_api::AccountBirthday; use zcash_client_backend::{data_api::AccountBirthday, keys::UnifiedAddressRequest};
let network = Network::MainNetwork; let network = Network::MainNetwork;
let data_file = NamedTempFile::new().unwrap(); let data_file = NamedTempFile::new().unwrap();
@ -1090,7 +1090,7 @@ mod tests {
assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork)); assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
db_data db_data
.get_next_available_address(account) .get_next_available_address(account, UnifiedAddressRequest::DEFAULT)
.unwrap() .unwrap()
.expect("get_next_available_address generated an address"); .expect("get_next_available_address generated an address");
} else { } else {