From 34ec1e5bdb2119f2d39534106ef4e79d4769bbab Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 25 Jan 2024 14:12:19 -0700 Subject: [PATCH] zcash_keys: Update key and address types to include ZIP-316 metadata items. --- components/zip321/src/lib.rs | 1 + zcash_client_sqlite/src/lib.rs | 3 +- zcash_client_sqlite/src/wallet.rs | 8 +- zcash_client_sqlite/src/wallet/init.rs | 6 +- .../init/migrations/add_transaction_views.rs | 4 +- .../wallet/init/migrations/addresses_table.rs | 10 +- .../migrations/ensure_orchard_ua_receiver.rs | 4 +- .../wallet/init/migrations/ufvk_support.rs | 3 +- zcash_client_sqlite/src/wallet/orchard.rs | 5 +- zcash_keys/CHANGELOG.md | 8 +- zcash_keys/src/address.rs | 220 +++++--- zcash_keys/src/keys.rs | 485 ++++++++++++------ 12 files changed, 498 insertions(+), 259 deletions(-) diff --git a/components/zip321/src/lib.rs b/components/zip321/src/lib.rs index 91974da43..a12a117fd 100644 --- a/components/zip321/src/lib.rs +++ b/components/zip321/src/lib.rs @@ -799,6 +799,7 @@ pub mod testing { use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis}; use super::{MemoBytes, Payment, TransactionRequest}; + pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*"; prop_compose! { diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 733c35973..73d5a7e37 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -133,7 +133,7 @@ pub(crate) const UA_TRANSPARENT: bool = false; pub(crate) const UA_TRANSPARENT: bool = true; 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. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] @@ -1157,7 +1157,6 @@ impl WalletWrite for WalletDb Recipient::External(wallet_address, PoolType::Shielded(ShieldedProtocol::Orchard)) }; - wallet::put_sent_output( wdb.conn.0, *output.account(), diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 5e6540d4f..06e5f0d81 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -683,7 +683,7 @@ pub(crate) fn get_legacy_transparent_address( conn: &rusqlite::Connection, account_id: AccountId, ) -> Result, SqliteClientError> { - use zcash_address::unified::Container; + use zcash_address::unified::{Container, Item}; use zcash_primitives::legacy::keys::ExternalIvk; // Get the UIVK for the account. @@ -705,9 +705,9 @@ pub(crate) fn get_legacy_transparent_address( } // Derive the default transparent address (if it wasn't already part of a derived UA). - for item in uivk.items() { - if let Ivk::P2pkh(tivk_bytes) = item { - let tivk = ExternalIvk::deserialize(&tivk_bytes)?; + for item in uivk.items_as_parsed() { + if let Item::Data(Ivk::P2pkh(tivk_bytes)) = item { + let tivk = ExternalIvk::deserialize(tivk_bytes)?; return Ok(Some(tivk.default_address())); } } diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index 9fff7e3a3..db1308040 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -1418,7 +1418,8 @@ mod tests { // Unified addresses at the time of the addition of migrations did not contain an // 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( ufvk.default_address(ua_request) .expect("A valid default address exists for the UFVK") @@ -1545,7 +1546,8 @@ mod tests { assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork)); // 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 .get_next_available_address(account_id, ua_request) .unwrap() diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs index a84f99a05..4b32bcb32 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs @@ -443,12 +443,12 @@ mod tests { let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap(); let ufvk = usk.to_unified_full_viewing_key(); let (ua, _) = ufvk - .default_address(UnifiedAddressRequest::unsafe_new( + .default_address(UnifiedAddressRequest::unsafe_new_without_expiry( false, true, UA_TRANSPARENT, )) - .expect("A valid default address exists for the UFVK"); + .unwrap(); let taddr = ufvk .transparent() .and_then(|k| { diff --git a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs index 584679164..61966769a 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs @@ -86,7 +86,7 @@ impl RusqliteMigration for Migration

{ )); }; 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 { return Err(WalletMigrationError::CorruptedData(format!( @@ -157,11 +157,9 @@ impl RusqliteMigration for Migration

{ ], )?; - let (address, d_idx) = ufvk.default_address(UnifiedAddressRequest::unsafe_new( - false, - true, - UA_TRANSPARENT, - ))?; + let (address, d_idx) = ufvk.default_address( + UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT), + )?; insert_address(transaction, &self.params, account, d_idx, &address)?; } diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs b/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs index 8818b5f76..f02bd1bb8 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs @@ -70,7 +70,7 @@ impl RusqliteMigration for Migration

{ }; 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(); @@ -144,7 +144,7 @@ mod tests { .unwrap(); let (addr, diversifier_index) = ufvk - .default_address(UnifiedAddressRequest::unsafe_new( + .default_address(UnifiedAddressRequest::unsafe_new_without_expiry( false, true, UA_TRANSPARENT, diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs index b4af04235..278ad6964 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -83,7 +83,8 @@ impl RusqliteMigration for Migration

{ // our second assumption above, and we report this as corrupted data. 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([])?; while let Some(row) = rows.next()? { // We only need to check for the presence of the seed if we have keys that diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs index 373d76c01..6b44b5d08 100644 --- a/zcash_client_sqlite/src/wallet/orchard.rs +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -463,7 +463,6 @@ pub(crate) mod tests { None, None, ) - .unwrap() .into() } @@ -545,9 +544,7 @@ pub(crate) mod tests { return Ok(result.map(|(note, addr, memo)| { ( Note::Orchard(note), - UnifiedAddress::from_receivers(Some(addr), None, None) - .unwrap() - .into(), + UnifiedAddress::from_receivers(Some(addr), None, None).into(), MemoBytes::from_bytes(&memo).expect("correct length"), ) })); diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index aa8442608..4735d9baf 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -12,7 +12,12 @@ and this library adheres to Rust's notion of ## [0.2.0] - 2024-03-25 ### 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 std::error::Error for zcash_keys::keys::AddressGenerationError` - `impl From 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` feature flag. UFVKs should only be produced by derivation from the USK, or parsed from their string representation. +- `zcash_keys::address::UnifiedAddress::from_receivers` ### Fixed - `UnifiedFullViewingKey::find_address` can now find an address for a diversifier diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index 8d3e0e460..3a659c9ba 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -1,15 +1,17 @@ //! Structs for handling supported address types. use zcash_address::{ - unified::{self, Container, Encoding, Typecode}, + unified::{self, Container, DataTypecode, Encoding, Item, Typecode}, ConversionError, ToAddress, TryFromRawAddress, ZcashAddress, }; use zcash_primitives::legacy::TransparentAddress; -use zcash_protocol::consensus::{self, NetworkType}; +use zcash_protocol::{ + consensus::{self, BlockHeight, NetworkType}, + PoolType, ShieldedProtocol, +}; #[cfg(feature = "sapling")] use sapling::PaymentAddress; -use zcash_protocol::{PoolType, ShieldedProtocol}; /// A Unified Address. #[derive(Clone, Debug, PartialEq, Eq)] @@ -19,7 +21,10 @@ pub struct UnifiedAddress { #[cfg(feature = "sapling")] sapling: Option, transparent: Option, - unknown: Vec<(u32, Vec)>, + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, } impl TryFrom for UnifiedAddress { @@ -31,14 +36,16 @@ impl TryFrom for UnifiedAddress { #[cfg(feature = "sapling")] let mut sapling = None; let mut transparent = None; - - let mut unknown: Vec<(u32, Vec)> = vec![]; + 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 // receivers we support from the unknown receivers. for item in ua.items_as_parsed() { match item { - unified::Receiver::Orchard(data) => { + Item::Data(unified::Receiver::Orchard(data)) => { #[cfg(feature = "orchard")] { orchard = Some( @@ -48,11 +55,11 @@ impl TryFrom for UnifiedAddress { } #[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")] { sapling = Some( @@ -62,20 +69,26 @@ impl TryFrom for UnifiedAddress { } #[cfg(not(feature = "sapling"))] { - unknown.push((unified::Typecode::Sapling.into(), data.to_vec())); + unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec())); } } - - unified::Receiver::P2pkh(data) => { + Item::Data(unified::Receiver::P2pkh(data)) => { transparent = Some(TransparentAddress::PublicKeyHash(*data)); } - - unified::Receiver::P2sh(data) => { + Item::Data(unified::Receiver::P2sh(data)) => { transparent = Some(TransparentAddress::ScriptHash(*data)); } - - unified::Receiver::Unknown { typecode, data } => { - unknown.push((*typecode, data.clone())); + Item::Data(unified::Receiver::Unknown { typecode, data }) => { + unknown_data.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 for UnifiedAddress { #[cfg(feature = "sapling")] sapling, transparent, - unknown, + unknown_data, + expiry_height, + expiry_time, + unknown_metadata, }) } } @@ -94,36 +110,43 @@ impl TryFrom for UnifiedAddress { impl UnifiedAddress { /// Constructs a Unified Address from a given set of receivers. /// - /// Returns `None` if the receivers would produce an invalid Unified Address (namely, - /// if no shielded receiver is provided). + /// This method is only available when the `test-dependencies` feature is enabled, as + /// 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( #[cfg(feature = "orchard")] orchard: Option, #[cfg(feature = "sapling")] sapling: Option, transparent: Option, - // TODO: Add handling for address metadata items. - ) -> Option { - #[cfg(feature = "orchard")] - let has_orchard = orchard.is_some(); - #[cfg(not(feature = "orchard"))] - let has_orchard = false; + ) -> Self { + Self::new_internal( + #[cfg(feature = "orchard")] + orchard, + #[cfg(feature = "sapling")] + sapling, + transparent, + None, + None, + ) + } - #[cfg(feature = "sapling")] - let has_sapling = sapling.is_some(); - #[cfg(not(feature = "sapling"))] - let has_sapling = false; - - if has_orchard || has_sapling { - Some(Self { - #[cfg(feature = "orchard")] - orchard, - #[cfg(feature = "sapling")] - sapling, - transparent, - unknown: vec![], - }) - } else { - // UAs require at least one shielded receiver. - None + pub(crate) fn new_internal( + #[cfg(feature = "orchard")] orchard: Option, + #[cfg(feature = "sapling")] sapling: Option, + transparent: Option, + expiry_height: Option, + expiry_time: Option, + ) -> Self { + Self { + #[cfg(feature = "orchard")] + orchard, + #[cfg(feature = "sapling")] + sapling, + transparent, + unknown_data: vec![], + expiry_height, + expiry_time, + unknown_metadata: vec![], } } @@ -168,22 +191,60 @@ impl UnifiedAddress { self.transparent.as_ref() } - /// Returns the set of unknown receivers of the unified address. - pub fn unknown(&self) -> &[(u32, Vec)] { - &self.unknown + /// Returns any unknown data items parsed from the encoded form of the address. + pub fn unknown_data(&self) -> &[(u32, Vec)] { + self.unknown_data.as_ref() + } + + /// Returns the expiration height for this address. + pub fn expiry_height(&self) -> Option { + 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 { + 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)] { + self.unknown_metadata.as_ref() } fn to_address(&self, net: NetworkType) -> ZcashAddress { - let items = self - .unknown - .iter() - .map(|(typecode, data)| unified::Receiver::Unknown { - typecode: *typecode, - data: data.clone(), - }); + let data_items = + self.unknown_data + .iter() + .map(|(typecode, data)| unified::Receiver::Unknown { + typecode: *typecode, + data: data.clone(), + }); #[cfg(feature = "orchard")] - let items = items.chain( + let data_items = data_items.chain( self.orchard .as_ref() .map(|addr| addr.to_raw_address_bytes()) @@ -191,20 +252,38 @@ impl UnifiedAddress { ); #[cfg(feature = "sapling")] - let items = items.chain( + let data_items = data_items.chain( self.sapling .as_ref() .map(|pa| pa.to_bytes()) .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::ScriptHash(data) => unified::Receiver::P2sh(*data), })); - let ua = unified::Address::try_from_items(items.collect()) - .expect("UnifiedAddress should only be constructed safely"); + let meta_items = 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)); + + 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) } @@ -217,17 +296,17 @@ impl UnifiedAddress { pub fn receiver_types(&self) -> Vec { let result = std::iter::empty(); #[cfg(feature = "orchard")] - let result = result.chain(self.orchard.map(|_| Typecode::Orchard)); + let result = result.chain(self.orchard.map(|_| Typecode::ORCHARD)); #[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 { - TransparentAddress::PublicKeyHash(_) => Typecode::P2pkh, - TransparentAddress::ScriptHash(_) => Typecode::P2sh, + TransparentAddress::PublicKeyHash(_) => Typecode::P2PKH, + TransparentAddress::ScriptHash(_) => Typecode::P2SH, })); let result = result.chain( - self.unknown() + self.unknown_data() .iter() - .map(|(typecode, _)| Typecode::Unknown(*typecode)), + .map(|(typecode, _)| Typecode::Data(DataTypecode::Unknown(*typecode))), ); result.collect() } @@ -256,7 +335,8 @@ impl Receiver { match self { #[cfg(feature = "orchard")] 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]) .expect("A unified address may contain a single Orchard receiver."); ZcashAddress::from_unified(net, ua) @@ -440,7 +520,7 @@ pub mod testing { params: Network, request: UnifiedAddressRequest, ) -> impl Strategy { - 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")] @@ -495,13 +575,13 @@ mod tests { let transparent = None; #[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"))] - let ua = UnifiedAddress::from_receivers(sapling, transparent).unwrap(); + let ua = UnifiedAddress::new_internal(sapling, transparent, None, None); #[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_str = addr.encode(&MAIN_NETWORK); @@ -512,7 +592,7 @@ mod tests { #[cfg(not(any(feature = "orchard", feature = "sapling")))] fn ua_round_trip() { let transparent = None; - assert_eq!(UnifiedAddress::from_receivers(transparent), None) + assert_eq!(UnifiedAddress::new_internal(transparent, None, None), None) } #[test] diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index 0115e1528..563de4bec 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -3,8 +3,8 @@ use std::{ error, fmt::{self, Display}, }; - -use zcash_address::unified::{self, Container, Encoding, Typecode, Ufvk, Uivk}; +use zcash_address::unified::{self, Container, Encoding, Item, MetadataItem, Typecode}; +use zcash_primitives::consensus::BlockHeight; use zcash_protocol::consensus; use zip32::{AccountId, DiversifierIndex}; @@ -258,7 +258,10 @@ impl UnifiedSpendingKey { sapling: Some(self.sapling.to_diversifiable_full_viewing_key()), #[cfg(feature = "orchard")] 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")] { 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(); CompactSize::write(&mut result, orchard_key_bytes.len()).unwrap(); @@ -308,7 +311,7 @@ impl UnifiedSpendingKey { #[cfg(feature = "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(); CompactSize::write(&mut result, sapling_key_bytes.len()).unwrap(); @@ -318,7 +321,7 @@ impl UnifiedSpendingKey { #[cfg(feature = "transparent-inputs")] { 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(); CompactSize::write(&mut result, account_tkey_bytes.len()).unwrap(); @@ -334,6 +337,8 @@ impl UnifiedSpendingKey { #[allow(clippy::unnecessary_unwrap)] #[cfg(feature = "unstable")] pub fn from_bytes(era: Era, encoded: &[u8]) -> Result { + use zcash_address::unified::DataTypecode; + let mut source = std::io::Cursor::new(encoded); let decoded_era = source .read_u32::() @@ -353,21 +358,23 @@ impl UnifiedSpendingKey { loop { let tc = CompactSize::read_t::<_, u32>(&mut source) .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) .map_err(|_| DecodingError::ReadError("key length"))?; match tc { - Typecode::Orchard => { + DataTypecode::Orchard => { if len != 32 { - return Err(DecodingError::LengthMismatch(Typecode::Orchard, len)); + return Err(DecodingError::LengthMismatch(Typecode::ORCHARD, len)); } let mut key = [0u8; 32]; source .read_exact(&mut key) - .map_err(|_| DecodingError::InsufficientData(Typecode::Orchard))?; + .map_err(|_| DecodingError::InsufficientData(Typecode::ORCHARD))?; #[cfg(feature = "orchard")] { @@ -375,43 +382,43 @@ impl UnifiedSpendingKey { Option::::from( 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 { - return Err(DecodingError::LengthMismatch(Typecode::Sapling, len)); + return Err(DecodingError::LengthMismatch(Typecode::SAPLING, len)); } let mut key = [0u8; 169]; source .read_exact(&mut key) - .map_err(|_| DecodingError::InsufficientData(Typecode::Sapling))?; + .map_err(|_| DecodingError::InsufficientData(Typecode::SAPLING))?; #[cfg(feature = "sapling")] { sapling = Some( sapling::ExtendedSpendingKey::from_bytes(&key) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::Sapling))?, + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::SAPLING))?, ); } } - Typecode::P2pkh => { + DataTypecode::P2pkh => { if len != 64 { - return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len)); + return Err(DecodingError::LengthMismatch(Typecode::P2PKH, len)); } let mut key = [0u8; 64]; source .read_exact(&mut key) - .map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?; + .map_err(|_| DecodingError::InsufficientData(Typecode::P2PKH))?; #[cfg(feature = "transparent-inputs")] { transparent = Some( 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")] orchard.unwrap(), ) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)); + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH)); } } } @@ -453,10 +460,8 @@ impl UnifiedSpendingKey { pub fn default_address( &self, request: UnifiedAddressRequest, - ) -> (UnifiedAddress, DiversifierIndex) { - self.to_unified_full_viewing_key() - .default_address(request) - .unwrap() + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + self.to_unified_full_viewing_key().default_address(request) } #[cfg(all( @@ -549,22 +554,28 @@ pub struct UnifiedAddressRequest { has_orchard: bool, has_sapling: bool, has_p2pkh: bool, + expiry_height: Option, + expiry_time: Option, } impl UnifiedAddressRequest { - /// Construct a new unified address request from its constituent parts. - /// - /// Returns `None` if the resulting unified address would not include at least one shielded receiver. - pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option { - let has_shielded_receiver = has_orchard || has_sapling; - - if !has_shielded_receiver { + /// Construct a new unified address request from its constituent parts + pub fn new( + has_orchard: bool, + has_sapling: bool, + has_p2pkh: bool, + expiry_height: Option, + expiry_time: Option, + ) -> Option { + if !(has_sapling || has_orchard || has_p2pkh) { None } else { Some(Self { has_orchard, has_sapling, has_p2pkh, + expiry_height, + expiry_time, }) } } @@ -584,21 +595,27 @@ impl UnifiedAddressRequest { #[cfg(feature = "transparent-inputs")] 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. /// - /// Panics: at least one of `has_orchard` or `has_sapling` must be `true`. - pub const fn unsafe_new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Self { - if !(has_orchard || has_sapling) { - panic!("At least one shielded receiver must be requested.") + /// Panics: at least one of `has_orchard`, `has_sapling`, or `has_p2pkh` must be `true`. + pub const fn unsafe_new_without_expiry( + has_orchard: bool, + has_sapling: bool, + has_p2pkh: bool, + ) -> Self { + if !(has_orchard || has_sapling || has_p2pkh) { + panic!("At least one receiver must be requested.") } Self { has_orchard, has_sapling, has_p2pkh, + expiry_height: None, + expiry_time: None, } } } @@ -619,7 +636,10 @@ pub struct UnifiedFullViewingKey { sapling: Option, #[cfg(feature = "orchard")] orchard: Option, - unknown: Vec<(u32, Vec)>, + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, } impl UnifiedFullViewingKey { @@ -645,6 +665,9 @@ impl UnifiedFullViewingKey { // We don't currently allow constructing new UFVKs with unknown items, but we store // this to allow parsing such UFVKs. vec![], + None, + None, + vec![], ) } @@ -654,7 +677,10 @@ impl UnifiedFullViewingKey { #[cfg(feature = "transparent-inputs")] transparent: Option, #[cfg(feature = "sapling")] sapling: Option, #[cfg(feature = "orchard")] orchard: Option, - unknown: Vec<(u32, Vec)>, + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, ) -> Result { // Verify that IVK derivation succeeds; we don't want to construct a UFVK // that can't derive transparent addresses. @@ -671,7 +697,10 @@ impl UnifiedFullViewingKey { sapling, #[cfg(feature = "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 pub fn decode(params: &P, encoding: &str) -> Result { - 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(); if net != expected_net { return Err(format!( @@ -694,64 +724,71 @@ impl UnifiedFullViewingKey { /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding. /// /// [ZIP 316]: https://zips.z.cash/zip-0316 - pub fn parse(ufvk: &Ufvk) -> Result { + pub fn parse(ufvk: &zcash_address::unified::Ufvk) -> Result { #[cfg(feature = "orchard")] let mut orchard = None; #[cfg(feature = "sapling")] let mut sapling = None; #[cfg(feature = "transparent-inputs")] 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 // receivers we support from the unknown receivers. - let unknown = ufvk - .items_as_parsed() - .iter() - .filter_map(|receiver| match receiver { - #[cfg(feature = "orchard")] - unified::Fvk::Orchard(data) => orchard::keys::FullViewingKey::from_bytes(data) - .ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard)) - .map(|addr| { - orchard = Some(addr); - None - }) - .transpose(), - #[cfg(not(feature = "orchard"))] - 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() + for item in ufvk.items_as_parsed() { + match item { + Item::Data(unified::Fvk::Orchard(data)) => { + #[cfg(feature = "orchard")] + { + orchard = Some( + orchard::keys::FullViewingKey::from_bytes(data) + .ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?, + ); + } + + #[cfg(not(feature = "orchard"))] + unknown_data.push((unified::DataTypecode::Orchard.into(), data.to_vec())); } - #[cfg(not(feature = "sapling"))] - unified::Fvk::Sapling(data) => Some(Ok::<_, DecodingError>(( - u32::from(unified::Typecode::Sapling), - data.to_vec(), - ))), - #[cfg(feature = "transparent-inputs")] - unified::Fvk::P2pkh(data) => legacy::AccountPubKey::deserialize(data) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)) - .map(|tfvk| { - transparent = Some(tfvk); - None - }) - .transpose(), - #[cfg(not(feature = "transparent-inputs"))] - unified::Fvk::P2pkh(data) => Some(Ok::<_, DecodingError>(( - u32::from(unified::Typecode::P2pkh), - data.to_vec(), - ))), - unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))), - }) - .collect::>()?; + Item::Data(unified::Fvk::Sapling(data)) => { + #[cfg(feature = "sapling")] + { + sapling = Some( + sapling::DiversifiableFullViewingKey::from_bytes(data) + .ok_or(DecodingError::KeyDataInvalid(Typecode::SAPLING))?, + ); + } + #[cfg(not(feature = "sapling"))] + unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec())); + } + Item::Data(unified::Fvk::P2pkh(data)) => { + #[cfg(feature = "transparent-inputs")] + { + transparent = Some( + legacy::AccountPubKey::deserialize(data) + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?, + ); + } + + #[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( #[cfg(feature = "transparent-inputs")] @@ -760,9 +797,12 @@ impl UnifiedFullViewingKey { sapling, #[cfg(feature = "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. @@ -771,37 +811,56 @@ impl UnifiedFullViewingKey { } /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. - fn to_ufvk(&self) -> Ufvk { - let items = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| { - unified::Fvk::Unknown { - typecode: *typecode, - data: data.clone(), - } - })); + fn to_ufvk(&self) -> zcash_address::unified::Ufvk { + let data_items = + std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| { + unified::Fvk::Unknown { + typecode: *typecode, + data: data.clone(), + } + })); #[cfg(feature = "orchard")] - let items = items.chain( + let data_items = data_items.chain( self.orchard .as_ref() .map(|fvk| fvk.to_bytes()) .map(unified::Fvk::Orchard), ); #[cfg(feature = "sapling")] - let items = items.chain( + let data_items = data_items.chain( self.sapling .as_ref() .map(|dfvk| dfvk.to_bytes()) .map(unified::Fvk::Sapling), ); #[cfg(feature = "transparent-inputs")] - let items = items.chain( + let data_items = data_items.chain( self.transparent .as_ref() .map(|tfvk| tfvk.serialize().try_into().unwrap()) .map(unified::Fvk::P2pkh), ); - unified::Ufvk::try_from_items(items.collect()) - .expect("UnifiedFullViewingKey should only be constructed safely") + let meta_items = std::iter::empty() + .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. @@ -816,7 +875,11 @@ impl UnifiedFullViewingKey { sapling: self.sapling.as_ref().map(|s| s.to_external_ivk()), #[cfg(feature = "orchard")] 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() } + /// Returns any unknown data items parsed from the encoded form of the key. + pub fn unknown_data(&self) -> &[(u32, Vec)] { + self.unknown_data.as_ref() + } + + /// Returns the expiration height for this key. + pub fn expiry_height(&self) -> Option { + self.expiry_height + } + + /// Returns the expiration time for this key. + pub fn expiry_time(&self) -> Option { + self.expiry_time + } + + /// Returns any unknown metadata items parsed from the encoded form of the key. + pub fn unknown_metadata(&self) -> &[(u32, Vec)] { + self.unknown_metadata.as_ref() + } + /// Attempts to derive the Unified Address for the given diversifier index and /// receiver types. /// @@ -857,10 +940,9 @@ impl UnifiedFullViewingKey { /// /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features /// required to satisfy the unified address request are not properly enabled. - #[allow(unused_mut)] pub fn find_address( &self, - mut j: DiversifierIndex, + j: DiversifierIndex, request: UnifiedAddressRequest, ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { self.to_unified_incoming_viewing_key() @@ -889,8 +971,10 @@ pub struct UnifiedIncomingViewingKey { sapling: Option<::sapling::zip32::IncomingViewingKey>, #[cfg(feature = "orchard")] orchard: Option, - /// Stores the unrecognized elements of the unified encoding. - unknown: Vec<(u32, Vec)>, + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, } impl UnifiedIncomingViewingKey { @@ -906,7 +990,10 @@ impl UnifiedIncomingViewingKey { >, #[cfg(feature = "sapling")] sapling: Option<::sapling::zip32::IncomingViewingKey>, #[cfg(feature = "orchard")] orchard: Option, - // TODO: Implement construction of UIVKs with metadata items. + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, ) -> UnifiedIncomingViewingKey { UnifiedIncomingViewingKey { #[cfg(feature = "transparent-inputs")] @@ -917,7 +1004,10 @@ impl UnifiedIncomingViewingKey { orchard, // We don't allow constructing new UFVKs with unknown items, but we store // 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 pub fn decode(params: &P, encoding: &str) -> Result { - 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(); if net != expected_net { 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. - fn parse(uivk: &Uivk) -> Result { + fn parse(uivk: &zcash_address::unified::Uivk) -> Result { #[cfg(feature = "orchard")] let mut orchard = None; #[cfg(feature = "sapling")] let mut sapling = None; #[cfg(feature = "transparent-inputs")] let mut transparent = None; - - let mut unknown = vec![]; + 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 // receivers we support from the unknown receivers. for receiver in uivk.items_as_parsed() { match receiver { - unified::Ivk::Orchard(data) => { + Item::Data(unified::Ivk::Orchard(data)) => { #[cfg(feature = "orchard")] { orchard = Some( Option::from(orchard::keys::IncomingViewingKey::from_bytes(data)) - .ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?, + .ok_or(DecodingError::KeyDataInvalid(Typecode::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")] { sapling = Some( Option::from(::sapling::zip32::IncomingViewingKey::from_bytes(data)) - .ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling))?, + .ok_or(DecodingError::KeyDataInvalid(Typecode::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")] { transparent = Some( legacy::ExternalIvk::deserialize(data) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))?, + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?, ); } #[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 } => { - unknown.push((*typecode, data.clone())); + Item::Data(unified::Ivk::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())); } } } @@ -1001,7 +1102,10 @@ impl UnifiedIncomingViewingKey { sapling, #[cfg(feature = "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. - fn render(&self) -> Uivk { - let items = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| { - unified::Ivk::Unknown { - typecode: *typecode, - data: data.clone(), - } - })); + fn render(&self) -> zcash_address::unified::Uivk { + let data_items = + std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| { + unified::Ivk::Unknown { + typecode: *typecode, + data: data.clone(), + } + })); #[cfg(feature = "orchard")] - let items = items.chain( + let data_items = data_items.chain( self.orchard .as_ref() .map(|ivk| ivk.to_bytes()) .map(unified::Ivk::Orchard), ); #[cfg(feature = "sapling")] - let items = items.chain( + let data_items = data_items.chain( self.sapling .as_ref() .map(|divk| divk.to_bytes()) .map(unified::Ivk::Sapling), ); #[cfg(feature = "transparent-inputs")] - let items = items.chain( + let data_items = data_items.chain( self.transparent .as_ref() .map(|tivk| tivk.serialize().try_into().unwrap()) .map(unified::Ivk::P2pkh), ); - unified::Uivk::try_from_items(items.collect()) - .expect("UnifiedIncomingViewingKey should only be constructed safely.") + let meta_items = std::iter::empty() + .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. @@ -1062,6 +1185,26 @@ impl UnifiedIncomingViewingKey { &self.orchard } + /// Returns any unknown data items parsed from the encoded form of the key. + pub fn unknown_data(&self) -> &[(u32, Vec)] { + self.unknown_data.as_ref() + } + + /// Returns the expiration height for this key. + pub fn expiry_height(&self) -> Option { + self.expiry_height + } + + /// Returns the expiration time for this key. + pub fn expiry_time(&self) -> Option { + self.expiry_time + } + + /// Returns any unknown metadata items parsed from the encoded form of the key. + pub fn unknown_metadata(&self) -> &[(u32, Vec)] { + self.unknown_metadata.as_ref() + } + /// Attempts to derive the Unified Address for the given diversifier index and /// receiver types. /// @@ -1076,7 +1219,7 @@ impl UnifiedIncomingViewingKey { if request.has_orchard { #[cfg(not(feature = "orchard"))] return Err(AddressGenerationError::ReceiverTypeNotSupported( - Typecode::Orchard, + Typecode::ORCHARD, )); #[cfg(feature = "orchard")] @@ -1084,7 +1227,7 @@ impl UnifiedIncomingViewingKey { let orchard_j = orchard::keys::DiversifierIndex::from(*_j.as_bytes()); orchard = Some(oivk.address_at(orchard_j)) } else { - return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard)); + return Err(AddressGenerationError::KeyNotAvailable(Typecode::ORCHARD)); } } @@ -1093,7 +1236,7 @@ impl UnifiedIncomingViewingKey { if request.has_sapling { #[cfg(not(feature = "sapling"))] return Err(AddressGenerationError::ReceiverTypeNotSupported( - Typecode::Sapling, + Typecode::SAPLING, )); #[cfg(feature = "sapling")] @@ -1106,7 +1249,7 @@ impl UnifiedIncomingViewingKey { .ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(_j))?, ); } else { - return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling)); + return Err(AddressGenerationError::KeyNotAvailable(Typecode::SAPLING)); } } @@ -1115,7 +1258,7 @@ impl UnifiedIncomingViewingKey { if request.has_p2pkh { #[cfg(not(feature = "transparent-inputs"))] return Err(AddressGenerationError::ReceiverTypeNotSupported( - Typecode::P2pkh, + Typecode::P2PKH, )); #[cfg(feature = "transparent-inputs")] @@ -1131,20 +1274,21 @@ impl UnifiedIncomingViewingKey { .map_err(|_| AddressGenerationError::InvalidTransparentChildIndex(_j))?, ); } else { - return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh)); + return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2PKH)); } } #[cfg(not(feature = "transparent-inputs"))] let transparent = None; - UnifiedAddress::from_receivers( + Ok(UnifiedAddress::new_internal( #[cfg(feature = "orchard")] orchard, #[cfg(feature = "sapling")] sapling, transparent, - ) - .ok_or(AddressGenerationError::ShieldedReceiverRequired) + std::cmp::min(self.expiry_height, request.expiry_height), + std::cmp::min(self.expiry_time, request.expiry_time), + )) } /// 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 /// required to satisfy the unified address request are not properly enabled. - #[allow(unused_mut)] pub fn find_address( &self, mut j: DiversifierIndex, @@ -1181,6 +1324,8 @@ impl UnifiedIncomingViewingKey { Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => { if j.increment().is_err() { return Err(AddressGenerationError::DiversifierSpaceExhausted); + } else { + continue; } } Err(other) => { @@ -1236,7 +1381,7 @@ mod tests { #[cfg(any(feature = "sapling", feature = "orchard"))] use { super::{UnifiedFullViewingKey, UnifiedIncomingViewingKey}, - zcash_address::unified::{Encoding, Uivk}, + zcash_address::unified::Encoding, }; #[cfg(feature = "orchard")] @@ -1372,13 +1517,13 @@ mod tests { feature = "sapling", feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 0); + assert_eq!(decoded_with_t.unknown_data.len(), 0); #[cfg(all( feature = "orchard", feature = "sapling", not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); // Orchard enabled #[cfg(all( @@ -1386,13 +1531,13 @@ mod tests { not(feature = "sapling"), feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); #[cfg(all( feature = "orchard", not(feature = "sapling"), not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 2); + assert_eq!(decoded_with_t.unknown_data.len(), 2); // Sapling enabled #[cfg(all( @@ -1400,13 +1545,13 @@ mod tests { feature = "sapling", feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); #[cfg(all( not(feature = "orchard"), feature = "sapling", not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 2); + assert_eq!(decoded_with_t.unknown_data.len(), 2); } #[test] @@ -1435,7 +1580,10 @@ mod tests { } 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| { panic!( "unified address generation failed for account {}: {:?}", @@ -1497,6 +1645,10 @@ mod tests { sapling, #[cfg(feature = "orchard")] orchard, + vec![], + None, + None, + vec![], ); let encoded = uivk.render().encode(&NetworkType::Main); @@ -1517,7 +1669,7 @@ mod tests { 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); assert_eq!(encoded, reencoded); @@ -1538,7 +1690,7 @@ mod tests { ); 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")] assert_eq!( decoded_with_t.transparent.map(|t| t.serialize()), @@ -1551,13 +1703,13 @@ mod tests { feature = "sapling", feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 0); + assert_eq!(decoded_with_t.unknown_data.len(), 0); #[cfg(all( feature = "orchard", feature = "sapling", not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); // Orchard enabled #[cfg(all( @@ -1565,13 +1717,13 @@ mod tests { not(feature = "sapling"), feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); #[cfg(all( feature = "orchard", not(feature = "sapling"), not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 2); + assert_eq!(decoded_with_t.unknown_data.len(), 2); // Sapling enabled #[cfg(all( @@ -1579,13 +1731,13 @@ mod tests { feature = "sapling", feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); #[cfg(all( not(feature = "orchard"), feature = "sapling", not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 2); + assert_eq!(decoded_with_t.unknown_data.len(), 2); } #[test] @@ -1616,7 +1768,10 @@ mod tests { } 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| { panic!( "unified address generation failed for account {}: {:?}",