`zcash_address`: Add support for ZIP 316, Revision 1

This commit is contained in:
Kris Nuttycombe 2024-02-16 14:49:47 -07:00
parent c0542c9589
commit aea04d1ad2
9 changed files with 327 additions and 148 deletions

View File

@ -11,10 +11,12 @@ and this library adheres to Rust's notion of
- `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as, matches_receiver}`
- `zcash_address::unified`:
- `Address::{can_receive_memo, has_receiver_of_type, contains_receiver, receivers}`
- `Container::revision`
- `DataTypecode`
- `MetadataTypecode`
- `Item`
- `MetadataItem`
- `MetadataTypecode`
- `Revision`
- Module `zcash_address::testing` under the `test-dependencies` feature.
- Module `zcash_address::unified::address::testing` under the
`test-dependencies` feature.
@ -23,6 +25,7 @@ and this library adheres to Rust's notion of
- `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

View File

@ -182,7 +182,7 @@ mod tests {
use super::*;
use crate::{
kind::unified,
unified::{Item, Receiver},
unified::{Item, Receiver, Revision},
Network,
};
@ -234,21 +234,30 @@ mod tests {
"u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl",
ZcashAddress {
net: Network::Main,
kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);
encoding(
"utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s",
ZcashAddress {
net: Network::Test,
kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);
encoding(
"uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3",
ZcashAddress {
net: Network::Regtest,
kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);

View File

@ -215,9 +215,15 @@ pub enum MetadataItem {
impl MetadataItem {
/// Parse a metadata item for the specified metadata typecode from the provided bytes.
pub fn parse(typecode: MetadataTypecode, data: &[u8]) -> Result<Self, ParseError> {
match typecode {
MetadataTypecode::ExpiryHeight => data
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)
@ -226,7 +232,7 @@ impl MetadataItem {
"Expiry height must be a 32-bit little-endian value.".to_string(),
)
}),
MetadataTypecode::ExpiryTime => data
(R1, ExpiryTime) => data
.try_into()
.map(u64::from_le_bytes)
.map(MetadataItem::ExpiryTime)
@ -235,11 +241,12 @@ impl MetadataItem {
"Expiry time must be a 64-bit little-endian value.".to_string(),
)
}),
MetadataTypecode::MustUnderstand(tc) => Err(ParseError::NotUnderstood(tc)),
MetadataTypecode::Unknown(typecode) => Ok(MetadataItem::Unknown {
(R0 | R1, MustUnderstand(tc)) => Err(ParseError::NotUnderstood(tc)),
(R0 | R1, Unknown(typecode)) => Ok(MetadataItem::Unknown {
typecode,
data: data.to_vec(),
}),
(R0, ExpiryHeight | ExpiryTime) => Err(ParseError::NotUnderstood(typecode.into())),
}
}
@ -349,8 +356,15 @@ impl fmt::Display 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 {
use super::{DataTypecode, ParseError, Typecode, PADDING_LEN};
use super::{DataTypecode, ParseError, Revision, Typecode, PADDING_LEN};
use crate::{
unified::{Item, MetadataItem},
Network,
@ -375,29 +389,47 @@ pub(crate) mod private {
/// A Unified Container containing addresses or viewing keys.
pub trait SealedContainer: super::Container + std::marker::Sized {
const MAINNET: &'static str;
const TESTNET: &'static str;
const REGTEST: &'static str;
const MAINNET_R0: &'static str;
const TESTNET_R0: &'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
/// of the container type; the caller is guaranteed to check the
/// general invariants that apply to all unified containers.
fn from_inner(items: Vec<Item<Self::DataItem>>) -> Self;
fn from_inner(revision: Revision, items: Vec<Item<Self::DataItem>>) -> Self;
fn network_hrp(network: &Network) -> &'static str {
match network {
Network::Main => Self::MAINNET,
Network::Test => Self::TESTNET,
Network::Regtest => Self::REGTEST,
fn network_hrp(revision: Revision, network: &Network) -> &'static str {
match (revision, network) {
(Revision::R0, Network::Main) => Self::MAINNET_R0,
(Revision::R0, Network::Test) => Self::TESTNET_R0,
(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> {
if hrp == Self::MAINNET {
if hrp == Self::MAINNET_R0 || hrp == Self::MAINNET_R1 {
Some(Network::Main)
} else if hrp == Self::TESTNET {
} else if hrp == Self::TESTNET_R0 || hrp == Self::TESTNET_R1 {
Some(Network::Test)
} else if hrp == Self::REGTEST {
} else if hrp == Self::REGTEST_R0 || hrp == Self::REGTEST_R1 {
Some(Network::Regtest)
} else {
None
@ -434,11 +466,13 @@ pub(crate) mod private {
}
/// Parse the items of the unified container.
#[allow(clippy::type_complexity)]
fn parse_items<T: Into<Vec<u8>>>(
hrp: &str,
buf: T,
) -> Result<Vec<Item<Self::DataItem>>, ParseError> {
fn read_receiver<R: SealedDataItem>(
) -> Result<(Revision, Vec<Item<Self::DataItem>>), ParseError> {
fn read_item<R: SealedDataItem>(
revision: Revision,
mut cursor: &mut std::io::Cursor<&[u8]>,
) -> Result<Item<R>, ParseError> {
let typecode = CompactSize::read(&mut cursor)
@ -471,7 +505,9 @@ pub(crate) mod private {
let data = &buf[cursor.position() as usize..addr_end as usize];
let result = match Typecode::try_from(typecode)? {
Typecode::Data(tc) => Item::Data(R::parse(tc, data)?),
Typecode::Metadata(tc) => Item::Metadata(MetadataItem::parse(tc, data)?),
Typecode::Metadata(tc) => {
Item::Metadata(MetadataItem::parse(revision, tc, data)?)
}
};
cursor.set_position(addr_end);
Ok(result)
@ -498,19 +534,25 @@ 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 result = vec![];
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());
Ok(result)
Ok((revision, result))
}
/// A private function that constructs a unified container with the
/// specified items, which must be in ascending typecode order.
fn try_from_items_internal(items: Vec<Item<Self::DataItem>>) -> Result<Self, ParseError> {
fn try_from_items_internal(
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;
@ -538,12 +580,13 @@ pub(crate) mod private {
Err(ParseError::OnlyTransparent)
} else {
// All checks pass!
Ok(Self::from_inner(items))
Ok(Self::from_inner(revision, items))
}
}
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))
}
}
}
@ -563,9 +606,12 @@ pub trait Encoding: private::SealedContainer {
/// * the item list may not contain two items having the same typecode
/// * the item list may not contain only transparent items (or no items)
/// * the item list may not contain both P2PKH and P2SH items.
fn try_from_items(mut items: Vec<Item<Self::DataItem>>) -> Result<Self, ParseError> {
fn try_from_items(
revision: Revision,
mut items: Vec<Item<Self::DataItem>>,
) -> Result<Self, ParseError> {
items.sort_unstable_by(Item::encoding_order);
Self::try_from_items_internal(items)
Self::try_from_items_internal(revision, items)
}
/// Decodes a unified container from its string representation, preserving
@ -592,7 +638,7 @@ pub trait Encoding: private::SealedContainer {
/// ordering of the contained items such that it correctly obeys round-trip
/// serialization invariants.
fn encode(&self, network: &Network) -> String {
let hrp = Self::network_hrp(network);
let hrp = Self::network_hrp(self.revision(), network);
bech32::encode(
hrp,
self.to_jumbled_bytes(hrp).to_base32(),
@ -609,4 +655,8 @@ pub trait Container {
/// Returns the items in encoding order.
fn items_as_parsed(&self) -> &[Item<Self::DataItem>];
/// Returns the revision of the ZIP 316 standard that this unified container
/// conforms to.
fn revision(&self) -> Revision;
}

View File

@ -1,6 +1,6 @@
use zcash_protocol::{PoolType, ShieldedProtocol};
use super::{private::SealedDataItem, DataTypecode, Item, ParseError};
use super::{private::SealedDataItem, DataTypecode, Item, ParseError, Revision};
use std::{cmp, convert::TryInto};
@ -69,7 +69,7 @@ impl SealedDataItem for Receiver {
/// # use std::convert::Infallible;
/// # use std::error::Error;
/// use zcash_address::{
/// unified::{self, Container, Encoding, Item},
/// unified::{self, Container, Encoding, Item, Revision},
/// ConversionError, TryFromRawAddress, ZcashAddress,
/// };
///
@ -100,13 +100,16 @@ impl SealedDataItem for Receiver {
/// let receivers: Vec<unified::Receiver> = ua.receivers();
///
/// // And we can create the UA from a list of receivers:
/// let new_ua = unified::Address::try_from_items(receivers.into_iter().map(Item::Data).collect())?;
/// let new_ua = unified::Address::try_from_items(Revision::R0, receivers.into_iter().map(Item::Data).collect())?;
/// assert_eq!(new_ua, ua);
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Address(pub(crate) Vec<Item<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.
@ -115,7 +118,7 @@ impl Address {
/// of a type that wallet supports from the result.
pub fn receivers(&self) -> Vec<Receiver> {
let mut result = self
.0
.receivers
.iter()
.filter_map(|item| match item {
Item::Data(r) => Some(r.clone()),
@ -130,7 +133,7 @@ impl Address {
impl Address {
/// Returns whether this address has the ability to receive transfers of the given pool type.
pub fn has_receiver_of_type(&self, pool_type: PoolType) -> bool {
self.0.iter().any(|item| match item {
self.receivers.iter().any(|item| match item {
Item::Data(Receiver::Orchard(_)) => {
pool_type == PoolType::Shielded(ShieldedProtocol::Orchard)
}
@ -147,14 +150,14 @@ impl Address {
/// Returns whether this address contains the given receiver.
pub fn contains_receiver(&self, receiver: &Receiver) -> bool {
self.0
self.receivers
.iter()
.any(|item| matches!(item, Item::Data(r) if r == receiver))
}
/// Returns whether this address can receive a memo.
pub fn can_receive_memo(&self) -> bool {
self.0.iter().any(|r| {
self.receivers.iter().any(|r| {
matches!(
r,
Item::Data(Receiver::Sapling(_)) | Item::Data(Receiver::Orchard(_))
@ -164,25 +167,45 @@ impl 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].
///
/// [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].
///
/// [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.
const REGTEST: &'static str = "uregtest";
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified Address.
const REGTEST_R0: &'static str = "uregtest";
fn from_inner(receivers: Vec<Item<Self::DataItem>>) -> Self {
Self(receivers)
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified Address.
///
/// 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,
}
}
}
@ -191,7 +214,11 @@ impl super::Container for Address {
type DataItem = Receiver;
fn items_as_parsed(&self) -> &[Item<Receiver>] {
&self.0
&self.receivers
}
fn revision(&self) -> Revision {
self.revision
}
}
@ -206,7 +233,7 @@ pub mod testing {
};
use super::{Address, Receiver};
use crate::unified::{DataTypecode, Item};
use crate::unified::{DataTypecode, Item, Revision};
use zcash_encoding::MAX_COMPACT_SIZE;
prop_compose! {
@ -271,9 +298,12 @@ pub mod testing {
arb_typecodes()
.prop_flat_map(arb_unified_address_receivers)
.prop_map(|rs| {
let mut items = rs.into_iter().map(Item::Data).collect::<Vec<_>>();
items.sort_unstable_by(Item::encoding_order);
Address(items)
let mut receivers = rs.into_iter().map(Item::Data).collect::<Vec<_>>();
receivers.sort_unstable_by(Item::encoding_order);
Address {
revision: Revision::R0,
receivers,
}
})
}
}
@ -287,7 +317,7 @@ mod tests {
use crate::{
kind::unified::{private::SealedContainer, Encoding},
unified::{address::testing::arb_unified_address, Item, Typecode},
unified::{address::testing::arb_unified_address, Item, Revision, Typecode},
Network,
};
@ -322,7 +352,7 @@ mod tests {
0x7b, 0x28, 0x69, 0xc9, 0x84,
];
assert_eq!(
Address::parse_internal(Address::MAINNET, &invalid_padding[..]),
Address::parse_internal(Address::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -337,7 +367,7 @@ mod tests {
0x4b, 0x31, 0xee, 0x5a,
];
assert_eq!(
Address::parse_internal(Address::MAINNET, &truncated_padding[..]),
Address::parse_internal(Address::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -362,7 +392,7 @@ mod tests {
0xc6, 0x5e, 0x68, 0xa2, 0x78, 0x6c, 0x9e,
];
assert_matches!(
Address::parse_internal(Address::MAINNET, &truncated_sapling_data[..]),
Address::parse_internal(Address::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -375,7 +405,7 @@ mod tests {
0xe6, 0x70, 0x36, 0x5b, 0x7b, 0x9e,
];
assert_matches!(
Address::parse_internal(Address::MAINNET, &truncated_after_sapling_typecode[..]),
Address::parse_internal(Address::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -384,13 +414,16 @@ mod tests {
fn duplicate_typecode() {
// Construct and serialize an invalid UA. This must be done using private
// methods, as the public API does not permit construction of such invalid values.
let ua = Address(vec![
Item::Data(Receiver::Sapling([1; 43])),
Item::Data(Receiver::Sapling([2; 43])),
]);
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
let ua = Address {
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!(
Address::parse_internal(Address::MAINNET, &encoded[..]),
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
);
}
@ -399,14 +432,17 @@ mod tests {
fn p2pkh_and_p2sh() {
// Construct and serialize an invalid UA. This must be done using private
// methods, as the public API does not permit construction of such invalid values.
let ua = Address(vec![
Item::Data(Receiver::P2pkh([0; 20])),
Item::Data(Receiver::P2sh([0; 20])),
]);
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
let ua = Address {
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
assert_eq!(
Address::parse_internal(Address::MAINNET, &encoded[..]),
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::BothP2phkAndP2sh)
);
}
@ -415,14 +451,17 @@ mod tests {
fn addresses_out_of_order() {
// Construct and serialize an invalid UA. This must be done using private
// methods, as the public API does not permit construction of such invalid values.
let ua = Address(vec![
Item::Data(Receiver::Sapling([0; 43])),
Item::Data(Receiver::P2pkh([0; 20])),
]);
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
let ua = Address {
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
assert_eq!(
Address::parse_internal(Address::MAINNET, &encoded[..]),
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::InvalidTypecodeOrder)
);
}
@ -441,7 +480,7 @@ mod tests {
// with only one of them we don't have sufficient data for F4Jumble (so we hit a
// different error).
assert_matches!(
Address::parse_internal(Address::MAINNET, &encoded[..]),
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -449,15 +488,18 @@ mod tests {
#[test]
fn receivers_are_sorted() {
// Construct a UA with receivers in an unsorted order.
let ua = Address(vec![
Item::Data(Receiver::P2pkh([0; 20])),
Item::Data(Receiver::Orchard([0; 43])),
Item::Data(Receiver::Unknown {
typecode: 0xff,
data: vec![],
}),
Item::Data(Receiver::Sapling([0; 43])),
]);
let ua = Address {
revision: Revision::R0,
receivers: vec![
Item::Data(Receiver::P2pkh([0; 20])),
Item::Data(Receiver::Orchard([0; 43])),
Item::Data(Receiver::Unknown {
typecode: 0xff,
data: vec![],
}),
Item::Data(Receiver::Sapling([0; 43])),
],
};
// `Address::receivers` sorts the receivers in priority order.
assert_eq!(

View File

@ -2,7 +2,7 @@ use std::convert::TryInto;
use super::{
private::{SealedContainer, SealedDataItem},
Container, DataTypecode, Encoding, Item, ParseError,
Container, DataTypecode, Encoding, Item, ParseError, Revision,
};
/// The set of known FVKs for Unified FVKs.
@ -79,7 +79,7 @@ impl SealedDataItem for Fvk {
///
/// ```
/// # use std::error::Error;
/// use zcash_address::unified::{self, Container, Encoding, Item};
/// use zcash_address::unified::{self, Container, Encoding, Item, Revision};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv";
@ -91,44 +91,68 @@ impl SealedDataItem for Fvk {
/// let fvks: &[Item<unified::Fvk>] = ufvk.items_as_parsed();
///
/// // And we can create the UFVK from a list of FVKs:
/// let new_ufvk = unified::Ufvk::try_from_items(fvks.to_vec())?;
/// let new_ufvk = unified::Ufvk::try_from_items(Revision::R0, fvks.to_vec())?;
/// assert_eq!(new_ufvk, ufvk);
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Ufvk(pub(crate) Vec<Item<Fvk>>);
pub struct Ufvk {
pub(crate) revision: Revision,
pub(crate) fvks: Vec<Item<Fvk>>,
}
impl Container for Ufvk {
type DataItem = Fvk;
fn items_as_parsed(&self) -> &[Item<Fvk>] {
&self.0
&self.fvks
}
fn revision(&self) -> Revision {
self.revision
}
}
impl Encoding 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].
///
/// [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].
///
/// [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.
const REGTEST: &'static str = "uviewregtest";
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified FVK.
const REGTEST_R0: &'static str = "uviewregtest";
fn from_inner(fvks: Vec<Item<Self::DataItem>>) -> Self {
Self(fvks)
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified FVK.
///
/// 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 }
}
}
@ -141,7 +165,7 @@ mod tests {
use super::{Fvk, ParseError, Ufvk};
use crate::{
kind::unified::{private::SealedContainer, Encoding},
unified::{Item, Typecode},
unified::{Item, Revision, Typecode},
Network,
};
@ -199,9 +223,9 @@ mod tests {
shielded in arb_shielded_fvk(),
transparent in prop::option::of(arb_transparent_fvk()),
) -> Ufvk {
let mut items: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
items.sort_unstable_by(Item::encoding_order);
Ufvk(items)
let mut fvks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
fvks.sort_unstable_by(Item::encoding_order);
Ufvk { revision: Revision::R0, fvks }
}
}
@ -233,7 +257,7 @@ mod tests {
0xdf, 0x63, 0xe7, 0xef, 0x65, 0x6b, 0x18, 0x23, 0xf7, 0x3e, 0x35, 0x7c, 0xf3, 0xc4,
];
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &invalid_padding[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -251,7 +275,7 @@ mod tests {
0x43, 0x8e, 0xc0, 0x3e, 0x9f, 0xf4, 0xf1, 0x80, 0x32, 0xcf, 0x2f, 0x7e, 0x7f, 0x91,
];
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_padding[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -283,7 +307,7 @@ mod tests {
0x8c, 0x7a, 0xbf, 0x7b, 0x9a, 0xdd, 0xee, 0x18, 0x2c, 0x2d, 0xc2, 0xfc,
];
assert_matches!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_sapling_data[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -298,7 +322,7 @@ mod tests {
0x54, 0xd1, 0x9e, 0xec, 0x8b, 0xef, 0x35, 0xb8, 0x44, 0xdd, 0xab, 0x9a, 0x8d,
];
assert_matches!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_after_sapling_typecode[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -307,13 +331,16 @@ mod tests {
fn duplicate_typecode() {
// Construct and serialize an invalid Ufvk. This must be done using private
// methods, as the public API does not permit construction of such invalid values.
let ufvk = Ufvk(vec![
Item::Data(Fvk::Sapling([1; 128])),
Item::Data(Fvk::Sapling([2; 128])),
]);
let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET);
let ufvk = Ufvk {
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!(
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
);
}
@ -331,7 +358,7 @@ mod tests {
];
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]),
Err(ParseError::OnlyTransparent)
);
}

View File

@ -2,7 +2,7 @@ use std::convert::TryInto;
use super::{
private::{SealedContainer, SealedDataItem},
Container, DataTypecode, Encoding, Item, ParseError,
Container, DataTypecode, Encoding, Item, ParseError, Revision,
};
/// The set of known IVKs for Unified IVKs.
@ -84,7 +84,7 @@ impl SealedDataItem for Ivk {
///
/// ```
/// # use std::error::Error;
/// use zcash_address::unified::{self, Container, Encoding, Item};
/// use zcash_address::unified::{self, Container, Encoding, Item, Revision};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq";
@ -96,44 +96,68 @@ impl SealedDataItem for Ivk {
/// let ivks: &[Item<unified::Ivk>] = uivk.items_as_parsed();
///
/// // And we can create the UIVK from a vector of IVKs:
/// let new_uivk = unified::Uivk::try_from_items(ivks.to_vec())?;
/// let new_uivk = unified::Uivk::try_from_items(Revision::R0, ivks.to_vec())?;
/// assert_eq!(new_uivk, uivk);
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Uivk(pub(crate) Vec<Item<Ivk>>);
pub struct Uivk {
pub(crate) revision: Revision,
pub(crate) ivks: Vec<Item<Ivk>>,
}
impl Container for Uivk {
type DataItem = Ivk;
fn items_as_parsed(&self) -> &[Item<Ivk>] {
&self.0
&self.ivks
}
fn revision(&self) -> Revision {
self.revision
}
}
impl Encoding 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].
///
/// [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].
///
/// [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.
const REGTEST: &'static str = "uivkregtest";
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified IVK.
const REGTEST_R0: &'static str = "uivkregtest";
fn from_inner(ivks: Vec<Item<Self::DataItem>>) -> Self {
Self(ivks)
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified IVK.
///
/// 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 }
}
}
@ -150,7 +174,7 @@ mod tests {
use super::{Ivk, ParseError, Uivk};
use crate::{
kind::unified::{private::SealedContainer, Encoding},
unified::{Item, Typecode},
unified::{Item, Revision, Typecode},
Network,
};
@ -192,9 +216,9 @@ mod tests {
shielded in arb_shielded_ivk(),
transparent in prop::option::of(arb_transparent_ivk()),
) -> Uivk {
let mut items: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
items.sort_unstable_by(Item::encoding_order);
Uivk(items)
let mut ivks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
ivks.sort_unstable_by(Item::encoding_order);
Uivk { revision: Revision::R0, ivks }
}
}
@ -224,7 +248,7 @@ mod tests {
0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15,
];
assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -240,7 +264,7 @@ mod tests {
0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47,
];
assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -268,7 +292,7 @@ mod tests {
0xf5, 0xd5, 0x8a, 0xb5, 0x1a,
];
assert_matches!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -281,7 +305,7 @@ mod tests {
0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74,
];
assert_matches!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_after_sapling_typecode[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -289,10 +313,13 @@ mod tests {
#[test]
fn duplicate_typecode() {
// Construct and serialize an invalid UIVK.
let uivk = Uivk(vec![
Item::Data(Ivk::Sapling([1; 64])),
Item::Data(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);
assert_eq!(
Uivk::decode(&encoded),
@ -313,7 +340,7 @@ mod tests {
];
assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &encoded[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &encoded[..]),
Err(ParseError::OnlyTransparent)
);
}

View File

@ -8,7 +8,7 @@ use {
unified::{
self,
address::{test_vectors::TEST_VECTORS, Receiver},
Item,
Item, Revision,
},
Network, ToAddress, ZcashAddress,
},
@ -40,7 +40,13 @@ fn unified() {
.map(Item::Data)
.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
let addr: ZcashAddress = tv.unified_addr.parse().unwrap();

View File

@ -1,7 +1,7 @@
//! Structs for handling supported address types.
use zcash_address::{
unified::{self, Container, DataTypecode, Encoding, Item, Typecode},
unified::{self, Container, DataTypecode, Encoding, Item, Revision, Typecode},
ConversionError, ToAddress, TryFromRawAddress, ZcashAddress,
};
use zcash_primitives::legacy::TransparentAddress;
@ -278,6 +278,11 @@ impl UnifiedAddress {
.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() {
Revision::R1
} else {
Revision::R0
},
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))
@ -337,7 +342,7 @@ impl Receiver {
Receiver::Orchard(addr) => {
let receiver =
unified::Item::Data(unified::Receiver::Orchard(addr.to_raw_address_bytes()));
let ua = unified::Address::try_from_items(vec![receiver])
let ua = unified::Address::try_from_items(Revision::R0, vec![receiver])
.expect("A unified address may contain a single Orchard receiver.");
ZcashAddress::from_unified(net, ua)
}

View File

@ -3,7 +3,7 @@ use std::{
error,
fmt::{self, Display},
};
use zcash_address::unified::{self, Container, Encoding, Item, MetadataItem, Typecode};
use zcash_address::unified::{self, Container, Encoding, Item, MetadataItem, Revision, Typecode};
use zcash_primitives::consensus::BlockHeight;
use zcash_protocol::consensus;
use zip32::{AccountId, DiversifierIndex};
@ -855,6 +855,11 @@ impl UnifiedFullViewingKey {
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
zcash_address::unified::Ufvk::try_from_items(
if self.expiry_height().is_some() || self.expiry_time().is_some() {
Revision::R1
} else {
Revision::R0
},
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))
@ -1159,6 +1164,11 @@ impl UnifiedIncomingViewingKey {
.chain(self.expiry_time.map(unified::MetadataItem::ExpiryTime));
zcash_address::unified::Uivk::try_from_items(
if self.expiry_height.is_some() || self.expiry_time.is_some() {
Revision::R1
} else {
Revision::R0
},
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))