diff --git a/components/zcash_address/CHANGELOG.md b/components/zcash_address/CHANGELOG.md index daa450611..00e90bc81 100644 --- a/components/zcash_address/CHANGELOG.md +++ b/components/zcash_address/CHANGELOG.md @@ -9,11 +9,26 @@ and this library adheres to Rust's notion of ### Added - `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as, matches_receiver}` -- `zcash_address::unified::Address::{can_receive_memo, has_receiver_of_type, contains_receiver}` +- `zcash_address::unified`: + - `Address::{can_receive_memo, has_receiver_of_type, contains_receiver, receivers}` + - `DataTypecode` + - `MetadataTypecode` + - `Item` + - `MetadataItem` - Module `zcash_address::testing` under the `test-dependencies` feature. - Module `zcash_address::unified::address::testing` under the `test-dependencies` feature. +### Changed +- `zcash_address::unified`: + - `Typecode` has changed. Instead of having a variant for each receiver type, + it now has two variants, `Typecode::Data` and `Typecode::Metadata`. + +### Removed +- `zcash_address::unified::Container::items` Preference order is only + significant when considering unified address receivers; use + `Address::receivers` instead. + ## [0.3.2] - 2024-03-06 ### Added - `zcash_address::convert`: diff --git a/components/zcash_address/Cargo.toml b/components/zcash_address/Cargo.toml index c51b9f764..aa2d8c9f7 100644 --- a/components/zcash_address/Cargo.toml +++ b/components/zcash_address/Cargo.toml @@ -28,6 +28,7 @@ proptest = { workspace = true, optional = true } [dev-dependencies] assert_matches.workspace = true +proptest.workspace = true [features] test-dependencies = ["dep:proptest"] diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index a0d3c117d..a39b24db2 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -180,7 +180,11 @@ mod tests { use assert_matches::assert_matches; use super::*; - use crate::{kind::unified, Network}; + use crate::{ + kind::unified, + unified::{Item, Receiver}, + Network, + }; fn encoding(encoded: &str, decoded: ZcashAddress) { assert_eq!(decoded.to_string(), encoded); @@ -230,21 +234,21 @@ mod tests { "u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl", ZcashAddress { net: Network::Main, - kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), + kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])), }, ); encoding( "utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s", ZcashAddress { net: Network::Test, - kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), + kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])), }, ); encoding( "uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3", ZcashAddress { net: Network::Regtest, - kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), + kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])), }, ); diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 8f5ea1903..5cda10971 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -6,6 +6,7 @@ use std::convert::{TryFrom, TryInto}; use std::error::Error; use std::fmt; use std::num::TryFromIntError; +use zcash_encoding::MAX_COMPACT_SIZE; use crate::Network; @@ -22,9 +23,9 @@ const PADDING_LEN: usize = 16; /// The known Receiver and Viewing Key types. /// /// The typecodes `0xFFFA..=0xFFFF` reserved for experiments are currently not -/// distinguished from unknown values, and will be parsed as [`Typecode::Unknown`]. +/// distinguished from unknown values, and will be parsed as [`DataTypecode::Unknown`]. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Typecode { +pub enum DataTypecode { /// A transparent P2PKH address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316). P2pkh, /// A transparent P2SH address. @@ -39,7 +40,37 @@ pub enum Typecode { Unknown(u32), } -impl Typecode { +impl TryFrom for DataTypecode { + type Error = (); + + fn try_from(typecode: u32) -> Result { + match typecode { + 0x00 => Ok(DataTypecode::P2pkh), + 0x01 => Ok(DataTypecode::P2sh), + 0x02 => Ok(DataTypecode::Sapling), + 0x03 => Ok(DataTypecode::Orchard), + 0x04..=0xBF | 0xFD..=MAX_COMPACT_SIZE => Ok(DataTypecode::Unknown(typecode)), + _ => Err(()), + } + } +} + +impl From for u32 { + fn from(t: DataTypecode) -> Self { + match t { + DataTypecode::P2pkh => 0x00, + DataTypecode::P2sh => 0x01, + DataTypecode::Sapling => 0x02, + DataTypecode::Orchard => 0x03, + DataTypecode::Unknown(typecode) => typecode, + } + } +} + +impl DataTypecode { + /// A total ordering over the data typecodes that can be used to sort + /// receivers and/or key items in order of decreasing priority, + /// as specified in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses) pub fn preference_order(a: &Self, b: &Self) -> cmp::Ordering { match (a, b) { // Trivial equality checks. @@ -69,51 +100,203 @@ impl Typecode { (_, Self::P2pkh) => cmp::Ordering::Greater, } } +} - pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering { - u32::from(*a).cmp(&u32::from(*b)) +/// The known Metadata Typecodes +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MetadataTypecode { + /// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316) + ExpiryHeight, + /// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316) + ExpiryTime, + /// An unknown MUST-understand metadata item as specified in + /// [ZIP 316, Revision 1](https://zips.z.cash/zip-0316) + /// + /// A parser encountering this typecode MUST halt with an error. + MustUnderstand(u32), + /// An unknown metadata item as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316) + Unknown(u32), +} + +impl TryFrom for MetadataTypecode { + type Error = (); + + fn try_from(typecode: u32) -> Result { + match typecode { + 0xC0..=0xDF => Ok(MetadataTypecode::Unknown(typecode)), + 0xE0 => Ok(MetadataTypecode::ExpiryHeight), + 0xE1 => Ok(MetadataTypecode::ExpiryTime), + 0xE2..=0xFC => Ok(MetadataTypecode::MustUnderstand(typecode)), + _ => Err(()), + } } } +impl From for u32 { + fn from(t: MetadataTypecode) -> Self { + match t { + MetadataTypecode::ExpiryHeight => 0xE0, + MetadataTypecode::ExpiryTime => 0xE1, + MetadataTypecode::MustUnderstand(value) => value, + MetadataTypecode::Unknown(value) => value, + } + } +} + +/// An enumeration of the Unified Container Item Typecodes. +/// +/// Unified Address Items are partitioned into two sets: data items, which include +/// receivers and viewing keys, and metadata items. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Typecode { + /// A data (receiver or viewing key) typecode. + Data(DataTypecode), + /// A metadata typecode. + Metadata(MetadataTypecode), +} + +impl Typecode { + /// The typecode for p2pkh data items. + pub const P2PKH: Typecode = Typecode::Data(DataTypecode::P2pkh); + /// The typecode for p2sh data items. + pub const P2SH: Typecode = Typecode::Data(DataTypecode::P2sh); + /// The typecode for Sapling data items. + pub const SAPLING: Typecode = Typecode::Data(DataTypecode::Sapling); + /// The typecode for Orchard data items. + pub const ORCHARD: Typecode = Typecode::Data(DataTypecode::Orchard); +} + impl TryFrom for Typecode { type Error = ParseError; fn try_from(typecode: u32) -> Result { - match typecode { - 0x00 => Ok(Typecode::P2pkh), - 0x01 => Ok(Typecode::P2sh), - 0x02 => Ok(Typecode::Sapling), - 0x03 => Ok(Typecode::Orchard), - 0x04..=0x02000000 => Ok(Typecode::Unknown(typecode)), - 0x02000001..=u32::MAX => Err(ParseError::InvalidTypecodeValue(typecode as u64)), - } + DataTypecode::try_from(typecode) + .map_or_else( + |()| MetadataTypecode::try_from(typecode).map(Typecode::Metadata), + |t| Ok(Typecode::Data(t)), + ) + .map_err(|()| ParseError::InvalidTypecodeValue(typecode)) } } impl From for u32 { fn from(t: Typecode) -> Self { match t { - Typecode::P2pkh => 0x00, - Typecode::P2sh => 0x01, - Typecode::Sapling => 0x02, - Typecode::Orchard => 0x03, - Typecode::Unknown(typecode) => typecode, + Typecode::Data(tc) => tc.into(), + Typecode::Metadata(tc) => tc.into(), } } } impl TryFrom for usize { type Error = TryFromIntError; + fn try_from(t: Typecode) -> Result { u32::from(t).try_into() } } -impl Typecode { - fn is_transparent(&self) -> bool { - // Unknown typecodes are treated as not transparent for the purpose of disallowing - // only-transparent UAs, which can be represented with existing address encodings. - matches!(self, Typecode::P2pkh | Typecode::P2sh) +/// An enumeration of known Unified Metadata Item types. +/// +/// Unknown MUST-understand metadata items are NOT represented using this type, as the presence of +/// an unrecognized metadata item with a typecode in the `MUST-understand` range will result in a +/// parse failure, instead of the construction of a metadata item. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum MetadataItem { + /// The expiry height for a Unified Address or Unified Viewing Key + ExpiryHeight(u32), + /// The expiry time for a Unified Address or Unified Viewing Key + ExpiryTime(u64), + /// A Metadata Item with an unrecognized Typecode. MUST-understand metadata items are NOT + /// represented using this type, as the presence of an unrecognized metadata item with a + /// typecode in the `MUST-understand` range will result in a parse failure. + Unknown { typecode: u32, data: Vec }, +} + +impl MetadataItem { + /// Parse a metadata item for the specified metadata typecode from the provided bytes. + pub fn parse(typecode: MetadataTypecode, data: &[u8]) -> Result { + match typecode { + MetadataTypecode::ExpiryHeight => data + .try_into() + .map(u32::from_le_bytes) + .map(MetadataItem::ExpiryHeight) + .map_err(|_| { + ParseError::InvalidEncoding( + "Expiry height must be a 32-bit little-endian value.".to_string(), + ) + }), + MetadataTypecode::ExpiryTime => data + .try_into() + .map(u64::from_le_bytes) + .map(MetadataItem::ExpiryTime) + .map_err(|_| { + ParseError::InvalidEncoding( + "Expiry time must be a 64-bit little-endian value.".to_string(), + ) + }), + MetadataTypecode::MustUnderstand(tc) => Err(ParseError::NotUnderstood(tc)), + MetadataTypecode::Unknown(typecode) => Ok(MetadataItem::Unknown { + typecode, + data: data.to_vec(), + }), + } + } + + /// Returns the typecode for this metadata item. + pub fn typecode(&self) -> MetadataTypecode { + match self { + MetadataItem::ExpiryHeight(_) => MetadataTypecode::ExpiryHeight, + MetadataItem::ExpiryTime(_) => MetadataTypecode::ExpiryTime, + MetadataItem::Unknown { typecode, .. } => MetadataTypecode::Unknown(*typecode), + } + } + + /// Returns the raw bytes of this metadata item. + pub fn data(&self) -> Vec { + match self { + MetadataItem::ExpiryHeight(h) => h.to_le_bytes().to_vec(), + MetadataItem::ExpiryTime(t) => t.to_le_bytes().to_vec(), + MetadataItem::Unknown { data, .. } => data.clone(), + } + } +} + +/// A Unified Encoding Item. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Item { + /// A data item; either a receiver (for Unified Addresses) or a key (for Unified Viewing Keys) + Data(T), + /// A metadata item. + Metadata(MetadataItem), +} + +impl Item { + /// Returns the typecode for this item. + pub fn typecode(&self) -> Typecode { + match self { + Item::Data(d) => Typecode::Data(d.typecode()), + Item::Metadata(m) => Typecode::Metadata(m.typecode()), + } + } + + /// The total ordering over items by their typecodes, used for encoding as specified + /// in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses) + pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering { + u32::from(a.typecode()).cmp(&u32::from(b.typecode())) + } + + /// Returns the raw binary representation of the data for this item. + pub fn data(&self) -> Vec { + match self { + Item::Data(d) => d.data().to_vec(), + Item::Metadata(m) => m.data(), + } + } + + /// Returns whether this item is a transparent receiver or key. + pub fn is_transparent_data_item(&self) -> bool { + self.typecode() == Typecode::P2PKH || self.typecode() == Typecode::P2SH } } @@ -125,7 +308,7 @@ pub enum ParseError { /// The unified container contains a duplicated typecode. DuplicateTypecode(Typecode), /// The parsed typecode exceeds the maximum allowed CompactSize value. - InvalidTypecodeValue(u64), + InvalidTypecodeValue(u32), /// The string is an invalid encoding. InvalidEncoding(String), /// The items in the unified container are not in typecode order. @@ -136,6 +319,8 @@ pub enum ParseError { NotUnified, /// The Bech32m string has an unrecognized human-readable prefix. UnknownPrefix(String), + /// A `MUST-understand` metadata item was not recognized. + NotUnderstood(u32), } impl fmt::Display for ParseError { @@ -151,6 +336,13 @@ impl fmt::Display for ParseError { ParseError::UnknownPrefix(s) => { write!(f, "Unrecognized Bech32m human-readable prefix: {}", s) } + ParseError::NotUnderstood(tc) => { + write!( + f, + "MUST-understand metadata item with typecode {} was not recognized; please upgrade.", + tc + ) + } } } } @@ -158,33 +350,27 @@ impl fmt::Display for ParseError { impl Error for ParseError {} pub(crate) mod private { - use super::{ParseError, Typecode, PADDING_LEN}; - use crate::Network; + use super::{DataTypecode, ParseError, Typecode, PADDING_LEN}; + use crate::{ + unified::{Item, MetadataItem}, + Network, + }; use std::{ - cmp, convert::{TryFrom, TryInto}, io::Write, }; use zcash_encoding::CompactSize; /// A raw address or viewing key. - pub trait SealedItem: for<'a> TryFrom<(u32, &'a [u8]), Error = ParseError> + Clone { - fn typecode(&self) -> Typecode; + pub trait SealedDataItem: Clone { + /// Parse a data item for the specified data typecode from the provided bytes. + fn parse(tc: DataTypecode, value: &[u8]) -> Result; + + /// Returns the typecode of this data item. + fn typecode(&self) -> DataTypecode; + + /// Returns the raw bytes of this data item. fn data(&self) -> &[u8]; - - fn preference_order(a: &Self, b: &Self) -> cmp::Ordering { - match Typecode::preference_order(&a.typecode(), &b.typecode()) { - cmp::Ordering::Equal => a.data().cmp(b.data()), - res => res, - } - } - - fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering { - match Typecode::encoding_order(&a.typecode(), &b.typecode()) { - cmp::Ordering::Equal => a.data().cmp(b.data()), - res => res, - } - } } /// A Unified Container containing addresses or viewing keys. @@ -196,7 +382,7 @@ pub(crate) mod private { /// Implementations of this method should act as unchecked constructors /// of the container type; the caller is guaranteed to check the /// general invariants that apply to all unified containers. - fn from_inner(items: Vec) -> Self; + fn from_inner(items: Vec>) -> Self; fn network_hrp(network: &Network) -> &'static str { match network { @@ -227,7 +413,7 @@ pub(crate) mod private { ) .unwrap(); CompactSize::write(&mut writer, data.len()).unwrap(); - writer.write_all(data).unwrap(); + writer.write_all(&data).unwrap(); } } @@ -248,10 +434,13 @@ pub(crate) mod private { } /// Parse the items of the unified container. - fn parse_items>>(hrp: &str, buf: T) -> Result, ParseError> { - fn read_receiver( + fn parse_items>>( + hrp: &str, + buf: T, + ) -> Result>, ParseError> { + fn read_receiver( mut cursor: &mut std::io::Cursor<&[u8]>, - ) -> Result { + ) -> Result, ParseError> { let typecode = CompactSize::read(&mut cursor) .map(|v| u32::try_from(v).expect("CompactSize::read enforces MAX_SIZE limit")) .map_err(|e| { @@ -279,12 +468,13 @@ pub(crate) mod private { length ))); } - let result = R::try_from(( - typecode, - &buf[cursor.position() as usize..addr_end as usize], - )); + let data = &buf[cursor.position() as usize..addr_end as usize]; + let result = match Typecode::try_from(typecode)? { + Typecode::Data(tc) => Item::Data(R::parse(tc, data)?), + Typecode::Metadata(tc) => Item::Metadata(MetadataItem::parse(tc, data)?), + }; cursor.set_position(addr_end); - result + Ok(result) } // Here we allocate if necessary to get a mutable Vec to unjumble. @@ -320,8 +510,8 @@ pub(crate) mod private { /// A private function that constructs a unified container with the /// specified items, which must be in ascending typecode order. - fn try_from_items_internal(items: Vec) -> Result { - assert!(u32::from(Typecode::P2sh) == u32::from(Typecode::P2pkh) + 1); + fn try_from_items_internal(items: Vec>) -> Result { + assert!(u32::from(Typecode::P2SH) == u32::from(Typecode::P2PKH) + 1); let mut only_transparent = true; let mut prev_code = None; // less than any Some @@ -332,13 +522,15 @@ pub(crate) mod private { return Err(ParseError::InvalidTypecodeOrder); } else if t_code == prev_code { return Err(ParseError::DuplicateTypecode(t)); - } else if t == Typecode::P2sh && prev_code == Some(u32::from(Typecode::P2pkh)) { + } else if t == Typecode::Data(DataTypecode::P2sh) + && prev_code == Some(u32::from(DataTypecode::P2pkh)) + { // P2pkh and P2sh can only be in that order and next to each other, // otherwise we would detect an out-of-order or duplicate typecode. return Err(ParseError::BothP2phkAndP2sh); } else { prev_code = t_code; - only_transparent = only_transparent && t.is_transparent(); + only_transparent = only_transparent && item.is_transparent_data_item(); } } @@ -356,13 +548,14 @@ pub(crate) mod private { } } -use private::SealedItem; +use private::SealedDataItem; /// Trait providing common encoding and decoding logic for Unified containers. pub trait Encoding: private::SealedContainer { - /// Constructs a value of a unified container type from a vector - /// of container items, sorted according to typecode as specified - /// in ZIP 316. + /// Constructs a value of a unified container type from a vector of container + /// items. These items will be sorted according to typecode as specified in ZIP + /// 316, so this method is not necessarily round-trip compatible with + /// [`Container::items_as_parsed`]. /// /// This function will return an error in the case that the following ZIP 316 /// invariants concerning the composition of a unified container are @@ -370,8 +563,8 @@ pub trait Encoding: private::SealedContainer { /// * the item list may not contain two items having the same typecode /// * the item list may not contain only transparent items (or no items) /// * the item list may not contain both P2PKH and P2SH items. - fn try_from_items(mut items: Vec) -> Result { - items.sort_unstable_by(Self::Item::encoding_order); + fn try_from_items(mut items: Vec>) -> Result { + items.sort_unstable_by(Item::encoding_order); Self::try_from_items_internal(items) } @@ -411,20 +604,9 @@ pub trait Encoding: private::SealedContainer { /// Trait for for Unified containers, that exposes the items within them. pub trait Container { - /// The type of item in this unified container. - type Item: SealedItem; + /// The type of data items in this unified container. + type DataItem: SealedDataItem; - /// Returns the items contained within this container, sorted in preference order. - fn items(&self) -> Vec { - let mut items = self.items_as_parsed().to_vec(); - // Unstable sorting is fine, because all items are guaranteed by construction - // to have distinct typecodes. - items.sort_unstable_by(Self::Item::preference_order); - items - } - - /// Returns the items in the order they were parsed from the string encoding. - /// - /// This API is for advanced usage; in most cases you should use `Self::items`. - fn items_as_parsed(&self) -> &[Self::Item]; + /// Returns the items in encoding order. + fn items_as_parsed(&self) -> &[Item]; } diff --git a/components/zcash_address/src/kind/unified/address.rs b/components/zcash_address/src/kind/unified/address.rs index 00d3c5c54..7b6e0f24f 100644 --- a/components/zcash_address/src/kind/unified/address.rs +++ b/components/zcash_address/src/kind/unified/address.rs @@ -1,10 +1,10 @@ use zcash_protocol::{PoolType, ShieldedProtocol}; -use super::{private::SealedItem, ParseError, Typecode}; +use super::{private::SealedDataItem, DataTypecode, Item, ParseError}; -use std::convert::{TryFrom, TryInto}; +use std::{cmp, convert::TryInto}; -/// The set of known Receivers for Unified Addresses. +/// The enumeration of Unified Address Receivers of known types. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Receiver { Orchard([u8; 43]), @@ -14,34 +14,39 @@ pub enum Receiver { Unknown { typecode: u32, data: Vec }, } -impl TryFrom<(u32, &[u8])> for Receiver { - type Error = ParseError; - - fn try_from((typecode, addr): (u32, &[u8])) -> Result { - match typecode.try_into()? { - Typecode::P2pkh => addr.try_into().map(Receiver::P2pkh), - Typecode::P2sh => addr.try_into().map(Receiver::P2sh), - Typecode::Sapling => addr.try_into().map(Receiver::Sapling), - Typecode::Orchard => addr.try_into().map(Receiver::Orchard), - Typecode::Unknown(_) => Ok(Receiver::Unknown { - typecode, - data: addr.to_vec(), - }), - } - .map_err(|e| { - ParseError::InvalidEncoding(format!("Invalid address for typecode {}: {}", typecode, e)) - }) +impl Receiver { + fn preference_order(a: &Self, b: &Self) -> cmp::Ordering { + DataTypecode::preference_order(&a.typecode(), &b.typecode()) } } -impl SealedItem for Receiver { - fn typecode(&self) -> Typecode { +impl SealedDataItem for Receiver { + fn parse(typecode: DataTypecode, data: &[u8]) -> Result { + match typecode { + DataTypecode::P2pkh => data.try_into().map(Receiver::P2pkh), + DataTypecode::P2sh => data.try_into().map(Receiver::P2sh), + DataTypecode::Sapling => data.try_into().map(Receiver::Sapling), + DataTypecode::Orchard => data.try_into().map(Receiver::Orchard), + DataTypecode::Unknown(typecode) => Ok(Receiver::Unknown { + typecode, + data: data.to_vec(), + }), + } + .map_err(|e| { + ParseError::InvalidEncoding(format!( + "Invalid address for typecode {:?}: {:?}", + typecode, e + )) + }) + } + + fn typecode(&self) -> DataTypecode { match self { - Receiver::P2pkh(_) => Typecode::P2pkh, - Receiver::P2sh(_) => Typecode::P2sh, - Receiver::Sapling(_) => Typecode::Sapling, - Receiver::Orchard(_) => Typecode::Orchard, - Receiver::Unknown { typecode, .. } => Typecode::Unknown(*typecode), + Receiver::P2pkh(_) => DataTypecode::P2pkh, + Receiver::P2sh(_) => DataTypecode::P2sh, + Receiver::Sapling(_) => DataTypecode::Sapling, + Receiver::Orchard(_) => DataTypecode::Orchard, + Receiver::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode), } } @@ -64,7 +69,7 @@ impl SealedItem for Receiver { /// # use std::convert::Infallible; /// # use std::error::Error; /// use zcash_address::{ -/// unified::{self, Container, Encoding}, +/// unified::{self, Container, Encoding, Item}, /// ConversionError, TryFromRawAddress, ZcashAddress, /// }; /// @@ -92,38 +97,69 @@ impl SealedItem for Receiver { /// /// // We can obtain the receivers for the UA in preference order /// // (the order in which wallets should prefer to use them): -/// let receivers: Vec = ua.items(); +/// let receivers: Vec = ua.receivers(); /// /// // And we can create the UA from a list of receivers: -/// let new_ua = unified::Address::try_from_items(receivers)?; +/// let new_ua = unified::Address::try_from_items(receivers.into_iter().map(Item::Data).collect())?; /// assert_eq!(new_ua, ua); /// # Ok(()) /// # } /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Address(pub(crate) Vec); +pub struct Address(pub(crate) Vec>); + +impl Address { + /// Returns the receiver items for this address, in order of decreasing preference. + /// + /// The receiver for a wallet to send to can safely be chosen by selecting the first receiver + /// of a type that wallet supports from the result. + pub fn receivers(&self) -> Vec { + let mut result = self + .0 + .iter() + .filter_map(|item| match item { + Item::Data(r) => Some(r.clone()), + Item::Metadata(_) => None, + }) + .collect::>(); + result.sort_unstable_by(Receiver::preference_order); + result + } +} impl Address { /// Returns whether this address has the ability to receive transfers of the given pool type. pub fn has_receiver_of_type(&self, pool_type: PoolType) -> bool { - self.0.iter().any(|r| match r { - Receiver::Orchard(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Orchard), - Receiver::Sapling(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Sapling), - Receiver::P2pkh(_) | Receiver::P2sh(_) => pool_type == PoolType::Transparent, - Receiver::Unknown { .. } => false, + self.0.iter().any(|item| match item { + Item::Data(Receiver::Orchard(_)) => { + pool_type == PoolType::Shielded(ShieldedProtocol::Orchard) + } + Item::Data(Receiver::Sapling(_)) => { + pool_type == PoolType::Shielded(ShieldedProtocol::Sapling) + } + Item::Data(Receiver::P2pkh(_)) | Item::Data(Receiver::P2sh(_)) => { + pool_type == PoolType::Transparent + } + Item::Data(Receiver::Unknown { .. }) => false, + Item::Metadata(_) => false, }) } /// Returns whether this address contains the given receiver. pub fn contains_receiver(&self, receiver: &Receiver) -> bool { - self.0.contains(receiver) + self.0 + .iter() + .any(|item| matches!(item, Item::Data(r) if r == receiver)) } /// Returns whether this address can receive a memo. pub fn can_receive_memo(&self) -> bool { - self.0 - .iter() - .any(|r| matches!(r, Receiver::Sapling(_) | Receiver::Orchard(_))) + self.0.iter().any(|r| { + matches!( + r, + Item::Data(Receiver::Sapling(_)) | Item::Data(Receiver::Orchard(_)) + ) + }) } } @@ -145,21 +181,21 @@ impl super::private::SealedContainer for Address { /// The HRP for a Bech32m-encoded regtest Unified Address. const REGTEST: &'static str = "uregtest"; - fn from_inner(receivers: Vec) -> Self { + fn from_inner(receivers: Vec>) -> Self { Self(receivers) } } impl super::Encoding for Address {} impl super::Container for Address { - type Item = Receiver; + type DataItem = Receiver; - fn items_as_parsed(&self) -> &[Receiver] { + fn items_as_parsed(&self) -> &[Item] { &self.0 } } -#[cfg(feature = "test-dependencies")] +#[cfg(any(test, feature = "test-dependencies"))] pub mod testing { use proptest::{ array::{uniform11, uniform20, uniform32}, @@ -168,10 +204,10 @@ pub mod testing { sample::select, strategy::Strategy, }; - use zcash_encoding::MAX_COMPACT_SIZE; use super::{Address, Receiver}; - use crate::unified::Typecode; + use crate::unified::{DataTypecode, Item}; + use zcash_encoding::MAX_COMPACT_SIZE; prop_compose! { fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] { @@ -183,61 +219,62 @@ pub mod testing { } /// A strategy to generate an arbitrary transparent typecode. - pub fn arb_transparent_typecode() -> impl Strategy { - select(vec![Typecode::P2pkh, Typecode::P2sh]) + fn arb_transparent_typecode() -> impl Strategy { + select(vec![DataTypecode::P2pkh, DataTypecode::P2sh]) } /// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode. - pub fn arb_shielded_typecode() -> impl Strategy { + fn arb_shielded_typecode() -> impl Strategy { prop_oneof![ - Just(Typecode::Sapling), - Just(Typecode::Orchard), - ((::from(Typecode::Orchard) + 1)..MAX_COMPACT_SIZE).prop_map(Typecode::Unknown) + Just(DataTypecode::Sapling), + Just(DataTypecode::Orchard), + ((::from(DataTypecode::Orchard) + 1)..MAX_COMPACT_SIZE) + .prop_map(DataTypecode::Unknown) ] } /// A strategy to generate an arbitrary valid set of typecodes without /// duplication and containing only one of P2sh and P2pkh transparent /// typecodes. The resulting vector will be sorted in encoding order. - pub fn arb_typecodes() -> impl Strategy> { + fn arb_typecodes() -> impl Strategy> { prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| { - prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| { - let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect(); - typecodes.sort_unstable_by(Typecode::encoding_order); - typecodes - }) + prop::collection::hash_set(arb_shielded_typecode(), 1..4) + .prop_map(move |xs| xs.into_iter().chain(transparent).collect::>()) }) } - /// Generates an arbitrary Unified address containing receivers corresponding to the provided - /// set of typecodes. The receivers of this address are likely to not represent valid protocol - /// receivers, and should only be used for testing parsing and/or encoding functions that do - /// not concern themselves with the validity of the underlying receivers. - pub fn arb_unified_address_for_typecodes( - typecodes: Vec, + /// A strategy to generate a vector of unified address receivers containing random data. The + /// resulting receivers may not be valid according to protocol rules; this generator is only + /// intended for use in testing parsing and serialization. + fn arb_unified_address_receivers( + typecodes: Vec, ) -> impl Strategy> { typecodes .into_iter() .map(|tc| match tc { - Typecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(), - Typecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(), - Typecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(), - Typecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(), - Typecode::Unknown(typecode) => vec(any::(), 32..256) + DataTypecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(), + DataTypecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(), + DataTypecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(), + DataTypecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(), + DataTypecode::Unknown(typecode) => vec(any::(), 32..256) .prop_map(move |data| Receiver::Unknown { typecode, data }) .boxed(), }) .collect::>() } - /// Generates an arbitrary Unified address. The receivers of this address are likely to not - /// represent valid protocol receivers, and should only be used for testing parsing and/or - /// encoding functions that do not concern themselves with the validity of the underlying - /// receivers. + /// A strategy to generate an arbitrary Unified Address containing only receivers, without + /// additional metadata. The items in this address will be sorted in encoding order. The + /// receivers in the resulting address may not be valid according to protocol rules; this + /// generator is only intended for use in testing parsing and serialization. pub fn arb_unified_address() -> impl Strategy { arb_typecodes() - .prop_flat_map(arb_unified_address_for_typecodes) - .prop_map(Address) + .prop_flat_map(arb_unified_address_receivers) + .prop_map(|rs| { + let mut items = rs.into_iter().map(Item::Data).collect::>(); + items.sort_unstable_by(Item::encoding_order); + Address(items) + }) } } @@ -249,14 +286,14 @@ mod tests { use assert_matches::assert_matches; use crate::{ - kind::unified::{private::SealedContainer, Container, Encoding}, - unified::address::testing::arb_unified_address, + kind::unified::{private::SealedContainer, Encoding}, + unified::{address::testing::arb_unified_address, Item, Typecode}, Network, }; use proptest::{prelude::*, sample::select}; - use super::{Address, ParseError, Receiver, Typecode}; + use super::{Address, ParseError, Receiver}; proptest! { #[test] @@ -347,11 +384,14 @@ mod tests { fn duplicate_typecode() { // Construct and serialize an invalid UA. This must be done using private // methods, as the public API does not permit construction of such invalid values. - let ua = Address(vec![Receiver::Sapling([1; 43]), Receiver::Sapling([2; 43])]); + let ua = Address(vec![ + Item::Data(Receiver::Sapling([1; 43])), + Item::Data(Receiver::Sapling([2; 43])), + ]); let encoded = ua.to_jumbled_bytes(Address::MAINNET); assert_eq!( Address::parse_internal(Address::MAINNET, &encoded[..]), - Err(ParseError::DuplicateTypecode(Typecode::Sapling)) + Err(ParseError::DuplicateTypecode(Typecode::SAPLING)) ); } @@ -359,7 +399,10 @@ mod tests { fn p2pkh_and_p2sh() { // Construct and serialize an invalid UA. This must be done using private // methods, as the public API does not permit construction of such invalid values. - let ua = Address(vec![Receiver::P2pkh([0; 20]), Receiver::P2sh([0; 20])]); + let ua = Address(vec![ + Item::Data(Receiver::P2pkh([0; 20])), + Item::Data(Receiver::P2sh([0; 20])), + ]); let encoded = ua.to_jumbled_bytes(Address::MAINNET); // ensure that decoding catches the error assert_eq!( @@ -372,7 +415,10 @@ mod tests { fn addresses_out_of_order() { // Construct and serialize an invalid UA. This must be done using private // methods, as the public API does not permit construction of such invalid values. - let ua = Address(vec![Receiver::Sapling([0; 43]), Receiver::P2pkh([0; 20])]); + let ua = Address(vec![ + Item::Data(Receiver::Sapling([0; 43])), + Item::Data(Receiver::P2pkh([0; 20])), + ]); let encoded = ua.to_jumbled_bytes(Address::MAINNET); // ensure that decoding catches the error assert_eq!( @@ -404,18 +450,18 @@ mod tests { fn receivers_are_sorted() { // Construct a UA with receivers in an unsorted order. let ua = Address(vec![ - Receiver::P2pkh([0; 20]), - Receiver::Orchard([0; 43]), - Receiver::Unknown { + Item::Data(Receiver::P2pkh([0; 20])), + Item::Data(Receiver::Orchard([0; 43])), + Item::Data(Receiver::Unknown { typecode: 0xff, data: vec![], - }, - Receiver::Sapling([0; 43]), + }), + Item::Data(Receiver::Sapling([0; 43])), ]); // `Address::receivers` sorts the receivers in priority order. assert_eq!( - ua.items(), + ua.receivers(), vec![ Receiver::Orchard([0; 43]), Receiver::Sapling([0; 43]), diff --git a/components/zcash_address/src/kind/unified/fvk.rs b/components/zcash_address/src/kind/unified/fvk.rs index cafff0089..1d617299a 100644 --- a/components/zcash_address/src/kind/unified/fvk.rs +++ b/components/zcash_address/src/kind/unified/fvk.rs @@ -1,8 +1,8 @@ -use std::convert::{TryFrom, TryInto}; +use std::convert::TryInto; use super::{ - private::{SealedContainer, SealedItem}, - Container, Encoding, ParseError, Typecode, + private::{SealedContainer, SealedDataItem}, + Container, DataTypecode, Encoding, Item, ParseError, }; /// The set of known FVKs for Unified FVKs. @@ -39,31 +39,27 @@ pub enum Fvk { }, } -impl TryFrom<(u32, &[u8])> for Fvk { - type Error = ParseError; - - fn try_from((typecode, data): (u32, &[u8])) -> Result { +impl SealedDataItem for Fvk { + fn parse(typecode: DataTypecode, data: &[u8]) -> Result { let data = data.to_vec(); - match typecode.try_into()? { - Typecode::P2pkh => data.try_into().map(Fvk::P2pkh), - Typecode::P2sh => Err(data), - Typecode::Sapling => data.try_into().map(Fvk::Sapling), - Typecode::Orchard => data.try_into().map(Fvk::Orchard), - Typecode::Unknown(_) => Ok(Fvk::Unknown { typecode, data }), + match typecode { + DataTypecode::P2pkh => data.try_into().map(Fvk::P2pkh), + DataTypecode::P2sh => Err(data), + DataTypecode::Sapling => data.try_into().map(Fvk::Sapling), + DataTypecode::Orchard => data.try_into().map(Fvk::Orchard), + DataTypecode::Unknown(typecode) => Ok(Fvk::Unknown { typecode, data }), } .map_err(|e| { - ParseError::InvalidEncoding(format!("Invalid fvk for typecode {}: {:?}", typecode, e)) + ParseError::InvalidEncoding(format!("Invalid fvk for typecode {:?}: {:?}", typecode, e)) }) } -} -impl SealedItem for Fvk { - fn typecode(&self) -> Typecode { + fn typecode(&self) -> DataTypecode { match self { - Fvk::P2pkh(_) => Typecode::P2pkh, - Fvk::Sapling(_) => Typecode::Sapling, - Fvk::Orchard(_) => Typecode::Orchard, - Fvk::Unknown { typecode, .. } => Typecode::Unknown(*typecode), + Fvk::P2pkh(_) => DataTypecode::P2pkh, + Fvk::Sapling(_) => DataTypecode::Sapling, + Fvk::Orchard(_) => DataTypecode::Orchard, + Fvk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode), } } @@ -83,7 +79,7 @@ impl SealedItem for Fvk { /// /// ``` /// # use std::error::Error; -/// use zcash_address::unified::{self, Container, Encoding}; +/// use zcash_address::unified::{self, Container, Encoding, Item}; /// /// # fn main() -> Result<(), Box> { /// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv"; @@ -91,28 +87,22 @@ impl SealedItem for Fvk { /// /// let (network, ufvk) = unified::Ufvk::decode(example_ufvk)?; /// -/// // We can obtain the pool-specific Full Viewing Keys for the UFVK in preference -/// // order (the order in which wallets should prefer to use their corresponding -/// // address receivers): -/// let fvks: Vec = ufvk.items(); +/// // We can obtain the pool-specific Full Viewing Keys for the UFVK. +/// let fvks: &[Item] = ufvk.items_as_parsed(); /// /// // And we can create the UFVK from a list of FVKs: -/// let new_ufvk = unified::Ufvk::try_from_items(fvks)?; +/// let new_ufvk = unified::Ufvk::try_from_items(fvks.to_vec())?; /// assert_eq!(new_ufvk, ufvk); /// # Ok(()) /// # } /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Ufvk(pub(crate) Vec); +pub struct Ufvk(pub(crate) Vec>); impl Container for Ufvk { - type Item = Fvk; + type DataItem = Fvk; - /// Returns the FVKs contained within this UFVK, in the order they were - /// parsed from the string encoding. - /// - /// This API is for advanced usage; in most cases you should use `Ufvk::receivers`. - fn items_as_parsed(&self) -> &[Fvk] { + fn items_as_parsed(&self) -> &[Item] { &self.0 } } @@ -137,7 +127,7 @@ impl SealedContainer for Ufvk { /// The HRP for a Bech32m-encoded regtest Unified FVK. const REGTEST: &'static str = "uviewregtest"; - fn from_inner(fvks: Vec) -> Self { + fn from_inner(fvks: Vec>) -> Self { Self(fvks) } } @@ -148,12 +138,10 @@ mod tests { use proptest::{array::uniform1, array::uniform32, prelude::*, sample::select}; - use super::{Fvk, ParseError, Typecode, Ufvk}; + use super::{Fvk, ParseError, Ufvk}; use crate::{ - kind::unified::{ - private::{SealedContainer, SealedItem}, - Container, Encoding, - }, + kind::unified::{private::SealedContainer, Encoding}, + unified::{Item, Typecode}, Network, }; @@ -211,8 +199,8 @@ mod tests { shielded in arb_shielded_fvk(), transparent in prop::option::of(arb_transparent_fvk()), ) -> Ufvk { - let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect(); - items.sort_unstable_by(Fvk::encoding_order); + let mut items: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect(); + items.sort_unstable_by(Item::encoding_order); Ufvk(items) } } @@ -319,11 +307,14 @@ mod tests { fn duplicate_typecode() { // Construct and serialize an invalid Ufvk. This must be done using private // methods, as the public API does not permit construction of such invalid values. - let ufvk = Ufvk(vec![Fvk::Sapling([1; 128]), Fvk::Sapling([2; 128])]); + let ufvk = Ufvk(vec![ + Item::Data(Fvk::Sapling([1; 128])), + Item::Data(Fvk::Sapling([2; 128])), + ]); let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET); assert_eq!( Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]), - Err(ParseError::DuplicateTypecode(Typecode::Sapling)) + Err(ParseError::DuplicateTypecode(Typecode::SAPLING)) ); } @@ -344,32 +335,4 @@ mod tests { Err(ParseError::OnlyTransparent) ); } - - #[test] - fn fvks_are_sorted() { - // Construct a UFVK with fvks in an unsorted order. - let ufvk = Ufvk(vec![ - Fvk::P2pkh([0; 65]), - Fvk::Orchard([0; 96]), - Fvk::Unknown { - typecode: 0xff, - data: vec![], - }, - Fvk::Sapling([0; 128]), - ]); - - // `Ufvk::items` sorts the fvks in priority order. - assert_eq!( - ufvk.items(), - vec![ - Fvk::Orchard([0; 96]), - Fvk::Sapling([0; 128]), - Fvk::P2pkh([0; 65]), - Fvk::Unknown { - typecode: 0xff, - data: vec![], - }, - ] - ) - } } diff --git a/components/zcash_address/src/kind/unified/ivk.rs b/components/zcash_address/src/kind/unified/ivk.rs index 5bf34e227..2dd39147e 100644 --- a/components/zcash_address/src/kind/unified/ivk.rs +++ b/components/zcash_address/src/kind/unified/ivk.rs @@ -1,8 +1,8 @@ -use std::convert::{TryFrom, TryInto}; +use std::convert::TryInto; use super::{ - private::{SealedContainer, SealedItem}, - Container, Encoding, ParseError, Typecode, + private::{SealedContainer, SealedDataItem}, + Container, DataTypecode, Encoding, Item, ParseError, }; /// The set of known IVKs for Unified IVKs. @@ -44,31 +44,27 @@ pub enum Ivk { }, } -impl TryFrom<(u32, &[u8])> for Ivk { - type Error = ParseError; - - fn try_from((typecode, data): (u32, &[u8])) -> Result { +impl SealedDataItem for Ivk { + fn parse(typecode: DataTypecode, data: &[u8]) -> Result { let data = data.to_vec(); - match typecode.try_into()? { - Typecode::P2pkh => data.try_into().map(Ivk::P2pkh), - Typecode::P2sh => Err(data), - Typecode::Sapling => data.try_into().map(Ivk::Sapling), - Typecode::Orchard => data.try_into().map(Ivk::Orchard), - Typecode::Unknown(_) => Ok(Ivk::Unknown { typecode, data }), + match typecode { + DataTypecode::P2pkh => data.try_into().map(Ivk::P2pkh), + DataTypecode::P2sh => Err(data), + DataTypecode::Sapling => data.try_into().map(Ivk::Sapling), + DataTypecode::Orchard => data.try_into().map(Ivk::Orchard), + DataTypecode::Unknown(typecode) => Ok(Ivk::Unknown { typecode, data }), } .map_err(|e| { - ParseError::InvalidEncoding(format!("Invalid ivk for typecode {}: {:?}", typecode, e)) + ParseError::InvalidEncoding(format!("Invalid ivk for typecode {:?}: {:?}", typecode, e)) }) } -} -impl SealedItem for Ivk { - fn typecode(&self) -> Typecode { + fn typecode(&self) -> DataTypecode { match self { - Ivk::P2pkh(_) => Typecode::P2pkh, - Ivk::Sapling(_) => Typecode::Sapling, - Ivk::Orchard(_) => Typecode::Orchard, - Ivk::Unknown { typecode, .. } => Typecode::Unknown(*typecode), + Ivk::P2pkh(_) => DataTypecode::P2pkh, + Ivk::Sapling(_) => DataTypecode::Sapling, + Ivk::Orchard(_) => DataTypecode::Orchard, + Ivk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode), } } @@ -88,7 +84,7 @@ impl SealedItem for Ivk { /// /// ``` /// # use std::error::Error; -/// use zcash_address::unified::{self, Container, Encoding}; +/// use zcash_address::unified::{self, Container, Encoding, Item}; /// /// # fn main() -> Result<(), Box> { /// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq"; @@ -96,28 +92,22 @@ impl SealedItem for Ivk { /// /// let (network, uivk) = unified::Uivk::decode(example_uivk)?; /// -/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK in -/// // preference order (the order in which wallets should prefer to use their -/// // corresponding address receivers): -/// let ivks: Vec = uivk.items(); +/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK. +/// let ivks: &[Item] = uivk.items_as_parsed(); /// -/// // And we can create the UIVK from a list of IVKs: -/// let new_uivk = unified::Uivk::try_from_items(ivks)?; +/// // And we can create the UIVK from a vector of IVKs: +/// let new_uivk = unified::Uivk::try_from_items(ivks.to_vec())?; /// assert_eq!(new_uivk, uivk); /// # Ok(()) /// # } /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Uivk(pub(crate) Vec); +pub struct Uivk(pub(crate) Vec>); impl Container for Uivk { - type Item = Ivk; + type DataItem = Ivk; - /// Returns the IVKs contained within this UIVK, in the order they were - /// parsed from the string encoding. - /// - /// This API is for advanced usage; in most cases you should use `Uivk::items`. - fn items_as_parsed(&self) -> &[Ivk] { + fn items_as_parsed(&self) -> &[Item] { &self.0 } } @@ -142,7 +132,7 @@ impl SealedContainer for Uivk { /// The HRP for a Bech32m-encoded regtest Unified IVK. const REGTEST: &'static str = "uivkregtest"; - fn from_inner(ivks: Vec) -> Self { + fn from_inner(ivks: Vec>) -> Self { Self(ivks) } } @@ -157,12 +147,10 @@ mod tests { sample::select, }; - use super::{Ivk, ParseError, Typecode, Uivk}; + use super::{Ivk, ParseError, Uivk}; use crate::{ - kind::unified::{ - private::{SealedContainer, SealedItem}, - Container, Encoding, - }, + kind::unified::{private::SealedContainer, Encoding}, + unified::{Item, Typecode}, Network, }; @@ -204,8 +192,8 @@ mod tests { shielded in arb_shielded_ivk(), transparent in prop::option::of(arb_transparent_ivk()), ) -> Uivk { - let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect(); - items.sort_unstable_by(Ivk::encoding_order); + let mut items: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect(); + items.sort_unstable_by(Item::encoding_order); Uivk(items) } } @@ -301,11 +289,14 @@ mod tests { #[test] fn duplicate_typecode() { // Construct and serialize an invalid UIVK. - let uivk = Uivk(vec![Ivk::Sapling([1; 64]), Ivk::Sapling([2; 64])]); + let uivk = Uivk(vec![ + Item::Data(Ivk::Sapling([1; 64])), + Item::Data(Ivk::Sapling([2; 64])), + ]); let encoded = uivk.encode(&Network::Main); assert_eq!( Uivk::decode(&encoded), - Err(ParseError::DuplicateTypecode(Typecode::Sapling)) + Err(ParseError::DuplicateTypecode(Typecode::SAPLING)) ); } @@ -326,32 +317,4 @@ mod tests { Err(ParseError::OnlyTransparent) ); } - - #[test] - fn ivks_are_sorted() { - // Construct a UIVK with ivks in an unsorted order. - let uivk = Uivk(vec![ - Ivk::P2pkh([0; 65]), - Ivk::Orchard([0; 64]), - Ivk::Unknown { - typecode: 0xff, - data: vec![], - }, - Ivk::Sapling([0; 64]), - ]); - - // `Uivk::items` sorts the ivks in priority order. - assert_eq!( - uivk.items(), - vec![ - Ivk::Orchard([0; 64]), - Ivk::Sapling([0; 64]), - Ivk::P2pkh([0; 65]), - Ivk::Unknown { - typecode: 0xff, - data: vec![], - }, - ] - ) - } } diff --git a/components/zcash_address/src/test_vectors.rs b/components/zcash_address/src/test_vectors.rs index 61cffcef9..598096da6 100644 --- a/components/zcash_address/src/test_vectors.rs +++ b/components/zcash_address/src/test_vectors.rs @@ -8,6 +8,7 @@ use { unified::{ self, address::{test_vectors::TEST_VECTORS, Receiver}, + Item, }, Network, ToAddress, ZcashAddress, }, @@ -36,6 +37,7 @@ fn unified() { data: data.to_vec(), }) })) + .map(Item::Data) .collect(); let expected_addr = ZcashAddress::from_unified(Network::Main, unified::Address(receivers));