zcash_address: Add handling for Unified Metadata Items

This commit is contained in:
Kris Nuttycombe 2024-01-23 15:04:10 -07:00
parent c3e1750007
commit 0341171c84
8 changed files with 492 additions and 316 deletions

View File

@ -9,11 +9,26 @@ and this library adheres to Rust's notion of
### Added ### Added
- `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as, matches_receiver}` - `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::testing` under the `test-dependencies` feature.
- Module `zcash_address::unified::address::testing` under the - Module `zcash_address::unified::address::testing` under the
`test-dependencies` feature. `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 ## [0.3.2] - 2024-03-06
### Added ### Added
- `zcash_address::convert`: - `zcash_address::convert`:

View File

@ -28,6 +28,7 @@ proptest = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
assert_matches.workspace = true assert_matches.workspace = true
proptest.workspace = true
[features] [features]
test-dependencies = ["dep:proptest"] test-dependencies = ["dep:proptest"]

View File

@ -180,7 +180,11 @@ mod tests {
use assert_matches::assert_matches; use assert_matches::assert_matches;
use super::*; use super::*;
use crate::{kind::unified, Network}; use crate::{
kind::unified,
unified::{Item, Receiver},
Network,
};
fn encoding(encoded: &str, decoded: ZcashAddress) { fn encoding(encoded: &str, decoded: ZcashAddress) {
assert_eq!(decoded.to_string(), encoded); assert_eq!(decoded.to_string(), encoded);
@ -230,21 +234,21 @@ mod tests {
"u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl", "u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl",
ZcashAddress { ZcashAddress {
net: Network::Main, 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( encoding(
"utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s", "utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s",
ZcashAddress { ZcashAddress {
net: Network::Test, 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( encoding(
"uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3", "uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3",
ZcashAddress { ZcashAddress {
net: Network::Regtest, 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]))])),
}, },
); );

View File

@ -6,6 +6,7 @@ use std::convert::{TryFrom, TryInto};
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use std::num::TryFromIntError; use std::num::TryFromIntError;
use zcash_encoding::MAX_COMPACT_SIZE;
use crate::Network; use crate::Network;
@ -22,9 +23,9 @@ const PADDING_LEN: usize = 16;
/// The known Receiver and Viewing Key types. /// The known Receiver and Viewing Key types.
/// ///
/// The typecodes `0xFFFA..=0xFFFF` reserved for experiments are currently not /// 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)] #[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). /// A transparent P2PKH address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316).
P2pkh, P2pkh,
/// A transparent P2SH address. /// A transparent P2SH address.
@ -39,7 +40,37 @@ pub enum Typecode {
Unknown(u32), Unknown(u32),
} }
impl Typecode { impl TryFrom<u32> for DataTypecode {
type Error = ();
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
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<DataTypecode> 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 { pub fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
match (a, b) { match (a, b) {
// Trivial equality checks. // Trivial equality checks.
@ -69,51 +100,203 @@ impl Typecode {
(_, Self::P2pkh) => cmp::Ordering::Greater, (_, Self::P2pkh) => cmp::Ordering::Greater,
} }
} }
}
pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering { /// The known Metadata Typecodes
u32::from(*a).cmp(&u32::from(*b)) #[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<u32> for MetadataTypecode {
type Error = ();
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
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<MetadataTypecode> 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<u32> for Typecode { impl TryFrom<u32> for Typecode {
type Error = ParseError; type Error = ParseError;
fn try_from(typecode: u32) -> Result<Self, Self::Error> { fn try_from(typecode: u32) -> Result<Self, Self::Error> {
match typecode { DataTypecode::try_from(typecode)
0x00 => Ok(Typecode::P2pkh), .map_or_else(
0x01 => Ok(Typecode::P2sh), |()| MetadataTypecode::try_from(typecode).map(Typecode::Metadata),
0x02 => Ok(Typecode::Sapling), |t| Ok(Typecode::Data(t)),
0x03 => Ok(Typecode::Orchard), )
0x04..=0x02000000 => Ok(Typecode::Unknown(typecode)), .map_err(|()| ParseError::InvalidTypecodeValue(typecode))
0x02000001..=u32::MAX => Err(ParseError::InvalidTypecodeValue(typecode as u64)),
}
} }
} }
impl From<Typecode> for u32 { impl From<Typecode> for u32 {
fn from(t: Typecode) -> Self { fn from(t: Typecode) -> Self {
match t { match t {
Typecode::P2pkh => 0x00, Typecode::Data(tc) => tc.into(),
Typecode::P2sh => 0x01, Typecode::Metadata(tc) => tc.into(),
Typecode::Sapling => 0x02,
Typecode::Orchard => 0x03,
Typecode::Unknown(typecode) => typecode,
} }
} }
} }
impl TryFrom<Typecode> for usize { impl TryFrom<Typecode> for usize {
type Error = TryFromIntError; type Error = TryFromIntError;
fn try_from(t: Typecode) -> Result<Self, Self::Error> { fn try_from(t: Typecode) -> Result<Self, Self::Error> {
u32::from(t).try_into() u32::from(t).try_into()
} }
} }
impl Typecode { /// An enumeration of known Unified Metadata Item types.
fn is_transparent(&self) -> bool { ///
// Unknown typecodes are treated as not transparent for the purpose of disallowing /// Unknown MUST-understand metadata items are NOT represented using this type, as the presence of
// only-transparent UAs, which can be represented with existing address encodings. /// an unrecognized metadata item with a typecode in the `MUST-understand` range will result in a
matches!(self, Typecode::P2pkh | Typecode::P2sh) /// 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<u8> },
}
impl MetadataItem {
/// Parse a metadata item for the specified metadata typecode from the provided bytes.
pub fn parse(typecode: MetadataTypecode, data: &[u8]) -> Result<Self, ParseError> {
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<u8> {
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<T> {
/// A data item; either a receiver (for Unified Addresses) or a key (for Unified Viewing Keys)
Data(T),
/// A metadata item.
Metadata(MetadataItem),
}
impl<T: private::SealedDataItem> Item<T> {
/// 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<u8> {
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. /// The unified container contains a duplicated typecode.
DuplicateTypecode(Typecode), DuplicateTypecode(Typecode),
/// The parsed typecode exceeds the maximum allowed CompactSize value. /// The parsed typecode exceeds the maximum allowed CompactSize value.
InvalidTypecodeValue(u64), InvalidTypecodeValue(u32),
/// The string is an invalid encoding. /// The string is an invalid encoding.
InvalidEncoding(String), InvalidEncoding(String),
/// The items in the unified container are not in typecode order. /// The items in the unified container are not in typecode order.
@ -136,6 +319,8 @@ pub enum ParseError {
NotUnified, NotUnified,
/// The Bech32m string has an unrecognized human-readable prefix. /// The Bech32m string has an unrecognized human-readable prefix.
UnknownPrefix(String), UnknownPrefix(String),
/// A `MUST-understand` metadata item was not recognized.
NotUnderstood(u32),
} }
impl fmt::Display for ParseError { impl fmt::Display for ParseError {
@ -151,6 +336,13 @@ impl fmt::Display for ParseError {
ParseError::UnknownPrefix(s) => { ParseError::UnknownPrefix(s) => {
write!(f, "Unrecognized Bech32m human-readable prefix: {}", 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 {} impl Error for ParseError {}
pub(crate) mod private { pub(crate) mod private {
use super::{ParseError, Typecode, PADDING_LEN}; use super::{DataTypecode, ParseError, Typecode, PADDING_LEN};
use crate::Network; use crate::{
unified::{Item, MetadataItem},
Network,
};
use std::{ use std::{
cmp,
convert::{TryFrom, TryInto}, convert::{TryFrom, TryInto},
io::Write, io::Write,
}; };
use zcash_encoding::CompactSize; use zcash_encoding::CompactSize;
/// A raw address or viewing key. /// A raw address or viewing key.
pub trait SealedItem: for<'a> TryFrom<(u32, &'a [u8]), Error = ParseError> + Clone { pub trait SealedDataItem: Clone {
fn typecode(&self) -> Typecode; /// Parse a data item for the specified data typecode from the provided bytes.
fn parse(tc: DataTypecode, value: &[u8]) -> Result<Self, ParseError>;
/// Returns the typecode of this data item.
fn typecode(&self) -> DataTypecode;
/// Returns the raw bytes of this data item.
fn data(&self) -> &[u8]; 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. /// 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 /// Implementations of this method should act as unchecked constructors
/// of the container type; the caller is guaranteed to check the /// of the container type; the caller is guaranteed to check the
/// general invariants that apply to all unified containers. /// general invariants that apply to all unified containers.
fn from_inner(items: Vec<Self::Item>) -> Self; fn from_inner(items: Vec<Item<Self::DataItem>>) -> Self;
fn network_hrp(network: &Network) -> &'static str { fn network_hrp(network: &Network) -> &'static str {
match network { match network {
@ -227,7 +413,7 @@ pub(crate) mod private {
) )
.unwrap(); .unwrap();
CompactSize::write(&mut writer, data.len()).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. /// Parse the items of the unified container.
fn parse_items<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Vec<Self::Item>, ParseError> { fn parse_items<T: Into<Vec<u8>>>(
fn read_receiver<R: SealedItem>( hrp: &str,
buf: T,
) -> Result<Vec<Item<Self::DataItem>>, ParseError> {
fn read_receiver<R: SealedDataItem>(
mut cursor: &mut std::io::Cursor<&[u8]>, mut cursor: &mut std::io::Cursor<&[u8]>,
) -> Result<R, ParseError> { ) -> Result<Item<R>, ParseError> {
let typecode = CompactSize::read(&mut cursor) let typecode = CompactSize::read(&mut cursor)
.map(|v| u32::try_from(v).expect("CompactSize::read enforces MAX_SIZE limit")) .map(|v| u32::try_from(v).expect("CompactSize::read enforces MAX_SIZE limit"))
.map_err(|e| { .map_err(|e| {
@ -279,12 +468,13 @@ pub(crate) mod private {
length length
))); )));
} }
let result = R::try_from(( let data = &buf[cursor.position() as usize..addr_end as usize];
typecode, let result = match Typecode::try_from(typecode)? {
&buf[cursor.position() as usize..addr_end as usize], Typecode::Data(tc) => Item::Data(R::parse(tc, data)?),
)); Typecode::Metadata(tc) => Item::Metadata(MetadataItem::parse(tc, data)?),
};
cursor.set_position(addr_end); cursor.set_position(addr_end);
result Ok(result)
} }
// Here we allocate if necessary to get a mutable Vec<u8> to unjumble. // Here we allocate if necessary to get a mutable Vec<u8> to unjumble.
@ -320,8 +510,8 @@ pub(crate) mod private {
/// A private function that constructs a unified container with the /// A private function that constructs a unified container with the
/// specified items, which must be in ascending typecode order. /// specified items, which must be in ascending typecode order.
fn try_from_items_internal(items: Vec<Self::Item>) -> Result<Self, ParseError> { fn try_from_items_internal(items: Vec<Item<Self::DataItem>>) -> Result<Self, ParseError> {
assert!(u32::from(Typecode::P2sh) == u32::from(Typecode::P2pkh) + 1); assert!(u32::from(Typecode::P2SH) == u32::from(Typecode::P2PKH) + 1);
let mut only_transparent = true; let mut only_transparent = true;
let mut prev_code = None; // less than any Some let mut prev_code = None; // less than any Some
@ -332,13 +522,15 @@ pub(crate) mod private {
return Err(ParseError::InvalidTypecodeOrder); return Err(ParseError::InvalidTypecodeOrder);
} else if t_code == prev_code { } else if t_code == prev_code {
return Err(ParseError::DuplicateTypecode(t)); 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, // 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. // otherwise we would detect an out-of-order or duplicate typecode.
return Err(ParseError::BothP2phkAndP2sh); return Err(ParseError::BothP2phkAndP2sh);
} else { } else {
prev_code = t_code; 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. /// Trait providing common encoding and decoding logic for Unified containers.
pub trait Encoding: private::SealedContainer { pub trait Encoding: private::SealedContainer {
/// Constructs a value of a unified container type from a vector /// Constructs a value of a unified container type from a vector of container
/// of container items, sorted according to typecode as specified /// items. These items will be sorted according to typecode as specified in ZIP
/// in ZIP 316. /// 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 /// This function will return an error in the case that the following ZIP 316
/// invariants concerning the composition of a unified container are /// 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 two items having the same typecode
/// * the item list may not contain only transparent items (or no items) /// * the item list may not contain only transparent items (or no items)
/// * the item list may not contain both P2PKH and P2SH items. /// * the item list may not contain both P2PKH and P2SH items.
fn try_from_items(mut items: Vec<Self::Item>) -> Result<Self, ParseError> { fn try_from_items(mut items: Vec<Item<Self::DataItem>>) -> Result<Self, ParseError> {
items.sort_unstable_by(Self::Item::encoding_order); items.sort_unstable_by(Item::encoding_order);
Self::try_from_items_internal(items) 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. /// Trait for for Unified containers, that exposes the items within them.
pub trait Container { pub trait Container {
/// The type of item in this unified container. /// The type of data items in this unified container.
type Item: SealedItem; type DataItem: SealedDataItem;
/// Returns the items contained within this container, sorted in preference order. /// Returns the items in encoding order.
fn items(&self) -> Vec<Self::Item> { fn items_as_parsed(&self) -> &[Item<Self::DataItem>];
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];
} }

View File

@ -1,10 +1,10 @@
use zcash_protocol::{PoolType, ShieldedProtocol}; 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)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Receiver { pub enum Receiver {
Orchard([u8; 43]), Orchard([u8; 43]),
@ -14,34 +14,39 @@ pub enum Receiver {
Unknown { typecode: u32, data: Vec<u8> }, Unknown { typecode: u32, data: Vec<u8> },
} }
impl TryFrom<(u32, &[u8])> for Receiver { impl Receiver {
type Error = ParseError; fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
DataTypecode::preference_order(&a.typecode(), &b.typecode())
fn try_from((typecode, addr): (u32, &[u8])) -> Result<Self, Self::Error> {
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 SealedItem for Receiver { impl SealedDataItem for Receiver {
fn typecode(&self) -> Typecode { fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
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 { match self {
Receiver::P2pkh(_) => Typecode::P2pkh, Receiver::P2pkh(_) => DataTypecode::P2pkh,
Receiver::P2sh(_) => Typecode::P2sh, Receiver::P2sh(_) => DataTypecode::P2sh,
Receiver::Sapling(_) => Typecode::Sapling, Receiver::Sapling(_) => DataTypecode::Sapling,
Receiver::Orchard(_) => Typecode::Orchard, Receiver::Orchard(_) => DataTypecode::Orchard,
Receiver::Unknown { typecode, .. } => Typecode::Unknown(*typecode), Receiver::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
} }
} }
@ -64,7 +69,7 @@ impl SealedItem for Receiver {
/// # use std::convert::Infallible; /// # use std::convert::Infallible;
/// # use std::error::Error; /// # use std::error::Error;
/// use zcash_address::{ /// use zcash_address::{
/// unified::{self, Container, Encoding}, /// unified::{self, Container, Encoding, Item},
/// ConversionError, TryFromRawAddress, ZcashAddress, /// ConversionError, TryFromRawAddress, ZcashAddress,
/// }; /// };
/// ///
@ -92,38 +97,69 @@ impl SealedItem for Receiver {
/// ///
/// // We can obtain the receivers for the UA in preference order /// // We can obtain the receivers for the UA in preference order
/// // (the order in which wallets should prefer to use them): /// // (the order in which wallets should prefer to use them):
/// let receivers: Vec<unified::Receiver> = ua.items(); /// let receivers: Vec<unified::Receiver> = ua.receivers();
/// ///
/// // And we can create the UA from a list of 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); /// assert_eq!(new_ua, ua);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Address(pub(crate) Vec<Receiver>); pub struct Address(pub(crate) Vec<Item<Receiver>>);
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<Receiver> {
let mut result = self
.0
.iter()
.filter_map(|item| match item {
Item::Data(r) => Some(r.clone()),
Item::Metadata(_) => None,
})
.collect::<Vec<Receiver>>();
result.sort_unstable_by(Receiver::preference_order);
result
}
}
impl Address { impl Address {
/// Returns whether this address has the ability to receive transfers of the given pool type. /// 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 { pub fn has_receiver_of_type(&self, pool_type: PoolType) -> bool {
self.0.iter().any(|r| match r { self.0.iter().any(|item| match item {
Receiver::Orchard(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Orchard), Item::Data(Receiver::Orchard(_)) => {
Receiver::Sapling(_) => pool_type == PoolType::Shielded(ShieldedProtocol::Sapling), pool_type == PoolType::Shielded(ShieldedProtocol::Orchard)
Receiver::P2pkh(_) | Receiver::P2sh(_) => pool_type == PoolType::Transparent, }
Receiver::Unknown { .. } => false, 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. /// Returns whether this address contains the given receiver.
pub fn contains_receiver(&self, receiver: &Receiver) -> bool { 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. /// Returns whether this address can receive a memo.
pub fn can_receive_memo(&self) -> bool { pub fn can_receive_memo(&self) -> bool {
self.0 self.0.iter().any(|r| {
.iter() matches!(
.any(|r| matches!(r, Receiver::Sapling(_) | Receiver::Orchard(_))) 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. /// The HRP for a Bech32m-encoded regtest Unified Address.
const REGTEST: &'static str = "uregtest"; const REGTEST: &'static str = "uregtest";
fn from_inner(receivers: Vec<Self::Item>) -> Self { fn from_inner(receivers: Vec<Item<Self::DataItem>>) -> Self {
Self(receivers) Self(receivers)
} }
} }
impl super::Encoding for Address {} impl super::Encoding for Address {}
impl super::Container 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<Receiver>] {
&self.0 &self.0
} }
} }
#[cfg(feature = "test-dependencies")] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use proptest::{ use proptest::{
array::{uniform11, uniform20, uniform32}, array::{uniform11, uniform20, uniform32},
@ -168,10 +204,10 @@ pub mod testing {
sample::select, sample::select,
strategy::Strategy, strategy::Strategy,
}; };
use zcash_encoding::MAX_COMPACT_SIZE;
use super::{Address, Receiver}; use super::{Address, Receiver};
use crate::unified::Typecode; use crate::unified::{DataTypecode, Item};
use zcash_encoding::MAX_COMPACT_SIZE;
prop_compose! { prop_compose! {
fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] { 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. /// A strategy to generate an arbitrary transparent typecode.
pub fn arb_transparent_typecode() -> impl Strategy<Value = Typecode> { fn arb_transparent_typecode() -> impl Strategy<Value = DataTypecode> {
select(vec![Typecode::P2pkh, Typecode::P2sh]) select(vec![DataTypecode::P2pkh, DataTypecode::P2sh])
} }
/// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode. /// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode.
pub fn arb_shielded_typecode() -> impl Strategy<Value = Typecode> { fn arb_shielded_typecode() -> impl Strategy<Value = DataTypecode> {
prop_oneof![ prop_oneof![
Just(Typecode::Sapling), Just(DataTypecode::Sapling),
Just(Typecode::Orchard), Just(DataTypecode::Orchard),
((<u32>::from(Typecode::Orchard) + 1)..MAX_COMPACT_SIZE).prop_map(Typecode::Unknown) ((<u32>::from(DataTypecode::Orchard) + 1)..MAX_COMPACT_SIZE)
.prop_map(DataTypecode::Unknown)
] ]
} }
/// A strategy to generate an arbitrary valid set of typecodes without /// A strategy to generate an arbitrary valid set of typecodes without
/// duplication and containing only one of P2sh and P2pkh transparent /// duplication and containing only one of P2sh and P2pkh transparent
/// typecodes. The resulting vector will be sorted in encoding order. /// typecodes. The resulting vector will be sorted in encoding order.
pub fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> { fn arb_typecodes() -> impl Strategy<Value = Vec<DataTypecode>> {
prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| { prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| {
prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| { prop::collection::hash_set(arb_shielded_typecode(), 1..4)
let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect(); .prop_map(move |xs| xs.into_iter().chain(transparent).collect::<Vec<_>>())
typecodes.sort_unstable_by(Typecode::encoding_order);
typecodes
})
}) })
} }
/// Generates an arbitrary Unified address containing receivers corresponding to the provided /// A strategy to generate a vector of unified address receivers containing random data. The
/// set of typecodes. The receivers of this address are likely to not represent valid protocol /// resulting receivers may not be valid according to protocol rules; this generator is only
/// receivers, and should only be used for testing parsing and/or encoding functions that do /// intended for use in testing parsing and serialization.
/// not concern themselves with the validity of the underlying receivers. fn arb_unified_address_receivers(
pub fn arb_unified_address_for_typecodes( typecodes: Vec<DataTypecode>,
typecodes: Vec<Typecode>,
) -> impl Strategy<Value = Vec<Receiver>> { ) -> impl Strategy<Value = Vec<Receiver>> {
typecodes typecodes
.into_iter() .into_iter()
.map(|tc| match tc { .map(|tc| match tc {
Typecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(), DataTypecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(),
Typecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(), DataTypecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(),
Typecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(), DataTypecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(),
Typecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(), DataTypecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(),
Typecode::Unknown(typecode) => vec(any::<u8>(), 32..256) DataTypecode::Unknown(typecode) => vec(any::<u8>(), 32..256)
.prop_map(move |data| Receiver::Unknown { typecode, data }) .prop_map(move |data| Receiver::Unknown { typecode, data })
.boxed(), .boxed(),
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
/// Generates an arbitrary Unified address. The receivers of this address are likely to not /// A strategy to generate an arbitrary Unified Address containing only receivers, without
/// represent valid protocol receivers, and should only be used for testing parsing and/or /// additional metadata. The items in this address will be sorted in encoding order. The
/// encoding functions that do not concern themselves with the validity of the underlying /// receivers in the resulting address may not be valid according to protocol rules; this
/// receivers. /// generator is only intended for use in testing parsing and serialization.
pub fn arb_unified_address() -> impl Strategy<Value = Address> { pub fn arb_unified_address() -> impl Strategy<Value = Address> {
arb_typecodes() arb_typecodes()
.prop_flat_map(arb_unified_address_for_typecodes) .prop_flat_map(arb_unified_address_receivers)
.prop_map(Address) .prop_map(|rs| {
let mut items = rs.into_iter().map(Item::Data).collect::<Vec<_>>();
items.sort_unstable_by(Item::encoding_order);
Address(items)
})
} }
} }
@ -249,14 +286,14 @@ mod tests {
use assert_matches::assert_matches; use assert_matches::assert_matches;
use crate::{ use crate::{
kind::unified::{private::SealedContainer, Container, Encoding}, kind::unified::{private::SealedContainer, Encoding},
unified::address::testing::arb_unified_address, unified::{address::testing::arb_unified_address, Item, Typecode},
Network, Network,
}; };
use proptest::{prelude::*, sample::select}; use proptest::{prelude::*, sample::select};
use super::{Address, ParseError, Receiver, Typecode}; use super::{Address, ParseError, Receiver};
proptest! { proptest! {
#[test] #[test]
@ -347,11 +384,14 @@ mod tests {
fn duplicate_typecode() { fn duplicate_typecode() {
// Construct and serialize an invalid UA. This must be done using private // 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. // 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); let encoded = ua.to_jumbled_bytes(Address::MAINNET);
assert_eq!( assert_eq!(
Address::parse_internal(Address::MAINNET, &encoded[..]), 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() { fn p2pkh_and_p2sh() {
// Construct and serialize an invalid UA. This must be done using private // 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. // 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); let encoded = ua.to_jumbled_bytes(Address::MAINNET);
// ensure that decoding catches the error // ensure that decoding catches the error
assert_eq!( assert_eq!(
@ -372,7 +415,10 @@ mod tests {
fn addresses_out_of_order() { fn addresses_out_of_order() {
// Construct and serialize an invalid UA. This must be done using private // 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. // 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); let encoded = ua.to_jumbled_bytes(Address::MAINNET);
// ensure that decoding catches the error // ensure that decoding catches the error
assert_eq!( assert_eq!(
@ -404,18 +450,18 @@ mod tests {
fn receivers_are_sorted() { fn receivers_are_sorted() {
// Construct a UA with receivers in an unsorted order. // Construct a UA with receivers in an unsorted order.
let ua = Address(vec![ let ua = Address(vec![
Receiver::P2pkh([0; 20]), Item::Data(Receiver::P2pkh([0; 20])),
Receiver::Orchard([0; 43]), Item::Data(Receiver::Orchard([0; 43])),
Receiver::Unknown { Item::Data(Receiver::Unknown {
typecode: 0xff, typecode: 0xff,
data: vec![], data: vec![],
}, }),
Receiver::Sapling([0; 43]), Item::Data(Receiver::Sapling([0; 43])),
]); ]);
// `Address::receivers` sorts the receivers in priority order. // `Address::receivers` sorts the receivers in priority order.
assert_eq!( assert_eq!(
ua.items(), ua.receivers(),
vec![ vec![
Receiver::Orchard([0; 43]), Receiver::Orchard([0; 43]),
Receiver::Sapling([0; 43]), Receiver::Sapling([0; 43]),

View File

@ -1,8 +1,8 @@
use std::convert::{TryFrom, TryInto}; use std::convert::TryInto;
use super::{ use super::{
private::{SealedContainer, SealedItem}, private::{SealedContainer, SealedDataItem},
Container, Encoding, ParseError, Typecode, Container, DataTypecode, Encoding, Item, ParseError,
}; };
/// The set of known FVKs for Unified FVKs. /// The set of known FVKs for Unified FVKs.
@ -39,31 +39,27 @@ pub enum Fvk {
}, },
} }
impl TryFrom<(u32, &[u8])> for Fvk { impl SealedDataItem for Fvk {
type Error = ParseError; fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
fn try_from((typecode, data): (u32, &[u8])) -> Result<Self, Self::Error> {
let data = data.to_vec(); let data = data.to_vec();
match typecode.try_into()? { match typecode {
Typecode::P2pkh => data.try_into().map(Fvk::P2pkh), DataTypecode::P2pkh => data.try_into().map(Fvk::P2pkh),
Typecode::P2sh => Err(data), DataTypecode::P2sh => Err(data),
Typecode::Sapling => data.try_into().map(Fvk::Sapling), DataTypecode::Sapling => data.try_into().map(Fvk::Sapling),
Typecode::Orchard => data.try_into().map(Fvk::Orchard), DataTypecode::Orchard => data.try_into().map(Fvk::Orchard),
Typecode::Unknown(_) => Ok(Fvk::Unknown { typecode, data }), DataTypecode::Unknown(typecode) => Ok(Fvk::Unknown { typecode, data }),
} }
.map_err(|e| { .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) -> DataTypecode {
fn typecode(&self) -> Typecode {
match self { match self {
Fvk::P2pkh(_) => Typecode::P2pkh, Fvk::P2pkh(_) => DataTypecode::P2pkh,
Fvk::Sapling(_) => Typecode::Sapling, Fvk::Sapling(_) => DataTypecode::Sapling,
Fvk::Orchard(_) => Typecode::Orchard, Fvk::Orchard(_) => DataTypecode::Orchard,
Fvk::Unknown { typecode, .. } => Typecode::Unknown(*typecode), Fvk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
} }
} }
@ -83,7 +79,7 @@ impl SealedItem for Fvk {
/// ///
/// ``` /// ```
/// # use std::error::Error; /// # use std::error::Error;
/// use zcash_address::unified::{self, Container, Encoding}; /// use zcash_address::unified::{self, Container, Encoding, Item};
/// ///
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv"; /// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv";
@ -91,28 +87,22 @@ impl SealedItem for Fvk {
/// ///
/// let (network, ufvk) = unified::Ufvk::decode(example_ufvk)?; /// let (network, ufvk) = unified::Ufvk::decode(example_ufvk)?;
/// ///
/// // We can obtain the pool-specific Full Viewing Keys for the UFVK in preference /// // We can obtain the pool-specific Full Viewing Keys for the UFVK.
/// // order (the order in which wallets should prefer to use their corresponding /// let fvks: &[Item<unified::Fvk>] = ufvk.items_as_parsed();
/// // address receivers):
/// let fvks: Vec<unified::Fvk> = ufvk.items();
/// ///
/// // And we can create the UFVK from a list of FVKs: /// // 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); /// assert_eq!(new_ufvk, ufvk);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Ufvk(pub(crate) Vec<Fvk>); pub struct Ufvk(pub(crate) Vec<Item<Fvk>>);
impl Container for Ufvk { impl Container for Ufvk {
type Item = Fvk; type DataItem = Fvk;
/// Returns the FVKs contained within this UFVK, in the order they were fn items_as_parsed(&self) -> &[Item<Fvk>] {
/// 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] {
&self.0 &self.0
} }
} }
@ -137,7 +127,7 @@ impl SealedContainer for Ufvk {
/// The HRP for a Bech32m-encoded regtest Unified FVK. /// The HRP for a Bech32m-encoded regtest Unified FVK.
const REGTEST: &'static str = "uviewregtest"; const REGTEST: &'static str = "uviewregtest";
fn from_inner(fvks: Vec<Self::Item>) -> Self { fn from_inner(fvks: Vec<Item<Self::DataItem>>) -> Self {
Self(fvks) Self(fvks)
} }
} }
@ -148,12 +138,10 @@ mod tests {
use proptest::{array::uniform1, array::uniform32, prelude::*, sample::select}; use proptest::{array::uniform1, array::uniform32, prelude::*, sample::select};
use super::{Fvk, ParseError, Typecode, Ufvk}; use super::{Fvk, ParseError, Ufvk};
use crate::{ use crate::{
kind::unified::{ kind::unified::{private::SealedContainer, Encoding},
private::{SealedContainer, SealedItem}, unified::{Item, Typecode},
Container, Encoding,
},
Network, Network,
}; };
@ -211,8 +199,8 @@ mod tests {
shielded in arb_shielded_fvk(), shielded in arb_shielded_fvk(),
transparent in prop::option::of(arb_transparent_fvk()), transparent in prop::option::of(arb_transparent_fvk()),
) -> Ufvk { ) -> Ufvk {
let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect(); let mut items: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
items.sort_unstable_by(Fvk::encoding_order); items.sort_unstable_by(Item::encoding_order);
Ufvk(items) Ufvk(items)
} }
} }
@ -319,11 +307,14 @@ mod tests {
fn duplicate_typecode() { fn duplicate_typecode() {
// Construct and serialize an invalid Ufvk. This must be done using private // 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. // 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); let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET);
assert_eq!( assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]), 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) 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![],
},
]
)
}
} }

View File

@ -1,8 +1,8 @@
use std::convert::{TryFrom, TryInto}; use std::convert::TryInto;
use super::{ use super::{
private::{SealedContainer, SealedItem}, private::{SealedContainer, SealedDataItem},
Container, Encoding, ParseError, Typecode, Container, DataTypecode, Encoding, Item, ParseError,
}; };
/// The set of known IVKs for Unified IVKs. /// The set of known IVKs for Unified IVKs.
@ -44,31 +44,27 @@ pub enum Ivk {
}, },
} }
impl TryFrom<(u32, &[u8])> for Ivk { impl SealedDataItem for Ivk {
type Error = ParseError; fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
fn try_from((typecode, data): (u32, &[u8])) -> Result<Self, Self::Error> {
let data = data.to_vec(); let data = data.to_vec();
match typecode.try_into()? { match typecode {
Typecode::P2pkh => data.try_into().map(Ivk::P2pkh), DataTypecode::P2pkh => data.try_into().map(Ivk::P2pkh),
Typecode::P2sh => Err(data), DataTypecode::P2sh => Err(data),
Typecode::Sapling => data.try_into().map(Ivk::Sapling), DataTypecode::Sapling => data.try_into().map(Ivk::Sapling),
Typecode::Orchard => data.try_into().map(Ivk::Orchard), DataTypecode::Orchard => data.try_into().map(Ivk::Orchard),
Typecode::Unknown(_) => Ok(Ivk::Unknown { typecode, data }), DataTypecode::Unknown(typecode) => Ok(Ivk::Unknown { typecode, data }),
} }
.map_err(|e| { .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) -> DataTypecode {
fn typecode(&self) -> Typecode {
match self { match self {
Ivk::P2pkh(_) => Typecode::P2pkh, Ivk::P2pkh(_) => DataTypecode::P2pkh,
Ivk::Sapling(_) => Typecode::Sapling, Ivk::Sapling(_) => DataTypecode::Sapling,
Ivk::Orchard(_) => Typecode::Orchard, Ivk::Orchard(_) => DataTypecode::Orchard,
Ivk::Unknown { typecode, .. } => Typecode::Unknown(*typecode), Ivk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode),
} }
} }
@ -88,7 +84,7 @@ impl SealedItem for Ivk {
/// ///
/// ``` /// ```
/// # use std::error::Error; /// # use std::error::Error;
/// use zcash_address::unified::{self, Container, Encoding}; /// use zcash_address::unified::{self, Container, Encoding, Item};
/// ///
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq"; /// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq";
@ -96,28 +92,22 @@ impl SealedItem for Ivk {
/// ///
/// let (network, uivk) = unified::Uivk::decode(example_uivk)?; /// let (network, uivk) = unified::Uivk::decode(example_uivk)?;
/// ///
/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK in /// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK.
/// // preference order (the order in which wallets should prefer to use their /// let ivks: &[Item<unified::Ivk>] = uivk.items_as_parsed();
/// // corresponding address receivers):
/// let ivks: Vec<unified::Ivk> = uivk.items();
/// ///
/// // And we can create the UIVK from a list of IVKs: /// // And we can create the UIVK from a vector of IVKs:
/// let new_uivk = unified::Uivk::try_from_items(ivks)?; /// let new_uivk = unified::Uivk::try_from_items(ivks.to_vec())?;
/// assert_eq!(new_uivk, uivk); /// assert_eq!(new_uivk, uivk);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Uivk(pub(crate) Vec<Ivk>); pub struct Uivk(pub(crate) Vec<Item<Ivk>>);
impl Container for Uivk { impl Container for Uivk {
type Item = Ivk; type DataItem = Ivk;
/// Returns the IVKs contained within this UIVK, in the order they were fn items_as_parsed(&self) -> &[Item<Ivk>] {
/// 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] {
&self.0 &self.0
} }
} }
@ -142,7 +132,7 @@ impl SealedContainer for Uivk {
/// The HRP for a Bech32m-encoded regtest Unified IVK. /// The HRP for a Bech32m-encoded regtest Unified IVK.
const REGTEST: &'static str = "uivkregtest"; const REGTEST: &'static str = "uivkregtest";
fn from_inner(ivks: Vec<Self::Item>) -> Self { fn from_inner(ivks: Vec<Item<Self::DataItem>>) -> Self {
Self(ivks) Self(ivks)
} }
} }
@ -157,12 +147,10 @@ mod tests {
sample::select, sample::select,
}; };
use super::{Ivk, ParseError, Typecode, Uivk}; use super::{Ivk, ParseError, Uivk};
use crate::{ use crate::{
kind::unified::{ kind::unified::{private::SealedContainer, Encoding},
private::{SealedContainer, SealedItem}, unified::{Item, Typecode},
Container, Encoding,
},
Network, Network,
}; };
@ -204,8 +192,8 @@ mod tests {
shielded in arb_shielded_ivk(), shielded in arb_shielded_ivk(),
transparent in prop::option::of(arb_transparent_ivk()), transparent in prop::option::of(arb_transparent_ivk()),
) -> Uivk { ) -> Uivk {
let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect(); let mut items: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
items.sort_unstable_by(Ivk::encoding_order); items.sort_unstable_by(Item::encoding_order);
Uivk(items) Uivk(items)
} }
} }
@ -301,11 +289,14 @@ mod tests {
#[test] #[test]
fn duplicate_typecode() { fn duplicate_typecode() {
// Construct and serialize an invalid UIVK. // 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); let encoded = uivk.encode(&Network::Main);
assert_eq!( assert_eq!(
Uivk::decode(&encoded), Uivk::decode(&encoded),
Err(ParseError::DuplicateTypecode(Typecode::Sapling)) Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
); );
} }
@ -326,32 +317,4 @@ mod tests {
Err(ParseError::OnlyTransparent) 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![],
},
]
)
}
} }

View File

@ -8,6 +8,7 @@ use {
unified::{ unified::{
self, self,
address::{test_vectors::TEST_VECTORS, Receiver}, address::{test_vectors::TEST_VECTORS, Receiver},
Item,
}, },
Network, ToAddress, ZcashAddress, Network, ToAddress, ZcashAddress,
}, },
@ -36,6 +37,7 @@ fn unified() {
data: data.to_vec(), data: data.to_vec(),
}) })
})) }))
.map(Item::Data)
.collect(); .collect();
let expected_addr = ZcashAddress::from_unified(Network::Main, unified::Address(receivers)); let expected_addr = ZcashAddress::from_unified(Network::Main, unified::Address(receivers));