zcash_keys: Update key and address types to include ZIP-316 metadata items.

This commit is contained in:
Kris Nuttycombe 2024-01-25 14:12:19 -07:00
parent 0341171c84
commit 34ec1e5bdb
12 changed files with 498 additions and 259 deletions

View File

@ -799,6 +799,7 @@ pub mod testing {
use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis}; use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis};
use super::{MemoBytes, Payment, TransactionRequest}; use super::{MemoBytes, Payment, TransactionRequest};
pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*"; pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*";
prop_compose! { prop_compose! {

View File

@ -133,7 +133,7 @@ pub(crate) const UA_TRANSPARENT: bool = false;
pub(crate) const UA_TRANSPARENT: bool = true; pub(crate) const UA_TRANSPARENT: bool = true;
pub(crate) const DEFAULT_UA_REQUEST: UnifiedAddressRequest = pub(crate) const DEFAULT_UA_REQUEST: UnifiedAddressRequest =
UnifiedAddressRequest::unsafe_new(UA_ORCHARD, true, UA_TRANSPARENT); UnifiedAddressRequest::unsafe_new_without_expiry(UA_ORCHARD, true, UA_TRANSPARENT);
/// The ID type for accounts. /// The ID type for accounts.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
@ -1157,7 +1157,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Orchard)) Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Orchard))
}; };
wallet::put_sent_output( wallet::put_sent_output(
wdb.conn.0, wdb.conn.0,
*output.account(), *output.account(),

View File

@ -683,7 +683,7 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
conn: &rusqlite::Connection, conn: &rusqlite::Connection,
account_id: AccountId, account_id: AccountId,
) -> Result<Option<(TransparentAddress, NonHardenedChildIndex)>, SqliteClientError> { ) -> Result<Option<(TransparentAddress, NonHardenedChildIndex)>, SqliteClientError> {
use zcash_address::unified::Container; use zcash_address::unified::{Container, Item};
use zcash_primitives::legacy::keys::ExternalIvk; use zcash_primitives::legacy::keys::ExternalIvk;
// Get the UIVK for the account. // Get the UIVK for the account.
@ -705,9 +705,9 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
} }
// Derive the default transparent address (if it wasn't already part of a derived UA). // Derive the default transparent address (if it wasn't already part of a derived UA).
for item in uivk.items() { for item in uivk.items_as_parsed() {
if let Ivk::P2pkh(tivk_bytes) = item { if let Item::Data(Ivk::P2pkh(tivk_bytes)) = item {
let tivk = ExternalIvk::deserialize(&tivk_bytes)?; let tivk = ExternalIvk::deserialize(tivk_bytes)?;
return Ok(Some(tivk.default_address())); return Ok(Some(tivk.default_address()));
} }
} }

View File

@ -1418,7 +1418,8 @@ mod tests {
// Unified addresses at the time of the addition of migrations did not contain an // Unified addresses at the time of the addition of migrations did not contain an
// Orchard component. // Orchard component.
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT); let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
let address_str = Address::Unified( let address_str = Address::Unified(
ufvk.default_address(ua_request) ufvk.default_address(ua_request)
.expect("A valid default address exists for the UFVK") .expect("A valid default address exists for the UFVK")
@ -1545,7 +1546,8 @@ mod tests {
assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork)); assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
// hardcoded with knowledge of what's coming next // hardcoded with knowledge of what's coming next
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, true); let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true);
db_data db_data
.get_next_available_address(account_id, ua_request) .get_next_available_address(account_id, ua_request)
.unwrap() .unwrap()

View File

@ -443,12 +443,12 @@ mod tests {
let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap(); let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap();
let ufvk = usk.to_unified_full_viewing_key(); let ufvk = usk.to_unified_full_viewing_key();
let (ua, _) = ufvk let (ua, _) = ufvk
.default_address(UnifiedAddressRequest::unsafe_new( .default_address(UnifiedAddressRequest::unsafe_new_without_expiry(
false, false,
true, true,
UA_TRANSPARENT, UA_TRANSPARENT,
)) ))
.expect("A valid default address exists for the UFVK"); .unwrap();
let taddr = ufvk let taddr = ufvk
.transparent() .transparent()
.and_then(|k| { .and_then(|k| {

View File

@ -86,7 +86,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
)); ));
}; };
let (expected_address, idx) = ufvk.default_address( let (expected_address, idx) = ufvk.default_address(
UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT), UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT),
)?; )?;
if decoded_address != expected_address { if decoded_address != expected_address {
return Err(WalletMigrationError::CorruptedData(format!( return Err(WalletMigrationError::CorruptedData(format!(
@ -157,11 +157,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
], ],
)?; )?;
let (address, d_idx) = ufvk.default_address(UnifiedAddressRequest::unsafe_new( let (address, d_idx) = ufvk.default_address(
false, UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT),
true, )?;
UA_TRANSPARENT,
))?;
insert_address(transaction, &self.params, account, d_idx, &address)?; insert_address(transaction, &self.params, account, d_idx, &address)?;
} }

View File

@ -70,7 +70,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
}; };
let (default_addr, diversifier_index) = uivk.default_address( let (default_addr, diversifier_index) = uivk.default_address(
UnifiedAddressRequest::unsafe_new(UA_ORCHARD, true, UA_TRANSPARENT), UnifiedAddressRequest::unsafe_new_without_expiry(UA_ORCHARD, true, UA_TRANSPARENT),
)?; )?;
let mut di_be = *diversifier_index.as_bytes(); let mut di_be = *diversifier_index.as_bytes();
@ -144,7 +144,7 @@ mod tests {
.unwrap(); .unwrap();
let (addr, diversifier_index) = ufvk let (addr, diversifier_index) = ufvk
.default_address(UnifiedAddressRequest::unsafe_new( .default_address(UnifiedAddressRequest::unsafe_new_without_expiry(
false, false,
true, true,
UA_TRANSPARENT, UA_TRANSPARENT,

View File

@ -83,7 +83,8 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
// our second assumption above, and we report this as corrupted data. // our second assumption above, and we report this as corrupted data.
let mut seed_is_relevant = false; let mut seed_is_relevant = false;
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT); let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
let mut rows = stmt_fetch_accounts.query([])?; let mut rows = stmt_fetch_accounts.query([])?;
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
// We only need to check for the presence of the seed if we have keys that // We only need to check for the presence of the seed if we have keys that

View File

@ -463,7 +463,6 @@ pub(crate) mod tests {
None, None,
None, None,
) )
.unwrap()
.into() .into()
} }
@ -545,9 +544,7 @@ pub(crate) mod tests {
return Ok(result.map(|(note, addr, memo)| { return Ok(result.map(|(note, addr, memo)| {
( (
Note::Orchard(note), Note::Orchard(note),
UnifiedAddress::from_receivers(Some(addr), None, None) UnifiedAddress::from_receivers(Some(addr), None, None).into(),
.unwrap()
.into(),
MemoBytes::from_bytes(&memo).expect("correct length"), MemoBytes::from_bytes(&memo).expect("correct length"),
) )
})); }));

View File

@ -12,7 +12,12 @@ and this library adheres to Rust's notion of
## [0.2.0] - 2024-03-25 ## [0.2.0] - 2024-03-25
### Added ### Added
- `zcash_keys::address::Address::has_receiver` - `zcash_keys::address`:
- `Address::has_receiver`
- `UnifiedAddress::{
new, expiry_height, expiry_time,
unknown_data, unknown_metadata
}`
- `impl Display for zcash_keys::keys::AddressGenerationError` - `impl Display for zcash_keys::keys::AddressGenerationError`
- `impl std::error::Error for zcash_keys::keys::AddressGenerationError` - `impl std::error::Error for zcash_keys::keys::AddressGenerationError`
- `impl From<hdwallet::error::Error> for zcash_keys::keys::DerivationError` - `impl From<hdwallet::error::Error> for zcash_keys::keys::DerivationError`
@ -35,6 +40,7 @@ and this library adheres to Rust's notion of
- `UnifiedFullViewingKey::new` has been placed behind the `test-dependencies` - `UnifiedFullViewingKey::new` has been placed behind the `test-dependencies`
feature flag. UFVKs should only be produced by derivation from the USK, or feature flag. UFVKs should only be produced by derivation from the USK, or
parsed from their string representation. parsed from their string representation.
- `zcash_keys::address::UnifiedAddress::from_receivers`
### Fixed ### Fixed
- `UnifiedFullViewingKey::find_address` can now find an address for a diversifier - `UnifiedFullViewingKey::find_address` can now find an address for a diversifier

View File

@ -1,15 +1,17 @@
//! Structs for handling supported address types. //! Structs for handling supported address types.
use zcash_address::{ use zcash_address::{
unified::{self, Container, Encoding, Typecode}, unified::{self, Container, DataTypecode, Encoding, Item, Typecode},
ConversionError, ToAddress, TryFromRawAddress, ZcashAddress, ConversionError, ToAddress, TryFromRawAddress, ZcashAddress,
}; };
use zcash_primitives::legacy::TransparentAddress; use zcash_primitives::legacy::TransparentAddress;
use zcash_protocol::consensus::{self, NetworkType}; use zcash_protocol::{
consensus::{self, BlockHeight, NetworkType},
PoolType, ShieldedProtocol,
};
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
use sapling::PaymentAddress; use sapling::PaymentAddress;
use zcash_protocol::{PoolType, ShieldedProtocol};
/// A Unified Address. /// A Unified Address.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -19,7 +21,10 @@ pub struct UnifiedAddress {
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
sapling: Option<PaymentAddress>, sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>, transparent: Option<TransparentAddress>,
unknown: Vec<(u32, Vec<u8>)>, unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
} }
impl TryFrom<unified::Address> for UnifiedAddress { impl TryFrom<unified::Address> for UnifiedAddress {
@ -31,14 +36,16 @@ impl TryFrom<unified::Address> for UnifiedAddress {
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
let mut sapling = None; let mut sapling = None;
let mut transparent = None; let mut transparent = None;
let mut unknown_data = vec![];
let mut unknown: Vec<(u32, Vec<u8>)> = vec![]; let mut expiry_height = None;
let mut expiry_time = None;
let mut unknown_metadata = vec![];
// We can use as-parsed order here for efficiency, because we're breaking out the // We can use as-parsed order here for efficiency, because we're breaking out the
// receivers we support from the unknown receivers. // receivers we support from the unknown receivers.
for item in ua.items_as_parsed() { for item in ua.items_as_parsed() {
match item { match item {
unified::Receiver::Orchard(data) => { Item::Data(unified::Receiver::Orchard(data)) => {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
{ {
orchard = Some( orchard = Some(
@ -48,11 +55,11 @@ impl TryFrom<unified::Address> for UnifiedAddress {
} }
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
{ {
unknown.push((unified::Typecode::Orchard.into(), data.to_vec())); unknown_data.push((unified::Typecode::ORCHARD.into(), data.to_vec()));
} }
} }
unified::Receiver::Sapling(data) => { Item::Data(unified::Receiver::Sapling(data)) => {
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
{ {
sapling = Some( sapling = Some(
@ -62,20 +69,26 @@ impl TryFrom<unified::Address> for UnifiedAddress {
} }
#[cfg(not(feature = "sapling"))] #[cfg(not(feature = "sapling"))]
{ {
unknown.push((unified::Typecode::Sapling.into(), data.to_vec())); unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec()));
} }
} }
Item::Data(unified::Receiver::P2pkh(data)) => {
unified::Receiver::P2pkh(data) => {
transparent = Some(TransparentAddress::PublicKeyHash(*data)); transparent = Some(TransparentAddress::PublicKeyHash(*data));
} }
Item::Data(unified::Receiver::P2sh(data)) => {
unified::Receiver::P2sh(data) => {
transparent = Some(TransparentAddress::ScriptHash(*data)); transparent = Some(TransparentAddress::ScriptHash(*data));
} }
Item::Data(unified::Receiver::Unknown { typecode, data }) => {
unified::Receiver::Unknown { typecode, data } => { unknown_data.push((*typecode, data.clone()));
unknown.push((*typecode, data.clone())); }
Item::Metadata(unified::MetadataItem::ExpiryHeight(h)) => {
expiry_height = Some(BlockHeight::from(*h));
}
Item::Metadata(unified::MetadataItem::ExpiryTime(t)) => {
expiry_time = Some(*t);
}
Item::Metadata(unified::MetadataItem::Unknown { typecode, data }) => {
unknown_metadata.push((*typecode, data.clone()));
} }
} }
} }
@ -86,7 +99,10 @@ impl TryFrom<unified::Address> for UnifiedAddress {
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
sapling, sapling,
transparent, transparent,
unknown, unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
}) })
} }
} }
@ -94,36 +110,43 @@ impl TryFrom<unified::Address> for UnifiedAddress {
impl UnifiedAddress { impl UnifiedAddress {
/// Constructs a Unified Address from a given set of receivers. /// Constructs a Unified Address from a given set of receivers.
/// ///
/// Returns `None` if the receivers would produce an invalid Unified Address (namely, /// This method is only available when the `test-dependencies` feature is enabled, as
/// if no shielded receiver is provided). /// derivation from the UFVK or UIVK, or deserialization from the serialized form should be
/// used instead.
#[cfg(any(test, feature = "test-dependencies"))]
pub fn from_receivers( pub fn from_receivers(
#[cfg(feature = "orchard")] orchard: Option<orchard::Address>, #[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
#[cfg(feature = "sapling")] sapling: Option<PaymentAddress>, #[cfg(feature = "sapling")] sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>, transparent: Option<TransparentAddress>,
// TODO: Add handling for address metadata items. ) -> Self {
) -> Option<Self> { Self::new_internal(
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let has_orchard = orchard.is_some(); orchard,
#[cfg(not(feature = "orchard"))] #[cfg(feature = "sapling")]
let has_orchard = false; sapling,
transparent,
None,
None,
)
}
#[cfg(feature = "sapling")] pub(crate) fn new_internal(
let has_sapling = sapling.is_some(); #[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
#[cfg(not(feature = "sapling"))] #[cfg(feature = "sapling")] sapling: Option<PaymentAddress>,
let has_sapling = false; transparent: Option<TransparentAddress>,
expiry_height: Option<BlockHeight>,
if has_orchard || has_sapling { expiry_time: Option<u64>,
Some(Self { ) -> Self {
#[cfg(feature = "orchard")] Self {
orchard, #[cfg(feature = "orchard")]
#[cfg(feature = "sapling")] orchard,
sapling, #[cfg(feature = "sapling")]
transparent, sapling,
unknown: vec![], transparent,
}) unknown_data: vec![],
} else { expiry_height,
// UAs require at least one shielded receiver. expiry_time,
None unknown_metadata: vec![],
} }
} }
@ -168,22 +191,60 @@ impl UnifiedAddress {
self.transparent.as_ref() self.transparent.as_ref()
} }
/// Returns the set of unknown receivers of the unified address. /// Returns any unknown data items parsed from the encoded form of the address.
pub fn unknown(&self) -> &[(u32, Vec<u8>)] { pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
&self.unknown self.unknown_data.as_ref()
}
/// Returns the expiration height for this address.
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
/// Sets the expiry height of this address.
pub fn set_expiry_height(&mut self, height: BlockHeight) {
self.expiry_height = Some(height);
}
/// Removes the expiry height from this address.
pub fn unset_expiry_height(&mut self) {
self.expiry_height = None;
}
/// Returns the expiration time for this address as a Unix Epoch Time.
pub fn expiry_time(&self) -> Option<u64> {
self.expiry_time
}
/// Sets the expiry time of this address.
pub fn set_expiry_time(&mut self, time: u64) {
self.expiry_time = Some(time);
}
/// Removes the expiry time from this address.
pub fn unset_expiry_time(&mut self) {
self.expiry_time = None;
}
/// Returns any unknown metadata items parsed from the encoded form of the address.
///
/// Unknown metadata items are guaranteed by construction and parsing to not have keys in the
/// MUST-understand metadata typecode range.
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
self.unknown_metadata.as_ref()
} }
fn to_address(&self, net: NetworkType) -> ZcashAddress { fn to_address(&self, net: NetworkType) -> ZcashAddress {
let items = self let data_items =
.unknown self.unknown_data
.iter() .iter()
.map(|(typecode, data)| unified::Receiver::Unknown { .map(|(typecode, data)| unified::Receiver::Unknown {
typecode: *typecode, typecode: *typecode,
data: data.clone(), data: data.clone(),
}); });
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let items = items.chain( let data_items = data_items.chain(
self.orchard self.orchard
.as_ref() .as_ref()
.map(|addr| addr.to_raw_address_bytes()) .map(|addr| addr.to_raw_address_bytes())
@ -191,20 +252,38 @@ impl UnifiedAddress {
); );
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
let items = items.chain( let data_items = data_items.chain(
self.sapling self.sapling
.as_ref() .as_ref()
.map(|pa| pa.to_bytes()) .map(|pa| pa.to_bytes())
.map(unified::Receiver::Sapling), .map(unified::Receiver::Sapling),
); );
let items = items.chain(self.transparent.as_ref().map(|taddr| match taddr { let data_items = data_items.chain(self.transparent.as_ref().map(|taddr| match taddr {
TransparentAddress::PublicKeyHash(data) => unified::Receiver::P2pkh(*data), TransparentAddress::PublicKeyHash(data) => unified::Receiver::P2pkh(*data),
TransparentAddress::ScriptHash(data) => unified::Receiver::P2sh(*data), TransparentAddress::ScriptHash(data) => unified::Receiver::P2sh(*data),
})); }));
let ua = unified::Address::try_from_items(items.collect()) let meta_items = self
.expect("UnifiedAddress should only be constructed safely"); .unknown_metadata
.iter()
.map(|(typecode, data)| unified::MetadataItem::Unknown {
typecode: *typecode,
data: data.clone(),
})
.chain(
self.expiry_height
.map(|h| unified::MetadataItem::ExpiryHeight(u32::from(h))),
)
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
let ua = unified::Address::try_from_items(
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))
.collect(),
)
.expect("UnifiedAddress should only be constructed safely");
ZcashAddress::from_unified(net, ua) ZcashAddress::from_unified(net, ua)
} }
@ -217,17 +296,17 @@ impl UnifiedAddress {
pub fn receiver_types(&self) -> Vec<Typecode> { pub fn receiver_types(&self) -> Vec<Typecode> {
let result = std::iter::empty(); let result = std::iter::empty();
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let result = result.chain(self.orchard.map(|_| Typecode::Orchard)); let result = result.chain(self.orchard.map(|_| Typecode::ORCHARD));
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
let result = result.chain(self.sapling.map(|_| Typecode::Sapling)); let result = result.chain(self.sapling.map(|_| Typecode::SAPLING));
let result = result.chain(self.transparent.map(|taddr| match taddr { let result = result.chain(self.transparent.map(|taddr| match taddr {
TransparentAddress::PublicKeyHash(_) => Typecode::P2pkh, TransparentAddress::PublicKeyHash(_) => Typecode::P2PKH,
TransparentAddress::ScriptHash(_) => Typecode::P2sh, TransparentAddress::ScriptHash(_) => Typecode::P2SH,
})); }));
let result = result.chain( let result = result.chain(
self.unknown() self.unknown_data()
.iter() .iter()
.map(|(typecode, _)| Typecode::Unknown(*typecode)), .map(|(typecode, _)| Typecode::Data(DataTypecode::Unknown(*typecode))),
); );
result.collect() result.collect()
} }
@ -256,7 +335,8 @@ impl Receiver {
match self { match self {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
Receiver::Orchard(addr) => { Receiver::Orchard(addr) => {
let receiver = unified::Receiver::Orchard(addr.to_raw_address_bytes()); let receiver =
unified::Item::Data(unified::Receiver::Orchard(addr.to_raw_address_bytes()));
let ua = unified::Address::try_from_items(vec![receiver]) let ua = unified::Address::try_from_items(vec![receiver])
.expect("A unified address may contain a single Orchard receiver."); .expect("A unified address may contain a single Orchard receiver.");
ZcashAddress::from_unified(net, ua) ZcashAddress::from_unified(net, ua)
@ -440,7 +520,7 @@ pub mod testing {
params: Network, params: Network,
request: UnifiedAddressRequest, request: UnifiedAddressRequest,
) -> impl Strategy<Value = UnifiedAddress> { ) -> impl Strategy<Value = UnifiedAddress> {
arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).0) arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).unwrap().0)
} }
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
@ -495,13 +575,13 @@ mod tests {
let transparent = None; let transparent = None;
#[cfg(all(feature = "orchard", feature = "sapling"))] #[cfg(all(feature = "orchard", feature = "sapling"))]
let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap(); let ua = UnifiedAddress::new_internal(orchard, sapling, transparent, None, None);
#[cfg(all(not(feature = "orchard"), feature = "sapling"))] #[cfg(all(not(feature = "orchard"), feature = "sapling"))]
let ua = UnifiedAddress::from_receivers(sapling, transparent).unwrap(); let ua = UnifiedAddress::new_internal(sapling, transparent, None, None);
#[cfg(all(feature = "orchard", not(feature = "sapling")))] #[cfg(all(feature = "orchard", not(feature = "sapling")))]
let ua = UnifiedAddress::from_receivers(orchard, transparent).unwrap(); let ua = UnifiedAddress::new_internal(orchard, transparent, None, None);
let addr = Address::Unified(ua); let addr = Address::Unified(ua);
let addr_str = addr.encode(&MAIN_NETWORK); let addr_str = addr.encode(&MAIN_NETWORK);
@ -512,7 +592,7 @@ mod tests {
#[cfg(not(any(feature = "orchard", feature = "sapling")))] #[cfg(not(any(feature = "orchard", feature = "sapling")))]
fn ua_round_trip() { fn ua_round_trip() {
let transparent = None; let transparent = None;
assert_eq!(UnifiedAddress::from_receivers(transparent), None) assert_eq!(UnifiedAddress::new_internal(transparent, None, None), None)
} }
#[test] #[test]

View File

@ -3,8 +3,8 @@ use std::{
error, error,
fmt::{self, Display}, fmt::{self, Display},
}; };
use zcash_address::unified::{self, Container, Encoding, Item, MetadataItem, Typecode};
use zcash_address::unified::{self, Container, Encoding, Typecode, Ufvk, Uivk}; use zcash_primitives::consensus::BlockHeight;
use zcash_protocol::consensus; use zcash_protocol::consensus;
use zip32::{AccountId, DiversifierIndex}; use zip32::{AccountId, DiversifierIndex};
@ -258,7 +258,10 @@ impl UnifiedSpendingKey {
sapling: Some(self.sapling.to_diversifiable_full_viewing_key()), sapling: Some(self.sapling.to_diversifiable_full_viewing_key()),
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard: Some((&self.orchard).into()), orchard: Some((&self.orchard).into()),
unknown: vec![], unknown_data: vec![],
expiry_height: None,
expiry_time: None,
unknown_metadata: vec![],
} }
} }
@ -298,7 +301,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
{ {
let orchard_key = self.orchard(); let orchard_key = self.orchard();
CompactSize::write(&mut result, usize::try_from(Typecode::Orchard).unwrap()).unwrap(); CompactSize::write(&mut result, usize::try_from(Typecode::ORCHARD).unwrap()).unwrap();
let orchard_key_bytes = orchard_key.to_bytes(); let orchard_key_bytes = orchard_key.to_bytes();
CompactSize::write(&mut result, orchard_key_bytes.len()).unwrap(); CompactSize::write(&mut result, orchard_key_bytes.len()).unwrap();
@ -308,7 +311,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
{ {
let sapling_key = self.sapling(); let sapling_key = self.sapling();
CompactSize::write(&mut result, usize::try_from(Typecode::Sapling).unwrap()).unwrap(); CompactSize::write(&mut result, usize::try_from(Typecode::SAPLING).unwrap()).unwrap();
let sapling_key_bytes = sapling_key.to_bytes(); let sapling_key_bytes = sapling_key.to_bytes();
CompactSize::write(&mut result, sapling_key_bytes.len()).unwrap(); CompactSize::write(&mut result, sapling_key_bytes.len()).unwrap();
@ -318,7 +321,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
{ {
let account_tkey = self.transparent(); let account_tkey = self.transparent();
CompactSize::write(&mut result, usize::try_from(Typecode::P2pkh).unwrap()).unwrap(); CompactSize::write(&mut result, usize::try_from(Typecode::P2PKH).unwrap()).unwrap();
let account_tkey_bytes = account_tkey.to_bytes(); let account_tkey_bytes = account_tkey.to_bytes();
CompactSize::write(&mut result, account_tkey_bytes.len()).unwrap(); CompactSize::write(&mut result, account_tkey_bytes.len()).unwrap();
@ -334,6 +337,8 @@ impl UnifiedSpendingKey {
#[allow(clippy::unnecessary_unwrap)] #[allow(clippy::unnecessary_unwrap)]
#[cfg(feature = "unstable")] #[cfg(feature = "unstable")]
pub fn from_bytes(era: Era, encoded: &[u8]) -> Result<Self, DecodingError> { pub fn from_bytes(era: Era, encoded: &[u8]) -> Result<Self, DecodingError> {
use zcash_address::unified::DataTypecode;
let mut source = std::io::Cursor::new(encoded); let mut source = std::io::Cursor::new(encoded);
let decoded_era = source let decoded_era = source
.read_u32::<LittleEndian>() .read_u32::<LittleEndian>()
@ -353,21 +358,23 @@ impl UnifiedSpendingKey {
loop { loop {
let tc = CompactSize::read_t::<_, u32>(&mut source) let tc = CompactSize::read_t::<_, u32>(&mut source)
.map_err(|_| DecodingError::ReadError("typecode")) .map_err(|_| DecodingError::ReadError("typecode"))
.and_then(|v| Typecode::try_from(v).map_err(|_| DecodingError::TypecodeInvalid))?; .and_then(|v| {
DataTypecode::try_from(v).map_err(|_| DecodingError::TypecodeInvalid)
})?;
let len = CompactSize::read_t::<_, u32>(&mut source) let len = CompactSize::read_t::<_, u32>(&mut source)
.map_err(|_| DecodingError::ReadError("key length"))?; .map_err(|_| DecodingError::ReadError("key length"))?;
match tc { match tc {
Typecode::Orchard => { DataTypecode::Orchard => {
if len != 32 { if len != 32 {
return Err(DecodingError::LengthMismatch(Typecode::Orchard, len)); return Err(DecodingError::LengthMismatch(Typecode::ORCHARD, len));
} }
let mut key = [0u8; 32]; let mut key = [0u8; 32];
source source
.read_exact(&mut key) .read_exact(&mut key)
.map_err(|_| DecodingError::InsufficientData(Typecode::Orchard))?; .map_err(|_| DecodingError::InsufficientData(Typecode::ORCHARD))?;
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
{ {
@ -375,43 +382,43 @@ impl UnifiedSpendingKey {
Option::<orchard::keys::SpendingKey>::from( Option::<orchard::keys::SpendingKey>::from(
orchard::keys::SpendingKey::from_bytes(key), orchard::keys::SpendingKey::from_bytes(key),
) )
.ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?, .ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?,
); );
} }
} }
Typecode::Sapling => { DataTypecode::Sapling => {
if len != 169 { if len != 169 {
return Err(DecodingError::LengthMismatch(Typecode::Sapling, len)); return Err(DecodingError::LengthMismatch(Typecode::SAPLING, len));
} }
let mut key = [0u8; 169]; let mut key = [0u8; 169];
source source
.read_exact(&mut key) .read_exact(&mut key)
.map_err(|_| DecodingError::InsufficientData(Typecode::Sapling))?; .map_err(|_| DecodingError::InsufficientData(Typecode::SAPLING))?;
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
{ {
sapling = Some( sapling = Some(
sapling::ExtendedSpendingKey::from_bytes(&key) sapling::ExtendedSpendingKey::from_bytes(&key)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::Sapling))?, .map_err(|_| DecodingError::KeyDataInvalid(Typecode::SAPLING))?,
); );
} }
} }
Typecode::P2pkh => { DataTypecode::P2pkh => {
if len != 64 { if len != 64 {
return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len)); return Err(DecodingError::LengthMismatch(Typecode::P2PKH, len));
} }
let mut key = [0u8; 64]; let mut key = [0u8; 64];
source source
.read_exact(&mut key) .read_exact(&mut key)
.map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?; .map_err(|_| DecodingError::InsufficientData(Typecode::P2PKH))?;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
{ {
transparent = Some( transparent = Some(
legacy::AccountPrivKey::from_bytes(&key) legacy::AccountPrivKey::from_bytes(&key)
.ok_or(DecodingError::KeyDataInvalid(Typecode::P2pkh))?, .ok_or(DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
); );
} }
} }
@ -444,7 +451,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard.unwrap(), orchard.unwrap(),
) )
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)); .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH));
} }
} }
} }
@ -453,10 +460,8 @@ impl UnifiedSpendingKey {
pub fn default_address( pub fn default_address(
&self, &self,
request: UnifiedAddressRequest, request: UnifiedAddressRequest,
) -> (UnifiedAddress, DiversifierIndex) { ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
self.to_unified_full_viewing_key() self.to_unified_full_viewing_key().default_address(request)
.default_address(request)
.unwrap()
} }
#[cfg(all( #[cfg(all(
@ -549,22 +554,28 @@ pub struct UnifiedAddressRequest {
has_orchard: bool, has_orchard: bool,
has_sapling: bool, has_sapling: bool,
has_p2pkh: bool, has_p2pkh: bool,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
} }
impl UnifiedAddressRequest { impl UnifiedAddressRequest {
/// Construct a new unified address request from its constituent parts. /// Construct a new unified address request from its constituent parts
/// pub fn new(
/// Returns `None` if the resulting unified address would not include at least one shielded receiver. has_orchard: bool,
pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option<Self> { has_sapling: bool,
let has_shielded_receiver = has_orchard || has_sapling; has_p2pkh: bool,
expiry_height: Option<BlockHeight>,
if !has_shielded_receiver { expiry_time: Option<u64>,
) -> Option<Self> {
if !(has_sapling || has_orchard || has_p2pkh) {
None None
} else { } else {
Some(Self { Some(Self {
has_orchard, has_orchard,
has_sapling, has_sapling,
has_p2pkh, has_p2pkh,
expiry_height,
expiry_time,
}) })
} }
} }
@ -584,21 +595,27 @@ impl UnifiedAddressRequest {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let _has_p2pkh = true; let _has_p2pkh = true;
Self::new(_has_orchard, _has_sapling, _has_p2pkh) Self::new(_has_orchard, _has_sapling, _has_p2pkh, None, None)
} }
/// Construct a new unified address request from its constituent parts. /// Construct a new unified address request from its constituent parts.
/// ///
/// Panics: at least one of `has_orchard` or `has_sapling` must be `true`. /// Panics: at least one of `has_orchard`, `has_sapling`, or `has_p2pkh` must be `true`.
pub const fn unsafe_new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Self { pub const fn unsafe_new_without_expiry(
if !(has_orchard || has_sapling) { has_orchard: bool,
panic!("At least one shielded receiver must be requested.") has_sapling: bool,
has_p2pkh: bool,
) -> Self {
if !(has_orchard || has_sapling || has_p2pkh) {
panic!("At least one receiver must be requested.")
} }
Self { Self {
has_orchard, has_orchard,
has_sapling, has_sapling,
has_p2pkh, has_p2pkh,
expiry_height: None,
expiry_time: None,
} }
} }
} }
@ -619,7 +636,10 @@ pub struct UnifiedFullViewingKey {
sapling: Option<sapling::DiversifiableFullViewingKey>, sapling: Option<sapling::DiversifiableFullViewingKey>,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard: Option<orchard::keys::FullViewingKey>, orchard: Option<orchard::keys::FullViewingKey>,
unknown: Vec<(u32, Vec<u8>)>, unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
} }
impl UnifiedFullViewingKey { impl UnifiedFullViewingKey {
@ -645,6 +665,9 @@ impl UnifiedFullViewingKey {
// We don't currently allow constructing new UFVKs with unknown items, but we store // We don't currently allow constructing new UFVKs with unknown items, but we store
// this to allow parsing such UFVKs. // this to allow parsing such UFVKs.
vec![], vec![],
None,
None,
vec![],
) )
} }
@ -654,7 +677,10 @@ impl UnifiedFullViewingKey {
#[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>, #[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>,
#[cfg(feature = "sapling")] sapling: Option<sapling::DiversifiableFullViewingKey>, #[cfg(feature = "sapling")] sapling: Option<sapling::DiversifiableFullViewingKey>,
#[cfg(feature = "orchard")] orchard: Option<orchard::keys::FullViewingKey>, #[cfg(feature = "orchard")] orchard: Option<orchard::keys::FullViewingKey>,
unknown: Vec<(u32, Vec<u8>)>, unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
) -> Result<UnifiedFullViewingKey, DerivationError> { ) -> Result<UnifiedFullViewingKey, DerivationError> {
// Verify that IVK derivation succeeds; we don't want to construct a UFVK // Verify that IVK derivation succeeds; we don't want to construct a UFVK
// that can't derive transparent addresses. // that can't derive transparent addresses.
@ -671,7 +697,10 @@ impl UnifiedFullViewingKey {
sapling, sapling,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
unknown, unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
}) })
} }
@ -679,7 +708,8 @@ impl UnifiedFullViewingKey {
/// ///
/// [ZIP 316]: https://zips.z.cash/zip-0316 /// [ZIP 316]: https://zips.z.cash/zip-0316
pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> { 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 (net, ufvk) =
zcash_address::unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?;
let expected_net = params.network_type(); let expected_net = params.network_type();
if net != expected_net { if net != expected_net {
return Err(format!( return Err(format!(
@ -694,64 +724,71 @@ impl UnifiedFullViewingKey {
/// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding. /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding.
/// ///
/// [ZIP 316]: https://zips.z.cash/zip-0316 /// [ZIP 316]: https://zips.z.cash/zip-0316
pub fn parse(ufvk: &Ufvk) -> Result<Self, DecodingError> { pub fn parse(ufvk: &zcash_address::unified::Ufvk) -> Result<Self, DecodingError> {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let mut orchard = None; let mut orchard = None;
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
let mut sapling = None; let mut sapling = None;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let mut transparent = None; let mut transparent = None;
let mut unknown_data = vec![];
let mut expiry_height = None;
let mut expiry_time = None;
let mut unknown_metadata = vec![];
// We can use as-parsed order here for efficiency, because we're breaking out the // We can use as-parsed order here for efficiency, because we're breaking out the
// receivers we support from the unknown receivers. // receivers we support from the unknown receivers.
let unknown = ufvk for item in ufvk.items_as_parsed() {
.items_as_parsed() match item {
.iter() Item::Data(unified::Fvk::Orchard(data)) => {
.filter_map(|receiver| match receiver { #[cfg(feature = "orchard")]
#[cfg(feature = "orchard")] {
unified::Fvk::Orchard(data) => orchard::keys::FullViewingKey::from_bytes(data) orchard = Some(
.ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard)) orchard::keys::FullViewingKey::from_bytes(data)
.map(|addr| { .ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?,
orchard = Some(addr); );
None }
})
.transpose(), #[cfg(not(feature = "orchard"))]
#[cfg(not(feature = "orchard"))] unknown_data.push((unified::DataTypecode::Orchard.into(), data.to_vec()));
unified::Fvk::Orchard(data) => Some(Ok::<_, DecodingError>((
u32::from(unified::Typecode::Orchard),
data.to_vec(),
))),
#[cfg(feature = "sapling")]
unified::Fvk::Sapling(data) => {
sapling::DiversifiableFullViewingKey::from_bytes(data)
.ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling))
.map(|pa| {
sapling = Some(pa);
None
})
.transpose()
} }
#[cfg(not(feature = "sapling"))] Item::Data(unified::Fvk::Sapling(data)) => {
unified::Fvk::Sapling(data) => Some(Ok::<_, DecodingError>(( #[cfg(feature = "sapling")]
u32::from(unified::Typecode::Sapling), {
data.to_vec(), sapling = Some(
))), sapling::DiversifiableFullViewingKey::from_bytes(data)
#[cfg(feature = "transparent-inputs")] .ok_or(DecodingError::KeyDataInvalid(Typecode::SAPLING))?,
unified::Fvk::P2pkh(data) => legacy::AccountPubKey::deserialize(data) );
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)) }
.map(|tfvk| { #[cfg(not(feature = "sapling"))]
transparent = Some(tfvk); unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec()));
None }
}) Item::Data(unified::Fvk::P2pkh(data)) => {
.transpose(), #[cfg(feature = "transparent-inputs")]
#[cfg(not(feature = "transparent-inputs"))] {
unified::Fvk::P2pkh(data) => Some(Ok::<_, DecodingError>(( transparent = Some(
u32::from(unified::Typecode::P2pkh), legacy::AccountPubKey::deserialize(data)
data.to_vec(), .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
))), );
unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))), }
})
.collect::<Result<_, _>>()?; #[cfg(not(feature = "transparent-inputs"))]
unknown_data.push((unified::DataTypecode::P2pkh.into(), data.to_vec()));
}
Item::Data(unified::Fvk::Unknown { typecode, data }) => {
unknown_data.push((*typecode, data.clone()));
}
Item::Metadata(MetadataItem::ExpiryHeight(h)) => {
expiry_height = Some(BlockHeight::from(*h));
}
Item::Metadata(MetadataItem::ExpiryTime(t)) => {
expiry_time = Some(*t);
}
Item::Metadata(MetadataItem::Unknown { typecode, data }) => {
unknown_metadata.push((*typecode, data.clone()));
}
}
}
Self::from_checked_parts( Self::from_checked_parts(
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -760,9 +797,12 @@ impl UnifiedFullViewingKey {
sapling, sapling,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
unknown, unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
) )
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)) .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))
} }
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
@ -771,37 +811,56 @@ impl UnifiedFullViewingKey {
} }
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
fn to_ufvk(&self) -> Ufvk { fn to_ufvk(&self) -> zcash_address::unified::Ufvk {
let items = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| { let data_items =
unified::Fvk::Unknown { std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| {
typecode: *typecode, unified::Fvk::Unknown {
data: data.clone(), typecode: *typecode,
} data: data.clone(),
})); }
}));
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let items = items.chain( let data_items = data_items.chain(
self.orchard self.orchard
.as_ref() .as_ref()
.map(|fvk| fvk.to_bytes()) .map(|fvk| fvk.to_bytes())
.map(unified::Fvk::Orchard), .map(unified::Fvk::Orchard),
); );
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
let items = items.chain( let data_items = data_items.chain(
self.sapling self.sapling
.as_ref() .as_ref()
.map(|dfvk| dfvk.to_bytes()) .map(|dfvk| dfvk.to_bytes())
.map(unified::Fvk::Sapling), .map(unified::Fvk::Sapling),
); );
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let items = items.chain( let data_items = data_items.chain(
self.transparent self.transparent
.as_ref() .as_ref()
.map(|tfvk| tfvk.serialize().try_into().unwrap()) .map(|tfvk| tfvk.serialize().try_into().unwrap())
.map(unified::Fvk::P2pkh), .map(unified::Fvk::P2pkh),
); );
unified::Ufvk::try_from_items(items.collect()) let meta_items = std::iter::empty()
.expect("UnifiedFullViewingKey should only be constructed safely") .chain(self.unknown_metadata.iter().map(|(typecode, data)| {
unified::MetadataItem::Unknown {
typecode: *typecode,
data: data.clone(),
}
}))
.chain(
self.expiry_height
.map(|h| unified::MetadataItem::ExpiryHeight(u32::from(h))),
)
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
zcash_address::unified::Ufvk::try_from_items(
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))
.collect(),
)
.expect("UnifiedFullViewingKey should only be constructed safely")
} }
/// Derives a Unified Incoming Viewing Key from this Unified Full Viewing Key. /// Derives a Unified Incoming Viewing Key from this Unified Full Viewing Key.
@ -816,7 +875,11 @@ impl UnifiedFullViewingKey {
sapling: self.sapling.as_ref().map(|s| s.to_external_ivk()), sapling: self.sapling.as_ref().map(|s| s.to_external_ivk()),
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard: self.orchard.as_ref().map(|o| o.to_ivk(Scope::External)), orchard: self.orchard.as_ref().map(|o| o.to_ivk(Scope::External)),
unknown: Vec::new(), expiry_height: self.expiry_height,
expiry_time: self.expiry_time,
// We cannot translate unknown data or metadata items, as they may not be relevant to the IVK
unknown_data: vec![],
unknown_metadata: vec![],
} }
} }
@ -839,6 +902,26 @@ impl UnifiedFullViewingKey {
self.orchard.as_ref() self.orchard.as_ref()
} }
/// Returns any unknown data items parsed from the encoded form of the key.
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
self.unknown_data.as_ref()
}
/// Returns the expiration height for this key.
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
/// Returns the expiration time for this key.
pub fn expiry_time(&self) -> Option<u64> {
self.expiry_time
}
/// Returns any unknown metadata items parsed from the encoded form of the key.
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
self.unknown_metadata.as_ref()
}
/// Attempts to derive the Unified Address for the given diversifier index and /// Attempts to derive the Unified Address for the given diversifier index and
/// receiver types. /// receiver types.
/// ///
@ -857,10 +940,9 @@ impl UnifiedFullViewingKey {
/// ///
/// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features
/// required to satisfy the unified address request are not properly enabled. /// required to satisfy the unified address request are not properly enabled.
#[allow(unused_mut)]
pub fn find_address( pub fn find_address(
&self, &self,
mut j: DiversifierIndex, j: DiversifierIndex,
request: UnifiedAddressRequest, request: UnifiedAddressRequest,
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
self.to_unified_incoming_viewing_key() self.to_unified_incoming_viewing_key()
@ -889,8 +971,10 @@ pub struct UnifiedIncomingViewingKey {
sapling: Option<::sapling::zip32::IncomingViewingKey>, sapling: Option<::sapling::zip32::IncomingViewingKey>,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard: Option<orchard::keys::IncomingViewingKey>, orchard: Option<orchard::keys::IncomingViewingKey>,
/// Stores the unrecognized elements of the unified encoding. unknown_data: Vec<(u32, Vec<u8>)>,
unknown: Vec<(u32, Vec<u8>)>, expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
} }
impl UnifiedIncomingViewingKey { impl UnifiedIncomingViewingKey {
@ -906,7 +990,10 @@ impl UnifiedIncomingViewingKey {
>, >,
#[cfg(feature = "sapling")] sapling: Option<::sapling::zip32::IncomingViewingKey>, #[cfg(feature = "sapling")] sapling: Option<::sapling::zip32::IncomingViewingKey>,
#[cfg(feature = "orchard")] orchard: Option<orchard::keys::IncomingViewingKey>, #[cfg(feature = "orchard")] orchard: Option<orchard::keys::IncomingViewingKey>,
// TODO: Implement construction of UIVKs with metadata items. unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
) -> UnifiedIncomingViewingKey { ) -> UnifiedIncomingViewingKey {
UnifiedIncomingViewingKey { UnifiedIncomingViewingKey {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -917,7 +1004,10 @@ impl UnifiedIncomingViewingKey {
orchard, orchard,
// We don't allow constructing new UFVKs with unknown items, but we store // We don't allow constructing new UFVKs with unknown items, but we store
// this to allow parsing such UFVKs. // this to allow parsing such UFVKs.
unknown: vec![], unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
} }
} }
@ -925,7 +1015,7 @@ impl UnifiedIncomingViewingKey {
/// ///
/// [ZIP 316]: https://zips.z.cash/zip-0316 /// [ZIP 316]: https://zips.z.cash/zip-0316
pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> { pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> {
let (net, ufvk) = unified::Uivk::decode(encoding).map_err(|e| e.to_string())?; let (net, uivk) = unified::Uivk::decode(encoding).map_err(|e| e.to_string())?;
let expected_net = params.network_type(); let expected_net = params.network_type();
if net != expected_net { if net != expected_net {
return Err(format!( return Err(format!(
@ -934,62 +1024,73 @@ impl UnifiedIncomingViewingKey {
)); ));
} }
Self::parse(&ufvk).map_err(|e| e.to_string()) Self::parse(&uivk).map_err(|e| e.to_string())
} }
/// Constructs a unified incoming viewing key from a parsed unified encoding. /// Constructs a unified incoming viewing key from a parsed unified encoding.
fn parse(uivk: &Uivk) -> Result<Self, DecodingError> { fn parse(uivk: &zcash_address::unified::Uivk) -> Result<Self, DecodingError> {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let mut orchard = None; let mut orchard = None;
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
let mut sapling = None; let mut sapling = None;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let mut transparent = None; let mut transparent = None;
let mut unknown_data = vec![];
let mut unknown = vec![]; let mut expiry_height = None;
let mut expiry_time = None;
let mut unknown_metadata = vec![];
// We can use as-parsed order here for efficiency, because we're breaking out the // We can use as-parsed order here for efficiency, because we're breaking out the
// receivers we support from the unknown receivers. // receivers we support from the unknown receivers.
for receiver in uivk.items_as_parsed() { for receiver in uivk.items_as_parsed() {
match receiver { match receiver {
unified::Ivk::Orchard(data) => { Item::Data(unified::Ivk::Orchard(data)) => {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
{ {
orchard = Some( orchard = Some(
Option::from(orchard::keys::IncomingViewingKey::from_bytes(data)) Option::from(orchard::keys::IncomingViewingKey::from_bytes(data))
.ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?, .ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?,
); );
} }
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
unknown.push((u32::from(unified::Typecode::Orchard), data.to_vec())); unknown_data.push((u32::from(unified::Typecode::ORCHARD), data.to_vec()));
} }
unified::Ivk::Sapling(data) => { Item::Data(unified::Ivk::Sapling(data)) => {
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
{ {
sapling = Some( sapling = Some(
Option::from(::sapling::zip32::IncomingViewingKey::from_bytes(data)) Option::from(::sapling::zip32::IncomingViewingKey::from_bytes(data))
.ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling))?, .ok_or(DecodingError::KeyDataInvalid(Typecode::SAPLING))?,
); );
} }
#[cfg(not(feature = "sapling"))] #[cfg(not(feature = "sapling"))]
unknown.push((u32::from(unified::Typecode::Sapling), data.to_vec())); unknown_data.push((u32::from(unified::Typecode::SAPLING), data.to_vec()));
} }
unified::Ivk::P2pkh(data) => { Item::Data(unified::Ivk::P2pkh(data)) => {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
{ {
transparent = Some( transparent = Some(
legacy::ExternalIvk::deserialize(data) legacy::ExternalIvk::deserialize(data)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))?, .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
); );
} }
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
unknown.push((u32::from(unified::Typecode::P2pkh), data.to_vec())); unknown_data.push((u32::from(unified::Typecode::P2PKH), data.to_vec()));
} }
unified::Ivk::Unknown { typecode, data } => { Item::Data(unified::Ivk::Unknown { typecode, data }) => {
unknown.push((*typecode, data.clone())); unknown_data.push((*typecode, data.clone()));
}
Item::Metadata(MetadataItem::ExpiryHeight(h)) => {
expiry_height = Some(BlockHeight::from(*h));
}
Item::Metadata(MetadataItem::ExpiryTime(t)) => {
expiry_time = Some(*t);
}
Item::Metadata(MetadataItem::Unknown { typecode, data }) => {
unknown_metadata.push((*typecode, data.clone()));
} }
} }
} }
@ -1001,7 +1102,10 @@ impl UnifiedIncomingViewingKey {
sapling, sapling,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
unknown, unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
}) })
} }
@ -1011,37 +1115,56 @@ impl UnifiedIncomingViewingKey {
} }
/// Converts this unified incoming viewing key to a unified encoding. /// Converts this unified incoming viewing key to a unified encoding.
fn render(&self) -> Uivk { fn render(&self) -> zcash_address::unified::Uivk {
let items = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| { let data_items =
unified::Ivk::Unknown { std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| {
typecode: *typecode, unified::Ivk::Unknown {
data: data.clone(), typecode: *typecode,
} data: data.clone(),
})); }
}));
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let items = items.chain( let data_items = data_items.chain(
self.orchard self.orchard
.as_ref() .as_ref()
.map(|ivk| ivk.to_bytes()) .map(|ivk| ivk.to_bytes())
.map(unified::Ivk::Orchard), .map(unified::Ivk::Orchard),
); );
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
let items = items.chain( let data_items = data_items.chain(
self.sapling self.sapling
.as_ref() .as_ref()
.map(|divk| divk.to_bytes()) .map(|divk| divk.to_bytes())
.map(unified::Ivk::Sapling), .map(unified::Ivk::Sapling),
); );
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let items = items.chain( let data_items = data_items.chain(
self.transparent self.transparent
.as_ref() .as_ref()
.map(|tivk| tivk.serialize().try_into().unwrap()) .map(|tivk| tivk.serialize().try_into().unwrap())
.map(unified::Ivk::P2pkh), .map(unified::Ivk::P2pkh),
); );
unified::Uivk::try_from_items(items.collect()) let meta_items = std::iter::empty()
.expect("UnifiedIncomingViewingKey should only be constructed safely.") .chain(self.unknown_metadata.iter().map(|(typecode, data)| {
unified::MetadataItem::Unknown {
typecode: *typecode,
data: data.clone(),
}
}))
.chain(
self.expiry_height
.map(|h| unified::MetadataItem::ExpiryHeight(u32::from(h))),
)
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
zcash_address::unified::Uivk::try_from_items(
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))
.collect(),
)
.expect("UnifiedIncomingViewingKey should only be constructed safely.")
} }
/// Returns the Transparent external IVK, if present. /// Returns the Transparent external IVK, if present.
@ -1062,6 +1185,26 @@ impl UnifiedIncomingViewingKey {
&self.orchard &self.orchard
} }
/// Returns any unknown data items parsed from the encoded form of the key.
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
self.unknown_data.as_ref()
}
/// Returns the expiration height for this key.
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
/// Returns the expiration time for this key.
pub fn expiry_time(&self) -> Option<u64> {
self.expiry_time
}
/// Returns any unknown metadata items parsed from the encoded form of the key.
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
self.unknown_metadata.as_ref()
}
/// Attempts to derive the Unified Address for the given diversifier index and /// Attempts to derive the Unified Address for the given diversifier index and
/// receiver types. /// receiver types.
/// ///
@ -1076,7 +1219,7 @@ impl UnifiedIncomingViewingKey {
if request.has_orchard { if request.has_orchard {
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported( return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::Orchard, Typecode::ORCHARD,
)); ));
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
@ -1084,7 +1227,7 @@ impl UnifiedIncomingViewingKey {
let orchard_j = orchard::keys::DiversifierIndex::from(*_j.as_bytes()); let orchard_j = orchard::keys::DiversifierIndex::from(*_j.as_bytes());
orchard = Some(oivk.address_at(orchard_j)) orchard = Some(oivk.address_at(orchard_j))
} else { } else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard)); return Err(AddressGenerationError::KeyNotAvailable(Typecode::ORCHARD));
} }
} }
@ -1093,7 +1236,7 @@ impl UnifiedIncomingViewingKey {
if request.has_sapling { if request.has_sapling {
#[cfg(not(feature = "sapling"))] #[cfg(not(feature = "sapling"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported( return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::Sapling, Typecode::SAPLING,
)); ));
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
@ -1106,7 +1249,7 @@ impl UnifiedIncomingViewingKey {
.ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(_j))?, .ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(_j))?,
); );
} else { } else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling)); return Err(AddressGenerationError::KeyNotAvailable(Typecode::SAPLING));
} }
} }
@ -1115,7 +1258,7 @@ impl UnifiedIncomingViewingKey {
if request.has_p2pkh { if request.has_p2pkh {
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported( return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::P2pkh, Typecode::P2PKH,
)); ));
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -1131,20 +1274,21 @@ impl UnifiedIncomingViewingKey {
.map_err(|_| AddressGenerationError::InvalidTransparentChildIndex(_j))?, .map_err(|_| AddressGenerationError::InvalidTransparentChildIndex(_j))?,
); );
} else { } else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh)); return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2PKH));
} }
} }
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
let transparent = None; let transparent = None;
UnifiedAddress::from_receivers( Ok(UnifiedAddress::new_internal(
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
#[cfg(feature = "sapling")] #[cfg(feature = "sapling")]
sapling, sapling,
transparent, transparent,
) std::cmp::min(self.expiry_height, request.expiry_height),
.ok_or(AddressGenerationError::ShieldedReceiverRequired) std::cmp::min(self.expiry_time, request.expiry_time),
))
} }
/// 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
@ -1153,7 +1297,6 @@ impl UnifiedIncomingViewingKey {
/// ///
/// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features
/// required to satisfy the unified address request are not properly enabled. /// required to satisfy the unified address request are not properly enabled.
#[allow(unused_mut)]
pub fn find_address( pub fn find_address(
&self, &self,
mut j: DiversifierIndex, mut j: DiversifierIndex,
@ -1181,6 +1324,8 @@ impl UnifiedIncomingViewingKey {
Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => { Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => {
if j.increment().is_err() { if j.increment().is_err() {
return Err(AddressGenerationError::DiversifierSpaceExhausted); return Err(AddressGenerationError::DiversifierSpaceExhausted);
} else {
continue;
} }
} }
Err(other) => { Err(other) => {
@ -1236,7 +1381,7 @@ mod tests {
#[cfg(any(feature = "sapling", feature = "orchard"))] #[cfg(any(feature = "sapling", feature = "orchard"))]
use { use {
super::{UnifiedFullViewingKey, UnifiedIncomingViewingKey}, super::{UnifiedFullViewingKey, UnifiedIncomingViewingKey},
zcash_address::unified::{Encoding, Uivk}, zcash_address::unified::Encoding,
}; };
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
@ -1372,13 +1517,13 @@ mod tests {
feature = "sapling", feature = "sapling",
feature = "transparent-inputs" feature = "transparent-inputs"
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 0); assert_eq!(decoded_with_t.unknown_data.len(), 0);
#[cfg(all( #[cfg(all(
feature = "orchard", feature = "orchard",
feature = "sapling", feature = "sapling",
not(feature = "transparent-inputs") not(feature = "transparent-inputs")
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 1); assert_eq!(decoded_with_t.unknown_data.len(), 1);
// Orchard enabled // Orchard enabled
#[cfg(all( #[cfg(all(
@ -1386,13 +1531,13 @@ mod tests {
not(feature = "sapling"), not(feature = "sapling"),
feature = "transparent-inputs" feature = "transparent-inputs"
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 1); assert_eq!(decoded_with_t.unknown_data.len(), 1);
#[cfg(all( #[cfg(all(
feature = "orchard", feature = "orchard",
not(feature = "sapling"), not(feature = "sapling"),
not(feature = "transparent-inputs") not(feature = "transparent-inputs")
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 2); assert_eq!(decoded_with_t.unknown_data.len(), 2);
// Sapling enabled // Sapling enabled
#[cfg(all( #[cfg(all(
@ -1400,13 +1545,13 @@ mod tests {
feature = "sapling", feature = "sapling",
feature = "transparent-inputs" feature = "transparent-inputs"
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 1); assert_eq!(decoded_with_t.unknown_data.len(), 1);
#[cfg(all( #[cfg(all(
not(feature = "orchard"), not(feature = "orchard"),
feature = "sapling", feature = "sapling",
not(feature = "transparent-inputs") not(feature = "transparent-inputs")
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 2); assert_eq!(decoded_with_t.unknown_data.len(), 2);
} }
#[test] #[test]
@ -1435,7 +1580,10 @@ mod tests {
} }
let ua = ufvk let ua = ufvk
.address(d_idx, UnifiedAddressRequest::unsafe_new(false, true, true)) .address(
d_idx,
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true),
)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
panic!( panic!(
"unified address generation failed for account {}: {:?}", "unified address generation failed for account {}: {:?}",
@ -1497,6 +1645,10 @@ mod tests {
sapling, sapling,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
vec![],
None,
None,
vec![],
); );
let encoded = uivk.render().encode(&NetworkType::Main); let encoded = uivk.render().encode(&NetworkType::Main);
@ -1517,7 +1669,7 @@ mod tests {
assert_eq!(encoded, _encoded_no_t); assert_eq!(encoded, _encoded_no_t);
} }
let decoded = UnifiedIncomingViewingKey::parse(&Uivk::decode(&encoded).unwrap().1).unwrap(); let decoded = UnifiedIncomingViewingKey::decode(&MAIN_NETWORK, &encoded).unwrap();
let reencoded = decoded.render().encode(&NetworkType::Main); let reencoded = decoded.render().encode(&NetworkType::Main);
assert_eq!(encoded, reencoded); assert_eq!(encoded, reencoded);
@ -1538,7 +1690,7 @@ mod tests {
); );
let decoded_with_t = let decoded_with_t =
UnifiedIncomingViewingKey::parse(&Uivk::decode(encoded_with_t).unwrap().1).unwrap(); UnifiedIncomingViewingKey::decode(&MAIN_NETWORK, encoded_with_t).unwrap();
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
assert_eq!( assert_eq!(
decoded_with_t.transparent.map(|t| t.serialize()), decoded_with_t.transparent.map(|t| t.serialize()),
@ -1551,13 +1703,13 @@ mod tests {
feature = "sapling", feature = "sapling",
feature = "transparent-inputs" feature = "transparent-inputs"
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 0); assert_eq!(decoded_with_t.unknown_data.len(), 0);
#[cfg(all( #[cfg(all(
feature = "orchard", feature = "orchard",
feature = "sapling", feature = "sapling",
not(feature = "transparent-inputs") not(feature = "transparent-inputs")
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 1); assert_eq!(decoded_with_t.unknown_data.len(), 1);
// Orchard enabled // Orchard enabled
#[cfg(all( #[cfg(all(
@ -1565,13 +1717,13 @@ mod tests {
not(feature = "sapling"), not(feature = "sapling"),
feature = "transparent-inputs" feature = "transparent-inputs"
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 1); assert_eq!(decoded_with_t.unknown_data.len(), 1);
#[cfg(all( #[cfg(all(
feature = "orchard", feature = "orchard",
not(feature = "sapling"), not(feature = "sapling"),
not(feature = "transparent-inputs") not(feature = "transparent-inputs")
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 2); assert_eq!(decoded_with_t.unknown_data.len(), 2);
// Sapling enabled // Sapling enabled
#[cfg(all( #[cfg(all(
@ -1579,13 +1731,13 @@ mod tests {
feature = "sapling", feature = "sapling",
feature = "transparent-inputs" feature = "transparent-inputs"
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 1); assert_eq!(decoded_with_t.unknown_data.len(), 1);
#[cfg(all( #[cfg(all(
not(feature = "orchard"), not(feature = "orchard"),
feature = "sapling", feature = "sapling",
not(feature = "transparent-inputs") not(feature = "transparent-inputs")
))] ))]
assert_eq!(decoded_with_t.unknown.len(), 2); assert_eq!(decoded_with_t.unknown_data.len(), 2);
} }
#[test] #[test]
@ -1616,7 +1768,10 @@ mod tests {
} }
let ua = uivk let ua = uivk
.address(d_idx, UnifiedAddressRequest::unsafe_new(false, true, true)) .address(
d_idx,
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true),
)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
panic!( panic!(
"unified address generation failed for account {}: {:?}", "unified address generation failed for account {}: {:?}",