From aea04d1ad22d74821dfb17b85902b206a1c54d17 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 16 Feb 2024 14:49:47 -0700 Subject: [PATCH] `zcash_address`: Add support for ZIP 316, Revision 1 --- components/zcash_address/CHANGELOG.md | 5 +- components/zcash_address/src/encoding.rs | 17 +- components/zcash_address/src/kind/unified.rs | 110 +++++++++---- .../zcash_address/src/kind/unified/address.rs | 150 +++++++++++------- .../zcash_address/src/kind/unified/fvk.rs | 83 ++++++---- .../zcash_address/src/kind/unified/ivk.rs | 79 ++++++--- components/zcash_address/src/test_vectors.rs | 10 +- zcash_keys/src/address.rs | 9 +- zcash_keys/src/keys.rs | 12 +- 9 files changed, 327 insertions(+), 148 deletions(-) diff --git a/components/zcash_address/CHANGELOG.md b/components/zcash_address/CHANGELOG.md index 00e90bc81..c1b1187b9 100644 --- a/components/zcash_address/CHANGELOG.md +++ b/components/zcash_address/CHANGELOG.md @@ -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 diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index a39b24db2..e212ba2f6 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -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]))] + }), }, ); diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 5cda10971..ef350d60d 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -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 { - match typecode { - MetadataTypecode::ExpiryHeight => data + pub fn parse( + revision: Revision, + typecode: MetadataTypecode, + data: &[u8], + ) -> Result { + 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>) -> Self; + fn from_inner(revision: Revision, items: Vec>) -> 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 { + 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 { - 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>>( hrp: &str, buf: T, - ) -> Result>, ParseError> { - fn read_receiver( + ) -> Result<(Revision, Vec>), ParseError> { + fn read_item( + revision: Revision, mut cursor: &mut std::io::Cursor<&[u8]>, ) -> Result, 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>) -> Result { + fn try_from_items_internal( + revision: Revision, + items: Vec>, + ) -> Result { 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>>(hrp: &str, buf: T) -> Result { - 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>) -> Result { + fn try_from_items( + revision: Revision, + mut items: Vec>, + ) -> Result { 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]; + + /// Returns the revision of the ZIP 316 standard that this unified container + /// conforms to. + fn revision(&self) -> Revision; } diff --git a/components/zcash_address/src/kind/unified/address.rs b/components/zcash_address/src/kind/unified/address.rs index 7b6e0f24f..4b91f6e42 100644 --- a/components/zcash_address/src/kind/unified/address.rs +++ b/components/zcash_address/src/kind/unified/address.rs @@ -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 = 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>); +pub struct Address { + pub(crate) revision: Revision, + pub(crate) receivers: Vec>, +} 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 { 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>) -> 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>) -> Self { + Self { + revision, + receivers, + } } } @@ -191,7 +214,11 @@ impl super::Container for Address { type DataItem = Receiver; fn items_as_parsed(&self) -> &[Item] { - &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::>(); - items.sort_unstable_by(Item::encoding_order); - Address(items) + let mut receivers = rs.into_iter().map(Item::Data).collect::>(); + 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!( diff --git a/components/zcash_address/src/kind/unified/fvk.rs b/components/zcash_address/src/kind/unified/fvk.rs index 1d617299a..f12adcd96 100644 --- a/components/zcash_address/src/kind/unified/fvk.rs +++ b/components/zcash_address/src/kind/unified/fvk.rs @@ -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> { /// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv"; @@ -91,44 +91,68 @@ impl SealedDataItem for Fvk { /// let fvks: &[Item] = ufvk.items_as_parsed(); /// /// // And we can create the UFVK from a list of FVKs: -/// let new_ufvk = unified::Ufvk::try_from_items(fvks.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>); +pub struct Ufvk { + pub(crate) revision: Revision, + pub(crate) fvks: Vec>, +} impl Container for Ufvk { type DataItem = Fvk; fn items_as_parsed(&self) -> &[Item] { - &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>) -> 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>) -> 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) ); } diff --git a/components/zcash_address/src/kind/unified/ivk.rs b/components/zcash_address/src/kind/unified/ivk.rs index 2dd39147e..5abe1d20b 100644 --- a/components/zcash_address/src/kind/unified/ivk.rs +++ b/components/zcash_address/src/kind/unified/ivk.rs @@ -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> { /// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq"; @@ -96,44 +96,68 @@ impl SealedDataItem for Ivk { /// let ivks: &[Item] = 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>); +pub struct Uivk { + pub(crate) revision: Revision, + pub(crate) ivks: Vec>, +} impl Container for Uivk { type DataItem = Ivk; fn items_as_parsed(&self) -> &[Item] { - &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>) -> 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>) -> 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) ); } diff --git a/components/zcash_address/src/test_vectors.rs b/components/zcash_address/src/test_vectors.rs index 598096da6..991d16209 100644 --- a/components/zcash_address/src/test_vectors.rs +++ b/components/zcash_address/src/test_vectors.rs @@ -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(); diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index cf0a48234..96353a085 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -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) } diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index 563de4bec..1c01c54ad 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -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))