Compare commits

...

8 Commits

Author SHA1 Message Date
Kris Nuttycombe e15551b618
Merge fa225d479a into 895afe51f7 2024-04-23 22:31:03 +00:00
Kris Nuttycombe fa225d479a Allow transparent-only unified addresses and viewing keys. 2024-04-23 16:30:52 -06:00
Kris Nuttycombe cab4d84464 Apply suggestions from code review
Co-authored-by: Daira-Emma Hopwood <daira@jacaranda.org>
2024-04-22 17:54:32 -06:00
Kris Nuttycombe aea04d1ad2 `zcash_address`: Add support for ZIP 316, Revision 1 2024-04-22 17:53:44 -06:00
Kris Nuttycombe c0542c9589 zcash_keys: Box `Address` elements to avoid large variations in enum variant sizes 2024-04-22 17:52:33 -06:00
Kris Nuttycombe 34ec1e5bdb zcash_keys: Update key and address types to include ZIP-316 metadata items. 2024-04-22 17:52:28 -06:00
Kris Nuttycombe 0341171c84 zcash_address: Add handling for Unified Metadata Items 2024-04-22 17:18:53 -06:00
Kris Nuttycombe c3e1750007 zcash_keys: Add missing entries to `zcash_keys-0.1.0` CHANGELOG 2024-04-22 16:27:08 -06:00
25 changed files with 1344 additions and 707 deletions

View File

@ -9,11 +9,29 @@ 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}`
- `Container::revision`
- `DataTypecode`
- `Item`
- `MetadataItem`
- `MetadataTypecode`
- `Revision`
- 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`.
- `Encoding::try_from_items` now takes an additional `Revision` argument.
### 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, Revision},
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,30 @@ 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 {
revision: Revision::R0,
receivers: 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 {
revision: Revision::R0,
receivers: 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 {
revision: Revision::R0,
receivers: 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,213 @@ 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(
revision: Revision,
typecode: MetadataTypecode,
data: &[u8],
) -> Result<Self, ParseError> {
use MetadataTypecode::*;
use Revision::*;
match (revision, typecode) {
(R1, 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(),
)
}),
(R1, 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(),
)
}),
(R0, ExpiryHeight | ExpiryTime) => Err(ParseError::NotUnderstood(typecode.into())),
(R0 | R1, MustUnderstand(tc)) => Err(ParseError::NotUnderstood(tc)),
// This implementation treats the 0xC0..OxFD range as unknown metadata for both R0 and
// R1, as no typecodes were specified in this range for R0 and were "reclaimed" as
// metadata codes by ZIP 316 at the time R1 was introduced.
(R0 | R1, 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 +318,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 +329,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,67 +346,93 @@ 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
)
}
} }
} }
} }
impl Error for ParseError {} impl Error for ParseError {}
/// The revision of the Unified Address standard that an address was parsed under.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Revision {
R0,
R1,
}
pub(crate) mod private { pub(crate) mod private {
use super::{ParseError, Typecode, PADDING_LEN}; use super::{DataTypecode, ParseError, Revision, 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.
pub trait SealedContainer: super::Container + std::marker::Sized { pub trait SealedContainer: super::Container + std::marker::Sized {
const MAINNET: &'static str; const MAINNET_R0: &'static str;
const TESTNET: &'static str; const TESTNET_R0: &'static str;
const REGTEST: &'static str; const REGTEST_R0: &'static str;
const MAINNET_R1: &'static str;
const TESTNET_R1: &'static str;
const REGTEST_R1: &'static str;
/// 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(revision: Revision, items: Vec<Item<Self::DataItem>>) -> Self;
fn network_hrp(network: &Network) -> &'static str { fn network_hrp(revision: Revision, network: &Network) -> &'static str {
match network { match (revision, network) {
Network::Main => Self::MAINNET, (Revision::R0, Network::Main) => Self::MAINNET_R0,
Network::Test => Self::TESTNET, (Revision::R0, Network::Test) => Self::TESTNET_R0,
Network::Regtest => Self::REGTEST, (Revision::R0, Network::Regtest) => Self::REGTEST_R0,
(Revision::R1, Network::Main) => Self::MAINNET_R1,
(Revision::R1, Network::Test) => Self::TESTNET_R1,
(Revision::R1, Network::Regtest) => Self::REGTEST_R1,
}
}
fn hrp_revision(hrp: &str) -> Option<Revision> {
if hrp == Self::MAINNET_R0 || hrp == Self::TESTNET_R0 || hrp == Self::REGTEST_R0 {
Some(Revision::R0)
} else if hrp == Self::MAINNET_R1 || hrp == Self::TESTNET_R1 || hrp == Self::REGTEST_R1
{
Some(Revision::R1)
} else {
None
} }
} }
fn hrp_network(hrp: &str) -> Option<Network> { fn hrp_network(hrp: &str) -> Option<Network> {
if hrp == Self::MAINNET { if hrp == Self::MAINNET_R0 || hrp == Self::MAINNET_R1 {
Some(Network::Main) Some(Network::Main)
} else if hrp == Self::TESTNET { } else if hrp == Self::TESTNET_R0 || hrp == Self::TESTNET_R1 {
Some(Network::Test) Some(Network::Test)
} else if hrp == Self::REGTEST { } else if hrp == Self::REGTEST_R0 || hrp == Self::REGTEST_R1 {
Some(Network::Regtest) Some(Network::Regtest)
} else { } else {
None None
@ -227,7 +448,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 +469,15 @@ 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> { #[allow(clippy::type_complexity)]
fn read_receiver<R: SealedItem>( fn parse_items<T: Into<Vec<u8>>>(
hrp: &str,
buf: T,
) -> Result<(Revision, Vec<Item<Self::DataItem>>), ParseError> {
fn read_item<R: SealedDataItem>(
revision: Revision,
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 +505,18 @@ pub(crate) mod private {
length length
))); )));
} }
let result = R::try_from(( // The "as usize" casts cannot change the values, because both
typecode, // cursor.position() and addr_end are u64 values <= buf.len()
&buf[cursor.position() as usize..addr_end as usize], // which is 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(revision, 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.
@ -308,22 +540,27 @@ pub(crate) mod private {
)), )),
}?; }?;
let revision = Self::hrp_revision(hrp)
.ok_or_else(|| ParseError::UnknownPrefix(hrp.to_string()))?;
let mut cursor = std::io::Cursor::new(encoded); let mut cursor = std::io::Cursor::new(encoded);
let mut result = vec![]; let mut result = vec![];
while cursor.position() < encoded.len().try_into().unwrap() { while cursor.position() < encoded.len().try_into().unwrap() {
result.push(read_receiver(&mut cursor)?); result.push(read_item(revision, &mut cursor)?);
} }
assert_eq!(cursor.position(), encoded.len().try_into().unwrap()); assert_eq!(cursor.position(), encoded.len().try_into().unwrap());
Ok(result) Ok((revision, result))
} }
/// 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(
assert!(u32::from(Typecode::P2sh) == u32::from(Typecode::P2pkh) + 1); revision: Revision,
items: Vec<Item<Self::DataItem>>,
) -> Result<Self, ParseError> {
assert!(u32::from(Typecode::P2SH) == u32::from(Typecode::P2PKH) + 1);
let mut only_transparent = true;
let mut prev_code = None; // less than any Some let mut prev_code = None; // less than any Some
for item in &items { for item in &items {
let t = item.typecode(); let t = item.typecode();
@ -332,47 +569,46 @@ 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::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();
} }
} }
if only_transparent { // All checks pass!
Err(ParseError::OnlyTransparent) Ok(Self::from_inner(revision, items))
} else {
// All checks pass!
Ok(Self::from_inner(items))
}
} }
fn parse_internal<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Self, ParseError> { fn parse_internal<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Self, ParseError> {
Self::parse_items(hrp, buf).and_then(Self::try_from_items_internal) Self::parse_items(hrp, buf)
.and_then(|(revision, items)| Self::try_from_items_internal(revision, items))
} }
} }
} }
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
/// violated: /// violated:
/// * 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 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(
items.sort_unstable_by(Self::Item::encoding_order); revision: Revision,
Self::try_from_items_internal(items) mut items: Vec<Item<Self::DataItem>>,
) -> Result<Self, ParseError> {
items.sort_unstable_by(Item::encoding_order);
Self::try_from_items_internal(revision, items)
} }
/// Decodes a unified container from its string representation, preserving /// Decodes a unified container from its string representation, preserving
@ -399,7 +635,7 @@ pub trait Encoding: private::SealedContainer {
/// ordering of the contained items such that it correctly obeys round-trip /// ordering of the contained items such that it correctly obeys round-trip
/// serialization invariants. /// serialization invariants.
fn encode(&self, network: &Network) -> String { fn encode(&self, network: &Network) -> String {
let hrp = Self::network_hrp(network); let hrp = Self::network_hrp(self.revision(), network);
bech32::encode( bech32::encode(
hrp, hrp,
self.to_jumbled_bytes(hrp).to_base32(), self.to_jumbled_bytes(hrp).to_base32(),
@ -411,20 +647,13 @@ 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. /// Returns the revision of the ZIP 316 standard that this unified container
/// /// conforms to.
/// This API is for advanced usage; in most cases you should use `Self::items`. fn revision(&self) -> Revision;
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, Revision};
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, Revision},
/// ConversionError, TryFromRawAddress, ZcashAddress, /// ConversionError, TryFromRawAddress, ZcashAddress,
/// }; /// };
/// ///
@ -92,74 +97,132 @@ 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(Revision::R0, 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) revision: Revision,
pub(crate) receivers: 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
.receivers
.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.receivers.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.receivers
.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.receivers.iter().any(|r| {
.iter() matches!(
.any(|r| matches!(r, Receiver::Sapling(_) | Receiver::Orchard(_))) r,
Item::Data(Receiver::Sapling(_)) | Item::Data(Receiver::Orchard(_))
)
})
} }
} }
impl super::private::SealedContainer for Address { impl super::private::SealedContainer for Address {
/// The HRP for a Bech32m-encoded mainnet Unified Address. /// The HRP for a Bech32m-encoded mainnet Revision 0 Unified Address.
/// ///
/// Defined in [ZIP 316][zip-0316]. /// Defined in [ZIP 316][zip-0316].
/// ///
/// [zip-0316]: https://zips.z.cash/zip-0316 /// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET: &'static str = "u"; const MAINNET_R0: &'static str = "u";
/// The HRP for a Bech32m-encoded testnet Unified Address. /// The HRP for a Bech32m-encoded testnet Revision 0 Unified Address.
/// ///
/// Defined in [ZIP 316][zip-0316]. /// Defined in [ZIP 316][zip-0316].
/// ///
/// [zip-0316]: https://zips.z.cash/zip-0316 /// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET: &'static str = "utest"; const TESTNET_R0: &'static str = "utest";
/// The HRP for a Bech32m-encoded regtest Unified Address. /// The HRP for a Bech32m-encoded regtest Revision 0 Unified Address.
const REGTEST: &'static str = "uregtest"; const REGTEST_R0: &'static str = "uregtest";
fn from_inner(receivers: Vec<Self::Item>) -> Self { /// The HRP for a Bech32m-encoded mainnet Revision 1 Unified Address.
Self(receivers) ///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET_R1: &'static str = "ur";
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET_R1: &'static str = "urtest";
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified Address.
const REGTEST_R1: &'static str = "urregtest";
fn from_inner(revision: Revision, receivers: Vec<Item<Self::DataItem>>) -> Self {
Self {
revision,
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.receivers
}
fn revision(&self) -> Revision {
self.revision
} }
} }
#[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 +231,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, Revision};
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 +246,65 @@ 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 receivers = rs.into_iter().map(Item::Data).collect::<Vec<_>>();
receivers.sort_unstable_by(Item::encoding_order);
Address {
revision: Revision::R0,
receivers,
}
})
} }
} }
@ -249,14 +316,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, Revision, 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]
@ -285,7 +352,7 @@ mod tests {
0x7b, 0x28, 0x69, 0xc9, 0x84, 0x7b, 0x28, 0x69, 0xc9, 0x84,
]; ];
assert_eq!( assert_eq!(
Address::parse_internal(Address::MAINNET, &invalid_padding[..]), Address::parse_internal(Address::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding( Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned() "Invalid padding bytes".to_owned()
)) ))
@ -300,7 +367,7 @@ mod tests {
0x4b, 0x31, 0xee, 0x5a, 0x4b, 0x31, 0xee, 0x5a,
]; ];
assert_eq!( assert_eq!(
Address::parse_internal(Address::MAINNET, &truncated_padding[..]), Address::parse_internal(Address::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding( Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned() "Invalid padding bytes".to_owned()
)) ))
@ -325,7 +392,7 @@ mod tests {
0xc6, 0x5e, 0x68, 0xa2, 0x78, 0x6c, 0x9e, 0xc6, 0x5e, 0x68, 0xa2, 0x78, 0x6c, 0x9e,
]; ];
assert_matches!( assert_matches!(
Address::parse_internal(Address::MAINNET, &truncated_sapling_data[..]), Address::parse_internal(Address::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_)) Err(ParseError::InvalidEncoding(_))
); );
@ -338,7 +405,7 @@ mod tests {
0xe6, 0x70, 0x36, 0x5b, 0x7b, 0x9e, 0xe6, 0x70, 0x36, 0x5b, 0x7b, 0x9e,
]; ];
assert_matches!( assert_matches!(
Address::parse_internal(Address::MAINNET, &truncated_after_sapling_typecode[..]), Address::parse_internal(Address::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_)) Err(ParseError::InvalidEncoding(_))
); );
} }
@ -347,11 +414,17 @@ 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 {
let encoded = ua.to_jumbled_bytes(Address::MAINNET); revision: Revision::R0,
receivers: vec![
Item::Data(Receiver::Sapling([1; 43])),
Item::Data(Receiver::Sapling([2; 43])),
],
};
let encoded = ua.to_jumbled_bytes(Address::MAINNET_R0);
assert_eq!( assert_eq!(
Address::parse_internal(Address::MAINNET, &encoded[..]), Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::Sapling)) Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
); );
} }
@ -359,11 +432,17 @@ 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 {
let encoded = ua.to_jumbled_bytes(Address::MAINNET); revision: Revision::R0,
receivers: vec![
Item::Data(Receiver::P2pkh([0; 20])),
Item::Data(Receiver::P2sh([0; 20])),
],
};
let encoded = ua.to_jumbled_bytes(Address::MAINNET_R0);
// ensure that decoding catches the error // ensure that decoding catches the error
assert_eq!( assert_eq!(
Address::parse_internal(Address::MAINNET, &encoded[..]), Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::BothP2phkAndP2sh) Err(ParseError::BothP2phkAndP2sh)
); );
} }
@ -372,11 +451,17 @@ 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 {
let encoded = ua.to_jumbled_bytes(Address::MAINNET); revision: Revision::R0,
receivers: vec![
Item::Data(Receiver::Sapling([0; 43])),
Item::Data(Receiver::P2pkh([0; 20])),
],
};
let encoded = ua.to_jumbled_bytes(Address::MAINNET_R0);
// ensure that decoding catches the error // ensure that decoding catches the error
assert_eq!( assert_eq!(
Address::parse_internal(Address::MAINNET, &encoded[..]), Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::InvalidTypecodeOrder) Err(ParseError::InvalidTypecodeOrder)
); );
} }
@ -395,7 +480,7 @@ mod tests {
// with only one of them we don't have sufficient data for F4Jumble (so we hit a // with only one of them we don't have sufficient data for F4Jumble (so we hit a
// different error). // different error).
assert_matches!( assert_matches!(
Address::parse_internal(Address::MAINNET, &encoded[..]), Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::InvalidEncoding(_)) Err(ParseError::InvalidEncoding(_))
); );
} }
@ -403,19 +488,22 @@ mod tests {
#[test] #[test]
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 {
Receiver::P2pkh([0; 20]), revision: Revision::R0,
Receiver::Orchard([0; 43]), receivers: vec![
Receiver::Unknown { Item::Data(Receiver::P2pkh([0; 20])),
typecode: 0xff, Item::Data(Receiver::Orchard([0; 43])),
data: vec![], Item::Data(Receiver::Unknown {
}, typecode: 0xff,
Receiver::Sapling([0; 43]), data: vec![],
]); }),
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, Revision,
}; };
/// 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, Revision};
/// ///
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv"; /// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv";
@ -91,54 +87,72 @@ 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(Revision::R0, 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) revision: Revision,
pub(crate) fvks: 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. &self.fvks
/// }
/// This API is for advanced usage; in most cases you should use `Ufvk::receivers`.
fn items_as_parsed(&self) -> &[Fvk] { fn revision(&self) -> Revision {
&self.0 self.revision
} }
} }
impl Encoding for Ufvk {} impl Encoding for Ufvk {}
impl SealedContainer for Ufvk { impl SealedContainer for Ufvk {
/// The HRP for a Bech32m-encoded mainnet Unified FVK. /// The HRP for a Bech32m-encoded mainnet Revision 0 Unified FVK.
/// ///
/// Defined in [ZIP 316][zip-0316]. /// Defined in [ZIP 316][zip-0316].
/// ///
/// [zip-0316]: https://zips.z.cash/zip-0316 /// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET: &'static str = "uview"; const MAINNET_R0: &'static str = "uview";
/// The HRP for a Bech32m-encoded testnet Unified FVK. /// The HRP for a Bech32m-encoded testnet Revision 0 Unified FVK.
/// ///
/// Defined in [ZIP 316][zip-0316]. /// Defined in [ZIP 316][zip-0316].
/// ///
/// [zip-0316]: https://zips.z.cash/zip-0316 /// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET: &'static str = "uviewtest"; const TESTNET_R0: &'static str = "uviewtest";
/// The HRP for a Bech32m-encoded regtest Unified FVK. /// The HRP for a Bech32m-encoded regtest Revision 0 Unified FVK.
const REGTEST: &'static str = "uviewregtest"; const REGTEST_R0: &'static str = "uviewregtest";
fn from_inner(fvks: Vec<Self::Item>) -> Self { /// The HRP for a Bech32m-encoded mainnet Revision 1 Unified FVK.
Self(fvks) ///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET_R1: &'static str = "urview";
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified FVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET_R1: &'static str = "urviewtest";
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified FVK.
const REGTEST_R1: &'static str = "urviewregtest";
fn from_inner(revision: Revision, fvks: Vec<Item<Self::DataItem>>) -> Self {
Self { revision, fvks }
} }
} }
@ -148,12 +162,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, Revision, Typecode},
Container, Encoding,
},
Network, Network,
}; };
@ -211,9 +223,9 @@ 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 fvks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
items.sort_unstable_by(Fvk::encoding_order); fvks.sort_unstable_by(Item::encoding_order);
Ufvk(items) Ufvk { revision: Revision::R0, fvks }
} }
} }
@ -245,7 +257,7 @@ mod tests {
0xdf, 0x63, 0xe7, 0xef, 0x65, 0x6b, 0x18, 0x23, 0xf7, 0x3e, 0x35, 0x7c, 0xf3, 0xc4, 0xdf, 0x63, 0xe7, 0xef, 0x65, 0x6b, 0x18, 0x23, 0xf7, 0x3e, 0x35, 0x7c, 0xf3, 0xc4,
]; ];
assert_eq!( assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &invalid_padding[..]), Ufvk::parse_internal(Ufvk::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding( Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned() "Invalid padding bytes".to_owned()
)) ))
@ -263,7 +275,7 @@ mod tests {
0x43, 0x8e, 0xc0, 0x3e, 0x9f, 0xf4, 0xf1, 0x80, 0x32, 0xcf, 0x2f, 0x7e, 0x7f, 0x91, 0x43, 0x8e, 0xc0, 0x3e, 0x9f, 0xf4, 0xf1, 0x80, 0x32, 0xcf, 0x2f, 0x7e, 0x7f, 0x91,
]; ];
assert_eq!( assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_padding[..]), Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding( Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned() "Invalid padding bytes".to_owned()
)) ))
@ -295,7 +307,7 @@ mod tests {
0x8c, 0x7a, 0xbf, 0x7b, 0x9a, 0xdd, 0xee, 0x18, 0x2c, 0x2d, 0xc2, 0xfc, 0x8c, 0x7a, 0xbf, 0x7b, 0x9a, 0xdd, 0xee, 0x18, 0x2c, 0x2d, 0xc2, 0xfc,
]; ];
assert_matches!( assert_matches!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_sapling_data[..]), Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_)) Err(ParseError::InvalidEncoding(_))
); );
@ -310,7 +322,7 @@ mod tests {
0x54, 0xd1, 0x9e, 0xec, 0x8b, 0xef, 0x35, 0xb8, 0x44, 0xdd, 0xab, 0x9a, 0x8d, 0x54, 0xd1, 0x9e, 0xec, 0x8b, 0xef, 0x35, 0xb8, 0x44, 0xdd, 0xab, 0x9a, 0x8d,
]; ];
assert_matches!( assert_matches!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_after_sapling_typecode[..]), Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_)) Err(ParseError::InvalidEncoding(_))
); );
} }
@ -319,11 +331,17 @@ 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 {
let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET); revision: Revision::R0,
fvks: vec![
Item::Data(Fvk::Sapling([1; 128])),
Item::Data(Fvk::Sapling([2; 128])),
],
};
let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET_R0);
assert_eq!( assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]), Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::Sapling)) Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
); );
} }
@ -339,37 +357,6 @@ mod tests {
0xf4, 0xf5, 0x16, 0xef, 0x5c, 0xe0, 0x26, 0xbc, 0x23, 0x73, 0x76, 0x3f, 0x4b, 0xf4, 0xf5, 0x16, 0xef, 0x5c, 0xe0, 0x26, 0xbc, 0x23, 0x73, 0x76, 0x3f, 0x4b,
]; ];
assert_eq!( assert_matches!(Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]), Ok(_));
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
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, Revision,
}; };
/// 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, Revision};
/// ///
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq"; /// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq";
@ -96,54 +92,72 @@ 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(Revision::R0, 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) revision: Revision,
pub(crate) ivks: 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. &self.ivks
/// }
/// This API is for advanced usage; in most cases you should use `Uivk::items`.
fn items_as_parsed(&self) -> &[Ivk] { fn revision(&self) -> Revision {
&self.0 self.revision
} }
} }
impl Encoding for Uivk {} impl Encoding for Uivk {}
impl SealedContainer for Uivk { impl SealedContainer for Uivk {
/// The HRP for a Bech32m-encoded mainnet Unified IVK. /// The HRP for a Bech32m-encoded mainnet Revision 0 Unified IVK.
/// ///
/// Defined in [ZIP 316][zip-0316]. /// Defined in [ZIP 316][zip-0316].
/// ///
/// [zip-0316]: https://zips.z.cash/zip-0316 /// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET: &'static str = "uivk"; const MAINNET_R0: &'static str = "uivk";
/// The HRP for a Bech32m-encoded testnet Unified IVK. /// The HRP for a Bech32m-encoded testnet Revision 0 Unified IVK.
/// ///
/// Defined in [ZIP 316][zip-0316]. /// Defined in [ZIP 316][zip-0316].
/// ///
/// [zip-0316]: https://zips.z.cash/zip-0316 /// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET: &'static str = "uivktest"; const TESTNET_R0: &'static str = "uivktest";
/// The HRP for a Bech32m-encoded regtest Unified IVK. /// The HRP for a Bech32m-encoded regtest Revision 0 Unified IVK.
const REGTEST: &'static str = "uivkregtest"; const REGTEST_R0: &'static str = "uivkregtest";
fn from_inner(ivks: Vec<Self::Item>) -> Self { /// The HRP for a Bech32m-encoded mainnet Revision 1 Unified IVK.
Self(ivks) ///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET_R1: &'static str = "urivk";
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified IVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET_R1: &'static str = "urivktest";
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified IVK.
const REGTEST_R1: &'static str = "urivkregtest";
fn from_inner(revision: Revision, ivks: Vec<Item<Self::DataItem>>) -> Self {
Self { revision, ivks }
} }
} }
@ -157,12 +171,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, Revision, Typecode},
Container, Encoding,
},
Network, Network,
}; };
@ -204,9 +216,9 @@ 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 ivks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
items.sort_unstable_by(Ivk::encoding_order); ivks.sort_unstable_by(Item::encoding_order);
Uivk(items) Uivk { revision: Revision::R0, ivks }
} }
} }
@ -236,7 +248,7 @@ mod tests {
0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15, 0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15,
]; ];
assert_eq!( assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..]), Uivk::parse_internal(Uivk::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding( Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned() "Invalid padding bytes".to_owned()
)) ))
@ -252,7 +264,7 @@ mod tests {
0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47, 0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47,
]; ];
assert_eq!( assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..]), Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding( Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned() "Invalid padding bytes".to_owned()
)) ))
@ -280,7 +292,7 @@ mod tests {
0xf5, 0xd5, 0x8a, 0xb5, 0x1a, 0xf5, 0xd5, 0x8a, 0xb5, 0x1a,
]; ];
assert_matches!( assert_matches!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..]), Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_)) Err(ParseError::InvalidEncoding(_))
); );
@ -293,7 +305,7 @@ mod tests {
0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74, 0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74,
]; ];
assert_matches!( assert_matches!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_after_sapling_typecode[..]), Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_)) Err(ParseError::InvalidEncoding(_))
); );
} }
@ -301,11 +313,17 @@ 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 {
revision: Revision::R0,
ivks: 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))
); );
} }
@ -321,37 +339,6 @@ mod tests {
0xbd, 0xfe, 0xa4, 0xb7, 0x47, 0x20, 0x92, 0x6, 0xf0, 0x0, 0xf9, 0x64, 0xbd, 0xfe, 0xa4, 0xb7, 0x47, 0x20, 0x92, 0x6, 0xf0, 0x0, 0xf9, 0x64,
]; ];
assert_eq!( assert_matches!(Uivk::parse_internal(Uivk::MAINNET_R0, &encoded[..]), Ok(_));
Uivk::parse_internal(Uivk::MAINNET, &encoded[..]),
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, Revision,
}, },
Network, ToAddress, ZcashAddress, Network, ToAddress, ZcashAddress,
}, },
@ -36,9 +37,16 @@ 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 {
revision: Revision::R0,
receivers,
},
);
// Test parsing // Test parsing
let addr: ZcashAddress = tv.unified_addr.parse().unwrap(); let addr: ZcashAddress = tv.unified_addr.parse().unwrap();

View File

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

View File

@ -133,6 +133,9 @@ and this library adheres to Rust's notion of
- Arguments to `ChangeStrategy::compute_balance` have changed. - Arguments to `ChangeStrategy::compute_balance` have changed.
- `ChangeError::DustInputs` now has an `orchard` field behind the `orchard` - `ChangeError::DustInputs` now has an `orchard` field behind the `orchard`
feature flag. feature flag.
- `zcash_client_backend::wallet`:
- The address variants of `Recipient` now `Box` their contents to avoid large
discrepancies in enum variant sizing.
- `zcash_client_backend::proto`: - `zcash_client_backend::proto`:
- `ProposalDecodingError` has a new variant `TransparentMemo`. - `ProposalDecodingError` has a new variant `TransparentMemo`.
- `zcash_client_backend::wallet::Recipient::InternalAccount` is now a structured - `zcash_client_backend::wallet::Recipient::InternalAccount` is now a structured

View File

@ -859,7 +859,7 @@ where
.convert_if_network(params.network_type())?; .convert_if_network(params.network_type())?;
let recipient_taddr = match recipient_address { let recipient_taddr = match recipient_address {
Address::Transparent(t) => Some(t), Address::Transparent(t) => Some(t.as_ref()),
Address::Unified(uaddr) => uaddr.transparent(), Address::Unified(uaddr) => uaddr.transparent(),
_ => None, _ => None,
} }
@ -1022,7 +1022,7 @@ where
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone()); let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_sapling_output( builder.add_sapling_output(
sapling_external_ovk, sapling_external_ovk,
addr, *addr,
payment.amount(), payment.amount(),
memo.clone(), memo.clone(),
)?; )?;

View File

@ -8,7 +8,7 @@ use std::{
}; };
use nonempty::NonEmpty; use nonempty::NonEmpty;
use zcash_address::ConversionError; use zcash_address::{ConversionError, ZcashAddress};
use zcash_primitives::{ use zcash_primitives::{
consensus::{self, BlockHeight}, consensus::{self, BlockHeight},
transaction::{ transaction::{
@ -21,7 +21,7 @@ use zcash_primitives::{
}; };
use crate::{ use crate::{
address::{Address, UnifiedAddress}, address::Address,
data_api::{InputSource, SimpleNoteRetention, SpendableNotes}, data_api::{InputSource, SimpleNoteRetention, SpendableNotes},
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy}, fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy},
proposal::{Proposal, ProposalError, ShieldedInputs}, proposal::{Proposal, ProposalError, ShieldedInputs},
@ -221,7 +221,7 @@ pub enum GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT> {
/// An intermediate value overflowed or underflowed the valid monetary range. /// An intermediate value overflowed or underflowed the valid monetary range.
Balance(BalanceError), Balance(BalanceError),
/// A unified address did not contain a supported receiver. /// A unified address did not contain a supported receiver.
UnsupportedAddress(Box<UnifiedAddress>), UnsupportedAddress(ZcashAddress),
/// An error was encountered in change selection. /// An error was encountered in change selection.
Change(ChangeError<ChangeStrategyErrT, NoteRefT>), Change(ChangeError<ChangeStrategyErrT, NoteRefT>),
} }
@ -234,10 +234,12 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for GreedyInputSelectorErro
"A balance calculation violated amount validity bounds: {:?}.", "A balance calculation violated amount validity bounds: {:?}.",
e e
), ),
GreedyInputSelectorError::UnsupportedAddress(_) => { GreedyInputSelectorError::UnsupportedAddress(addr) => {
// we can't encode the UA to its string representation because we write!(
// don't have network parameters here f,
write!(f, "Unified address contains no supported receivers.") "Unified address {} contains no supported receivers.",
addr.encode()
)
} }
GreedyInputSelectorError::Change(err) => { GreedyInputSelectorError::Change(err) => {
write!(f, "An error occurred computing change and fees: {}", err) write!(f, "An error occurred computing change and fees: {}", err)
@ -401,7 +403,9 @@ where
} }
return Err(InputSelectorError::Selection( return Err(InputSelectorError::Selection(
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr)), GreedyInputSelectorError::UnsupportedAddress(
payment.recipient_address().clone(),
),
)); ));
} }
} }

View File

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

View File

@ -1682,7 +1682,7 @@ fn fake_compact_block_spending<P: consensus::Parameters, Fvk: TestFvk>(
compact_sapling_output( compact_sapling_output(
params, params,
height, height,
recipient, *recipient,
value, value,
fvk.sapling_ovk(), fvk.sapling_ovk(),
&mut rng, &mut rng,

View File

@ -332,7 +332,7 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
// spends the first step's output. // spends the first step's output.
// The first step will deshield to the wallet's default transparent address // The first step will deshield to the wallet's default transparent address
let to0 = Address::Transparent(account.usk().default_transparent_address().0); let to0 = Address::from(account.usk().default_transparent_address().0);
let request0 = zip321::TransactionRequest::new(vec![Payment::without_memo( let request0 = zip321::TransactionRequest::new(vec![Payment::without_memo(
to0.to_zcash_address(&st.network()), to0.to_zcash_address(&st.network()),
NonNegativeAmount::const_from_u64(50000), NonNegativeAmount::const_from_u64(50000),
@ -364,7 +364,7 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
// We'll use an internal transparent address that hasn't been added to the wallet // We'll use an internal transparent address that hasn't been added to the wallet
// to simulate an external transparent recipient. // to simulate an external transparent recipient.
let to1 = Address::Transparent( let to1 = Address::from(
account account
.usk() .usk()
.transparent() .transparent()
@ -1620,7 +1620,7 @@ pub(crate) fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTes
let transfer_amount = NonNegativeAmount::const_from_u64(200000); let transfer_amount = NonNegativeAmount::const_from_u64(200000);
let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo( let p0_to_p1 = zip321::TransactionRequest::new(vec![Payment::without_memo(
Address::Transparent(p1_to).to_zcash_address(&st.network()), Address::Transparent(Box::new(p1_to)).to_zcash_address(&st.network()),
transfer_amount, transfer_amount,
)]) )])
.unwrap(); .unwrap();

View File

@ -555,7 +555,7 @@ pub(crate) fn get_current_address<P: consensus::Parameters>(
SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
}) })
.and_then(|addr| match addr { .and_then(|addr| match addr {
Address::Unified(ua) => Ok(ua), Address::Unified(ua) => Ok(*ua),
_ => Err(SqliteClientError::CorruptedData(format!( _ => Err(SqliteClientError::CorruptedData(format!(
"Addresses table contains {} which is not a unified address", "Addresses table contains {} which is not a unified address",
addr_str, addr_str,
@ -683,7 +683,7 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
conn: &rusqlite::Connection, conn: &rusqlite::Connection,
account_id: AccountId, account_id: AccountId,
) -> Result<Option<(TransparentAddress, NonHardenedChildIndex)>, SqliteClientError> { ) -> Result<Option<(TransparentAddress, NonHardenedChildIndex)>, SqliteClientError> {
use zcash_address::unified::Container; use zcash_address::unified::{Container, Item};
use zcash_primitives::legacy::keys::ExternalIvk; use zcash_primitives::legacy::keys::ExternalIvk;
// Get the UIVK for the account. // Get the UIVK for the account.
@ -705,9 +705,9 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
} }
// Derive the default transparent address (if it wasn't already part of a derived UA). // Derive the default transparent address (if it wasn't already part of a derived UA).
for item in uivk.items() { for item in uivk.items_as_parsed() {
if let Ivk::P2pkh(tivk_bytes) = item { if let Item::Data(Ivk::P2pkh(tivk_bytes)) = item {
let tivk = ExternalIvk::deserialize(&tivk_bytes)?; let tivk = ExternalIvk::deserialize(tivk_bytes)?;
return Ok(Some(tivk.default_address())); return Ok(Some(tivk.default_address()));
} }
} }
@ -2386,7 +2386,7 @@ pub(crate) fn select_receiving_address<P: consensus::Parameters>(
FROM addresses FROM addresses
WHERE cached_transparent_receiver_address = :taddr", WHERE cached_transparent_receiver_address = :taddr",
named_params! { named_params! {
":taddr": Address::Transparent(*taddr).encode(_params) ":taddr": Address::Transparent(Box::new(*taddr)).encode(_params)
}, },
|row| row.get::<_, String>(0), |row| row.get::<_, String>(0),
) )

View File

@ -1418,8 +1418,9 @@ mod tests {
// Unified addresses at the time of the addition of migrations did not contain an // Unified addresses at the time of the addition of migrations did not contain an
// Orchard component. // Orchard component.
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT); let ua_request =
let address_str = Address::Unified( UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
let address_str = Address::from(
ufvk.default_address(ua_request) ufvk.default_address(ua_request)
.expect("A valid default address exists for the UFVK") .expect("A valid default address exists for the UFVK")
.0, .0,
@ -1438,7 +1439,7 @@ mod tests {
// add a transparent "sent note" // add a transparent "sent note"
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
{ {
let taddr = Address::Transparent( let taddr = Address::from(
*ufvk *ufvk
.default_address(ua_request) .default_address(ua_request)
.expect("A valid default address exists for the UFVK") .expect("A valid default address exists for the UFVK")
@ -1545,7 +1546,8 @@ mod tests {
assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork)); assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
// hardcoded with knowledge of what's coming next // hardcoded with knowledge of what's coming next
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, true); let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true);
db_data db_data
.get_next_available_address(account_id, ua_request) .get_next_available_address(account_id, ua_request)
.unwrap() .unwrap()

View File

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

View File

@ -79,20 +79,20 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
)) ))
})?; })?;
let decoded_address = if let Address::Unified(ua) = decoded { let decoded_address = if let Address::Unified(ua) = decoded {
ua *ua
} else { } else {
return Err(WalletMigrationError::CorruptedData( return Err(WalletMigrationError::CorruptedData(
"Address in accounts table was not a Unified Address.".to_string(), "Address in accounts table was not a Unified Address.".to_string(),
)); ));
}; };
let (expected_address, idx) = ufvk.default_address( let (expected_address, idx) = ufvk.default_address(
UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT), UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT),
)?; )?;
if decoded_address != expected_address { if decoded_address != expected_address {
return Err(WalletMigrationError::CorruptedData(format!( return Err(WalletMigrationError::CorruptedData(format!(
"Decoded UA {} does not match the UFVK's default address {} at {:?}.", "Decoded UA {} does not match the UFVK's default address {} at {:?}.",
address, address,
Address::Unified(expected_address).encode(&self.params), Address::from(expected_address).encode(&self.params),
idx, idx,
))); )));
} }
@ -110,7 +110,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
let decoded_transparent_address = if let Address::Transparent(addr) = let decoded_transparent_address = if let Address::Transparent(addr) =
decoded_transparent decoded_transparent
{ {
addr *addr
} else { } else {
return Err(WalletMigrationError::CorruptedData( return Err(WalletMigrationError::CorruptedData(
"Address in transparent_address column of accounts table was not a transparent address.".to_string(), "Address in transparent_address column of accounts table was not a transparent address.".to_string(),
@ -157,11 +157,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
], ],
)?; )?;
let (address, d_idx) = ufvk.default_address(UnifiedAddressRequest::unsafe_new( let (address, d_idx) = ufvk.default_address(
false, UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT),
true, )?;
UA_TRANSPARENT,
))?;
insert_address(transaction, &self.params, account, d_idx, &address)?; insert_address(transaction, &self.params, account, d_idx, &address)?;
} }

View File

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

View File

@ -83,7 +83,8 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
// our second assumption above, and we report this as corrupted data. // our second assumption above, and we report this as corrupted data.
let mut seed_is_relevant = false; let mut seed_is_relevant = false;
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT); let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
let mut rows = stmt_fetch_accounts.query([])?; let mut rows = stmt_fetch_accounts.query([])?;
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
// We only need to check for the presence of the seed if we have keys that // We only need to check for the presence of the seed if we have keys that
@ -119,12 +120,12 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
let dfvk = ufvk.sapling().ok_or_else(|| let dfvk = ufvk.sapling().ok_or_else(||
WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?; WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?;
let (idx, expected_address) = dfvk.default_address(); let (idx, expected_address) = dfvk.default_address();
if decoded_address != expected_address { if *decoded_address != expected_address {
return Err(if seed_is_relevant { return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData( WalletMigrationError::CorruptedData(
format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.", format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
address, address,
Address::Sapling(expected_address).encode(&self.params), Address::from(expected_address).encode(&self.params),
idx)) idx))
} else { } else {
WalletMigrationError::SeedNotRelevant WalletMigrationError::SeedNotRelevant
@ -137,12 +138,12 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
} }
Address::Unified(decoded_address) => { Address::Unified(decoded_address) => {
let (expected_address, idx) = ufvk.default_address(ua_request)?; let (expected_address, idx) = ufvk.default_address(ua_request)?;
if decoded_address != expected_address { if *decoded_address != expected_address {
return Err(if seed_is_relevant { return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData( WalletMigrationError::CorruptedData(
format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.", format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
address, address,
Address::Unified(expected_address).encode(&self.params), Address::from(expected_address).encode(&self.params),
idx)) idx))
} else { } else {
WalletMigrationError::SeedNotRelevant WalletMigrationError::SeedNotRelevant

View File

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

View File

@ -12,7 +12,12 @@ and this library adheres to Rust's notion of
## [0.2.0] - 2024-03-25 ## [0.2.0] - 2024-03-25
### Added ### Added
- `zcash_keys::address::Address::has_receiver` - `zcash_keys::address`:
- `Address::has_receiver`
- `UnifiedAddress::{
new, expiry_height, expiry_time,
unknown_data, unknown_metadata
}`
- `impl Display for zcash_keys::keys::AddressGenerationError` - `impl Display for zcash_keys::keys::AddressGenerationError`
- `impl std::error::Error for zcash_keys::keys::AddressGenerationError` - `impl std::error::Error for zcash_keys::keys::AddressGenerationError`
- `impl From<hdwallet::error::Error> for zcash_keys::keys::DerivationError` - `impl From<hdwallet::error::Error> for zcash_keys::keys::DerivationError`
@ -31,10 +36,15 @@ and this library adheres to Rust's notion of
must be enabled for the `keys` module to be accessible. must be enabled for the `keys` module to be accessible.
- Updated to `zcash_primitives-0.15.0` - Updated to `zcash_primitives-0.15.0`
### Changed
- `zcash_keys::address::Address` variants now `Box` their contents to
avoid large discrepancies in enum variant sizing.
### Removed ### Removed
- `UnifiedFullViewingKey::new` has been placed behind the `test-dependencies` - `UnifiedFullViewingKey::new` has been placed behind the `test-dependencies`
feature flag. UFVKs should only be produced by derivation from the USK, or feature flag. UFVKs should only be produced by derivation from the USK, or
parsed from their string representation. parsed from their string representation.
- `zcash_keys::address::UnifiedAddress::from_receivers`
### Fixed ### Fixed
- `UnifiedFullViewingKey::find_address` can now find an address for a diversifier - `UnifiedFullViewingKey::find_address` can now find an address for a diversifier
@ -69,28 +79,47 @@ The entries below are relative to the `zcash_client_backend` crate as of
- A new `orchard` feature flag has been added to make it possible to - A new `orchard` feature flag has been added to make it possible to
build client code without `orchard` dependendencies. build client code without `orchard` dependendencies.
- `zcash_keys::address::Address::to_zcash_address` - `zcash_keys::address::Address::to_zcash_address`
- A new `sapling` feature flag has been added to make it possible to
build client code without `sapling` dependendencies.
- A new `transparent-inputs` feature flag has been added to make it possible to
build client code without providing support for generating transparent
addresses.
### Changed ### Changed
- The following methods and enum variants have been placed behind an `orchard` - The following methods, method arguments, and enum variants have been placed
feature flag: behind the `orchard` feature flag:
- `zcash_keys::address::UnifiedAddress::from_receivers` no longer takes an
Orchard receiver argument unless the `orchard` feature is enabled.
- `zcash_keys::keys::UnifiedFullViewingKey::new` no longer takes
an Orchard key argument unless the `orchard` feature is enabled.
- `zcash_keys::address::UnifiedAddress::orchard` - `zcash_keys::address::UnifiedAddress::orchard`
- `zcash_keys::keys::DerivationError::Orchard` - `zcash_keys::keys::DerivationError::Orchard`
- `zcash_keys::keys::UnifiedSpendingKey::orchard` - `zcash_keys::keys::UnifiedSpendingKey::orchard`
- `zcash_keys::keys::UnifiedFullViewingKey::orchard`
- The following methods and method arguments have been placed behind the
`sapling` feature flag:
- `UnifiedAddress::from_receivers` no longer takes a Sapling receiver
argument unless the `sapling` feature is enabled.
- `zcash_keys::keys::UnifiedFullViewingKey::new` no longer takes
a Sapling key argument unless the `sapling` feature is enabled.
- `zcash_keys::address::UnifiedAddress::sapling`
- `zcash_keys::keys::UnifiedSpendingKey::sapling`
- `zcash_keys::keys::UnifiedFullViewingKey::sapling`
- The following methods and method arguments have been placed behind the
`transparent-inputs` feature flag:
- `zcash_keys::keys::UnifiedFullViewingKey::transparent` no longer takes
a transparent key argument unless the `transparent-inputs` feature is enabled.
- `zcash_keys::keys::UnifiedSpendingKey::transparent`
- `zcash_keys::keys::UnifiedFullViewingKey::transparent`
- `zcash_keys::address`: - `zcash_keys::address`:
- `RecipientAddress` has been renamed to `Address`. - `RecipientAddress` has been renamed to `Address`.
- `Address::Shielded` has been renamed to `Address::Sapling`. - `Address::Shielded` has been renamed to `Address::Sapling`.
- `UnifiedAddress::from_receivers` no longer takes an Orchard receiver
argument unless the `orchard` feature is enabled.
- `zcash_keys::keys`: - `zcash_keys::keys`:
- `UnifiedSpendingKey::address` now takes an argument that specifies the - `UnifiedSpendingKey::address` now takes an argument that specifies the
receivers to be generated in the resulting address. Also, it now returns receivers to be generated in the resulting address. Also, it now returns
`Result<UnifiedAddress, AddressGenerationError>` instead of `Result<UnifiedAddress, AddressGenerationError>` instead of
`Option<UnifiedAddress>` so that we may better report to the user how `Option<UnifiedAddress>` so that we may better report to the user how
address generation has failed. address generation has failed.
- `UnifiedSpendingKey::transparent` is now only available when the
`transparent-inputs` feature is enabled.
- `UnifiedFullViewingKey::new` no longer takes an Orchard full viewing key
argument unless the `orchard` feature is enabled.
### Removed ### Removed
- `zcash_keys::address::AddressMetadata` - `zcash_keys::address::AddressMetadata`

View File

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

File diff suppressed because it is too large Load Diff